diff --git a/.core/TODO.md b/.core/TODO.md deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e5487ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +permissions: + contents: read + +env: + GOFLAGS: -buildvcs=false + GOWORK: "off" + GOPROXY: "direct" + GOSUMDB: "off" + +jobs: + test: + name: Test + Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + - name: Test with coverage + working-directory: go + run: go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./... + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: go/coverage.out + flags: unittests + fail_ci_if_error: false + + lint: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + - uses: golangci/golangci-lint-action@v9 + with: + version: latest + working-directory: go + args: --timeout=5m --tests=false + + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + - name: Test for coverage + working-directory: go + run: go test -coverprofile=coverage.out -covermode=atomic -count=1 ./... + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v6 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.organization=dappcore + -Dsonar.projectKey=dappcore_api + -Dsonar.sources=go + -Dsonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/*_test.go + -Dsonar.tests=go + -Dsonar.test.inclusions=**/*_test.go + -Dsonar.go.coverage.reportPaths=go/coverage.out diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ee1d225 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,32 @@ +[submodule "external/go"] + path = external/go + url = https://github.com/dappcore/go.git + branch = dev +[submodule "external/go-process"] + path = external/go-process + url = https://github.com/dappcore/go-process.git + branch = dev +[submodule "external/go-scm"] + path = external/go-scm + url = https://github.com/dappcore/go-scm.git + branch = dev +[submodule "external/go-proxy"] + path = external/go-proxy + url = https://github.com/dappcore/go-proxy.git + branch = dev +[submodule "external/go-ws"] + path = external/go-ws + url = https://github.com/dappcore/go-ws.git + branch = dev +[submodule "external/go-inference"] + path = external/go-inference + url = https://github.com/dappcore/go-inference.git + branch = dev +[submodule "external/go-io"] + path = external/go-io + url = https://github.com/dappcore/go-io.git + branch = dev +[submodule "external/go-log"] + path = external/go-log + url = https://github.com/dappcore/go-log.git + branch = dev diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..138594a --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,62 @@ +# Woodpecker CI pipeline. +# Server: ci.lthn.sh. +# Lint + test run in parallel; sonar (lthn.sh + sonarcloud) + codecov fan +# out from go-test. + +when: + - event: [push, manual, tag] + branch: [dev, main] + +steps: + - name: golangci-lint + image: golangci/golangci-lint:latest-alpine + depends_on: [] + environment: + GOFLAGS: -buildvcs=false + GOWORK: "off" + commands: + - cd go && golangci-lint run --timeout=5m ./... + + - name: go-test + image: golang:1.26-alpine + depends_on: [] + environment: + GOFLAGS: -buildvcs=false + GOWORK: "off" + CGO_ENABLED: "1" + commands: + - apk add --no-cache git build-base + - cd go && go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./... + + - name: sonar-internal + image: sonarsource/sonar-scanner-cli:latest + depends_on: [go-test] + environment: + SONAR_HOST_URL: https://sonar.lthn.sh + SONAR_TOKEN: + from_secret: sonar_token + commands: + - sonar-scanner + + - name: sonarcloud + image: sonarsource/sonar-scanner-cli:latest + depends_on: [go-test] + environment: + SONAR_TOKEN: + from_secret: sonarcloud_token + commands: + - sonar-scanner + -Dsonar.host.url=https://sonarcloud.io + -Dsonar.organization=dappcore + -Dsonar.projectKey=dappcore_api + -Dsonar.go.coverage.reportPaths=go/coverage.out + + - name: codecov + image: alpine:3.20 + depends_on: [go-test] + environment: + CODECOV_TOKEN: + from_secret: codecov_token + commands: + - apk add --no-cache curl bash git + - cd go && bash <(curl -s https://codecov.io/bash) -f coverage.out -t "$CODECOV_TOKEN" -F unittests diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3524590 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ + + +# Agent Notes + +This repository is the Go Core API framework. Treat it as infrastructure: keep +changes narrow, preserve the public `RouteGroup` and `Engine` contracts, and +verify the whole module before handing work back. + +## Code Map + +- `api.go` owns `Engine`, route group registration, handler construction, and + graceful serving. +- `options.go` contains public `With*` options. New middleware should enter + through this file unless it is strictly internal. +- `response.go`, `middleware.go`, `authentik.go`, `sse.go`, `websocket.go`, + `graphql.go`, `swagger.go`, `openapi.go`, `export.go`, and `codegen.go` + implement the user-facing HTTP features. +- `bridge.go` maps `ToolDescriptor` values to REST endpoints and OpenAPI + descriptions. +- `cmd/api` wires Core CLI actions for spec export and SDK generation. +- `cmd/gateway` builds the provider gateway binary. +- `pkg/provider` contains provider discovery, registry, and reverse proxy + support. +- `pkg/stream` contains declarative stream route groups. + +## Compliance Rules + +Follow the v0.9.0 Core compliance shape. Use `dappco.re/go` wrappers for JSON, +errors, formatting, strings, buffers, filesystem, process, and environment +helpers whenever a wrapper exists. Do not add files named `ax7*.go`, versioned +test files, or monolithic compliance dumps. + +For every production source file with public symbols, keep tests and examples +beside that file. Test names use `Test__Good`, +`Test__Bad`, and `Test__Ugly`. Examples use +`Example` or a valid lowercase suffix variant and print through Core +`Println`. + +## Before Stopping + +Use the exact repository gate, with `GOWORK=off` for Go commands: + +```bash +GOWORK=off go mod tidy +GOWORK=off go vet ./... +GOWORK=off go test -count=1 ./... +gofmt -l . +bash /Users/snider/Code/core/go/tests/cli/v090-upgrade/audit.sh . +``` + +If the sandbox cannot write the default Go build cache, set `GOCACHE` to a +temporary directory while running the same commands. diff --git a/CLAUDE.md b/CLAUDE.md index a538231..4fa2093 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,56 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Core API is the REST framework for the Lethean ecosystem, providing both a **Go HTTP engine** (Gin-based, with OpenAPI generation, WebSocket/SSE, ToolBridge) and a **PHP Laravel package** (rate limiting, webhooks, API key management, OpenAPI documentation). Both halves serve the same purpose in their respective stacks. -Module: `dappco.re/go/core/api` | Package: `dappco.re/php/service` | Licence: EUPL-1.2 +Module: `dappco.re/go/api` | Package: `dappco.re/php/service` | Licence: EUPL-1.2 + +## Repo Layout + +``` +core/api/ +├── go.work ← workspace root (one level above the module) +├── external// ← git submodules tracking dev branches on github +├── go/ ← Go module root (module dappco.re/go/api) +├── php/ ← PHP package +├── docs/ ← engine docs (symlinked from go/) +├── sdk-config/ ← cross-language SDK gen configs +└── scripts/ ← cross-language build helpers +``` + +Cross-language symmetry target: `dappco.re//api/` ↔ `core/api//` (Go today, PHP today, TS+Py later). + +## Go Resolution Modes + +Two ways the same `go/go.mod` resolves dappco.re/go/* deps: + +| Mode | When | What runs | +|------|------|-----------| +| **Workspace ON** (default for devs) | `go build` / `go test` from any subdir of `core/api/` | Walks up to `go.work`, uses local `external/` checkouts at submodule pin (typically dev tip). Fast iteration; finds upstream bugs early. | +| **`GOWORK=off`** | Woodpecker CI | Pure go.mod, fetches the pinned tag from the proxy. Reproducible builds. No replace directives — 0% policy intact. | + +### Working with submodules + +```bash +git clone --recursive https://github.com/dappcore/api.git # full dev workspace +git submodule update --init --recursive # if cloned without --recursive + +# Bump a single dep to its current dev tip +git submodule update --remote external/go-process + +# See latest tag in a dep +( cd external/go-process && git tag --sort=-v:refname | head ) + +# When ready to bump the api go.mod to that dep's new tag +( cd go && go get dappco.re/go/process@v0.11.0 && go mod tidy ) +``` + +### Workspace mode caveat + +Workspace mode validates each `external//go.sum` against current proxy bits. If an upstream repo has a stale or missing go.sum entry, the build errors with `verifying ...: checksum mismatch`. Two ways to handle: + +1. **Fix upstream**: `cd external/ && go mod tidy`, commit + push to that repo's dev, then `git submodule update --remote external/` here. +2. **CI mode locally**: `GOWORK=off go test ./go/...` — uses the api repo's pinned hashes. + +This is the "find bugs as they roll out" payoff: workspace mode surfaces stale-sum issues across the whole tree. ## Build and Test Commands @@ -36,7 +85,7 @@ composer lint # Laravel Pint (PSR-12) ./vendor/bin/pint --dirty # Format changed files ``` -Tests live in `src/php/src/Api/Tests/Feature/` (in-source) and `src/php/tests/` (standalone). +Tests live in `php/src/Api/Tests/Feature/` (in-source) and `php/tests/` (standalone). ## Architecture @@ -63,15 +112,15 @@ engine.Serve(ctx) **CLI** (`cmd/api/`): Registers `core api spec` and `core api sdk` commands. -### PHP Package (`src/php/`) +### PHP Package (`php/`) Three namespace roots: | Namespace | Path | Role | |-----------|------|------| -| `Core\Front\Api` | `src/php/src/Front/Api/` | API frontage — middleware, versioning, auto-discovered provider | -| `Core\Api` | `src/php/src/Api/` | Backend — auth, scopes, models, webhooks, OpenAPI docs | -| `Core\Website\Api` | `src/php/src/Website/Api/` | Documentation UI — controllers, Blade views, web routes | +| `Core\Front\Api` | `php/src/Front/Api/` | API frontage — middleware, versioning, auto-discovered provider | +| `Core\Api` | `php/src/Api/` | Backend — auth, scopes, models, webhooks, OpenAPI docs | +| `Core\Website\Api` | `php/src/Website/Api/` | Documentation UI — controllers, Blade views, web routes | Boot chain: `Front\Api\Boot` (auto-discovered) fires `ApiRoutesRegistering` -> `Api\Boot` registers middleware and routes. @@ -93,7 +142,7 @@ Key services: `WebhookService`, `RateLimitService`, `IpRestrictionService`, `Ope | Go module | Role | |-----------|------| -| `dappco.re/go/core/cli` | CLI command registration | +| `dappco.re/go/cli` | CLI command registration for the nested `cmd/api` module | | `github.com/gin-gonic/gin` | HTTP router | | `github.com/casbin/casbin/v2` | Authorisation policies | | `github.com/coreos/go-oidc/v3` | OIDC / Authentik | diff --git a/README.md b/README.md new file mode 100644 index 0000000..f85365a --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ + + +# dappco.re/go/api + +> Gin-based HTTP framework + multi-language REST gateway for the Core ecosystem. + +[![CI](https://github.com/dappcore/api/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/dappcore/api/actions/workflows/ci.yml) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=alert_status)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Coverage](https://codecov.io/gh/dappcore/api/branch/dev/graph/badge.svg)](https://codecov.io/gh/dappcore/api) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=security_rating)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=code_smells)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=dappcore_api&metric=ncloc)](https://sonarcloud.io/dashboard?id=dappcore_api) +[![Go Reference](https://pkg.go.dev/badge/dappco.re/go/api.svg)](https://pkg.go.dev/dappco.re/go/api) +[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](https://eupl.eu/1.2/en/) + +## Overview + +`dappco.re/go/api` is the Gin-based HTTP framework used by the Core Go +ecosystem. It provides a small `Engine` type, option-driven middleware +configuration, route group mounting, response envelopes, OpenAPI 3.1 +generation, SDK export/codegen helpers, SSE and WebSocket wiring, GraphQL +hosting, Authentik identity middleware, and the `core api` CLI commands. + +The package is a library first. Applications construct an engine, register +one or more `RouteGroup` implementations, then either call `Serve(ctx)` or use +`Handler()` with their own server. + +```go +engine, err := api.New( + api.WithAddr(":8080"), + api.WithRequestID(), + api.WithResponseMeta(), + api.WithSwagger("Core API", "Core service endpoints", "1.0.0"), +) +if err != nil { + return err +} +engine.Register(myRoutes) +return engine.Serve(ctx) +``` + +## Repository Layout + +``` +api/ +├── go/ Go module — module path: dappco.re/go/api +│ ├── api.go, options.go HTTP engine surface +│ ├── cmd/api/ core api spec + sdk CLI subcommands +│ ├── cmd/gateway/ runnable gateway, mounts Core providers +│ ├── pkg/provider/ provider discovery + proxy +│ └── pkg/stream/ SSE + WebSocket route group +├── php/ Laravel Core API package (REST middleware, +│ webhooks, OpenAPI, rate limiting) +├── docs/ Engine docs +├── sdk-config/ Multi-language SDK generator config +├── go.work + external/ Dev workspace mode (see CLAUDE.md) +└── .woodpecker.yml + .github/workflows/ CI (internal + public) +``` + +Cross-language symmetry target: `dappco.re//api/` ↔ +`api//` (Go today, PHP today, TS + Py later). + +## Local Verification + +Run the repository with the workspace disabled when checking this module +in isolation: + +```bash +cd go +GOWORK=off go mod tidy +GOWORK=off go vet ./... +GOWORK=off go test -count=1 ./... +gofmt -l . +bash /Users/snider/Code/core/go/tests/cli/v090-upgrade/audit.sh . +``` + +The audit is part of the development contract. Public symbols need +sibling triplet tests and examples, Core wrappers are used instead of +banned standard library imports, and generated AX7 dump files are not +accepted. + +## CI + +- **Internal** (homelab, full sonar.lthn.sh detail): Woodpecker pipeline + defined in `.woodpecker.yml` — runs lint, test with race + coverage, + and pushes results to `sonar.lthn.sh`. +- **Public** (mirror on github.com, badge surface): GitHub Actions + workflow at `.github/workflows/ci.yml` — runs the same shape and + pushes coverage to Codecov + analysis to SonarCloud. + +## Branch Model + +- `dev` — active development. All Cladius / codex lane work lands here + first. +- `main` — squash-stable. Promotion happens via the squash-and-push gate + on the public mirror only. + +## Licence + +EUPL-1.2 — see [LICENCE](LICENCE). + +## Authorship + +Maintained by Cladius (Snider's in-house Opus persona) via the +`agent/cladius` workspace at `forge.lthn.sh/agent/cladius`. Most +substantive commits land via the codex lane pattern documented in +`factory/`. diff --git a/RFC.md b/RFC.md deleted file mode 100644 index 3b65350..0000000 --- a/RFC.md +++ /dev/null @@ -1,58 +0,0 @@ -# API RFC Notes - -## Handler Metadata Example - -```go -type createWidgetHandler struct{} - -func (h *createWidgetHandler) Describe() api.RouteDescription { - return api.RouteDescription{ - StatusCode: http.StatusCreated, - RequestBody: map[string]any{ - "type": "object", - "properties": map[string]any{ - "name": map[string]any{"type": "string"}, - }, - }, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "id": map[string]any{"type": "string"}, - }, - }, - } -} - -func (h *createWidgetHandler) OperationID() string { return "widgets_create" } -func (h *createWidgetHandler) Tags() []string { return []string{"widgets"} } -func (h *createWidgetHandler) Summary() string { return "Create widget" } -func (h *createWidgetHandler) Description() string { return "Creates a widget." } - -func (h *createWidgetHandler) Render() api.RenderHints { - return api.RenderHints{ - Kind: "form", - Fields: []api.FieldHint{ - {Name: "name", Label: "Name", Type: "text", Required: true}, - }, - Actions: []api.ActionHint{ - {Name: "preview", Label: "Preview", Method: http.MethodGet}, - }, - } -} - -func (g *widgetsGroup) Describe() []api.RouteDescription { - handler := &createWidgetHandler{} - return []api.RouteDescription{ - { - Method: http.MethodPost, - Path: "/", - Handler: handler, - }, - } -} -``` - -When a `RouteDescription` carries a handler that implements `api.Describable` -and/or `api.Renderable`, `SpecBuilder` uses that metadata to populate the -OpenAPI `operationId`, `tags`, `summary`, `description`, and the -`x-render-hints` vendor extension. diff --git a/composer.json b/composer.json index e372a4c..f532ecc 100644 --- a/composer.json +++ b/composer.json @@ -29,15 +29,15 @@ ], "autoload": { "psr-4": { - "Core\\Api\\": "src/php/src/Api/", + "Core\\Api\\": "php/src/Api/", "Core\\Tenant\\": "../php-tenant/", - "Core\\Front\\Api\\": "src/php/src/Front/Api/", - "Core\\Website\\Api\\": "src/php/src/Website/Api/" + "Core\\Front\\Api\\": "php/src/Front/Api/", + "Core\\Website\\Api\\": "php/src/Website/Api/" } }, "autoload-dev": { "psr-4": { - "Core\\Api\\Tests\\": "src/php/tests/" + "Core\\Api\\Tests\\": "php/tests/" } }, "extra": { diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a208dd8 --- /dev/null +++ b/composer.lock @@ -0,0 +1,7388 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5e523ca6ef7f529abf0e56b016b76b93", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dedoc/scramble", + "version": "v0.13.22", + "source": { + "type": "git", + "url": "https://github.com/dedoc/scramble.git", + "reference": "b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96", + "reference": "b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "myclabs/deep-copy": "^1.12", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "larastan/larastan": "^3.3", + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.34|^3.7|^4.4", + "pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5|^11.5.3|^12.5.12", + "spatie/laravel-permission": "^6.10|^7.2", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" + } + ], + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", + "keywords": [ + "documentation", + "laravel", + "openapi" + ], + "support": { + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.13.22" + }, + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2026-04-27T20:30:51+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.0.5", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" + }, + "time": "2026-04-01T20:38:03+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.58.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/6172ae1f44ba5d89e111057ee4a4e7c27f5a610d", + "reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-04-26T16:42:04+00:00" + }, + { + "name": "laravel/passport", + "version": "v12.4.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "1d2e0170a52f150d5c35c9a6fc1f7ccebcde7626" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/1d2e0170a52f150d5c35c9a6fc1f7ccebcde7626", + "reference": "1d2e0170a52f150d5c35c9a6fc1f7ccebcde7626", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4|^7.0", + "illuminate/auth": "^9.21|^10.0|^11.0|^12.0", + "illuminate/console": "^9.21|^10.0|^11.0|^12.0", + "illuminate/container": "^9.21|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", + "illuminate/cookie": "^9.21|^10.0|^11.0|^12.0", + "illuminate/database": "^9.21|^10.0|^11.0|^12.0", + "illuminate/encryption": "^9.21|^10.0|^11.0|^12.0", + "illuminate/http": "^9.21|^10.0|^11.0|^12.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "lcobucci/jwt": "^4.3|^5.0", + "league/oauth2-server": "^8.5.3", + "nyholm/psr7": "^1.5", + "php": "^8.0", + "phpseclib/phpseclib": "^2.0|^3.0", + "symfony/console": "^6.0|^7.0", + "symfony/psr-http-message-bridge": "^2.1|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.35|^8.14|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3|^10.5|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/", + "Laravel\\Passport\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "support": { + "issues": "https://github.com/laravel/passport/issues", + "source": "https://github.com/laravel/passport" + }, + "time": "2026-02-19T14:14:05+00:00" + }, + { + "name": "laravel/pennant", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pennant.git", + "reference": "d3d531d0ba640f9d0bd3580990fb205244e956ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pennant/zipball/d3d531d0ba640f9d0bd3580990fb205244e956ca", + "reference": "d3d531d0ba640f9d0bd3580990fb205244e956ca", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/container": "^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/queue": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/finder": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/octane": "^1.4|^2.0", + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Feature": "Laravel\\Pennant\\Feature" + }, + "providers": [ + "Laravel\\Pennant\\PennantServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Pennant\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "A simple, lightweight library for managing feature flags.", + "homepage": "https://github.com/laravel/pennant", + "keywords": [ + "feature", + "flags", + "laravel", + "pennant" + ], + "support": { + "issues": "https://github.com/laravel/pennant/issues", + "source": "https://github.com/laravel/pennant" + }, + "time": "2026-03-19T02:27:39+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.17", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.17" + }, + "time": "2026-04-20T16:07:33+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "4cdd88f761e9be9095ccbedf3e08d61ae216c643" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/4cdd88f761e9be9095ccbedf3e08d61ae216c643", + "reference": "4cdd88f761e9be9095ccbedf3e08d61ae216c643", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.32", + "lcobucci/coding-standard": "^12.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.6.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2026-04-13T21:30:16+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.6.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.6.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-10-17T11:30:53+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/event", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "062ebb450efbe9a09bc2478e89b7c933875b0935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/062ebb450efbe9a09bc2478e89b7c933875b0935", + "reference": "062ebb450efbe9a09bc2478e89b7c933875b0935", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/2.3.0" + }, + "time": "2025-03-14T19:51:10+00:00" + }, + { + "name": "league/flysystem", + "version": "3.33.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "570b8871e0ce693764434b29154c54b434905350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350", + "reference": "570b8871e0ce693764434b29154c54b434905350", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.33.0" + }, + "time": "2026-03-25T07:59:30+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/oauth2-server", + "version": "8.5.5", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/cc8778350f905667e796b3c2364a9d3bd7a73518", + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.3", + "ext-openssl": "*", + "lcobucci/clock": "^2.2 || ^3.0", + "lcobucci/jwt": "^4.3 || ^5.0", + "league/event": "^2.2", + "league/uri": "^6.7 || ^7.0", + "php": "^8.0", + "psr/http-message": "^1.0.1 || ^2.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^3.0.0", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^9.6.6", + "roave/security-advisories": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.5" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2024-12-20T23:06:10+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "livewire/livewire", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "7d0bfa46269b1ec186b8cdd38baffee5cc647d10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/7d0bfa46269b1ec186b8cdd38baffee5cc647d10", + "reference": "7d0bfa46269b1ec186b8cdd38baffee5cc647d10", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v4.2.4" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-04-02T20:48:35+00:00" + }, + { + "name": "lthn/php", + "version": "dev-dev", + "dist": { + "type": "path", + "url": "../php", + "reference": "41a86e0953194db6c731f4018f14e29a8bd86865" + }, + "require": { + "laravel/framework": "^11.0|^12.0|^13.0", + "laravel/pennant": "^1.0", + "livewire/livewire": "^3.0|^4.0", + "php": "^8.2" + }, + "replace": { + "core/php": "self.version" + }, + "require-dev": { + "fakerphp/faker": "^1.23", + "infection/infection": "^0.32.3", + "larastan/larastan": "^3.9", + "laravel/pint": "^1.18", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpunit/phpunit": "^11.5", + "psalm/plugin-laravel": "^3.0", + "rector/rector": "^2.3", + "roave/security-advisories": "dev-latest", + "spatie/laravel-activitylog": "^4.8", + "vimeo/psalm": "^6.14" + }, + "suggest": { + "spatie/laravel-activitylog": "Required for activity logging features (^4.0)" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Core\\LifecycleEventProvider", + "Core\\Lang\\LangServiceProvider", + "Core\\Bouncer\\Gate\\Boot" + ] + } + }, + "autoload": { + "psr-4": { + "Core\\": "src/Core/", + "Core\\Website\\": "src/Website/", + "Core\\Mod\\": "src/Mod/", + "Core\\Plug\\": "src/Plug/" + }, + "files": [ + "src/Core/Media/Thumbnail/helpers.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Core\\Tests\\": "tests/", + "Core\\TestCore\\": "tests/Fixtures/Core/TestCore/", + "App\\Custom\\": "tests/Fixtures/Custom/", + "Mod\\": "tests/Fixtures/Mod/", + "Plug\\": "tests/Fixtures/Plug/", + "Website\\": "tests/Fixtures/Website/" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit" + ], + "pint": [ + "vendor/bin/pint" + ] + }, + "license": [ + "EUPL-1.2" + ], + "authors": [ + { + "name": "Host UK", + "email": "support@host.uk.com" + } + ], + "description": "Modular monolith framework for Laravel - event-driven architecture with lazy module loading", + "keywords": [ + "events", + "framework", + "laravel", + "modular", + "modules", + "monolith" + ], + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "lthn/php-tenant", + "version": "dev-dev", + "dist": { + "type": "path", + "url": "../php-tenant", + "reference": "5e021de7f579095786058b479f341e5c7f408c61" + }, + "require": { + "lthn/php": "*", + "php": "^8.2" + }, + "replace": { + "core/php-tenant": "self.version" + }, + "require-dev": { + "laravel/pint": "^1.18", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest": "^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Core\\Tenant\\Boot" + ] + } + }, + "autoload": { + "psr-4": { + "Core\\Tenant\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "Core\\Tenant\\Tests\\": "Tests/", + "Tests\\": "tests/" + } + }, + "scripts": { + "lint": [ + "pint" + ], + "test": [ + "pest" + ] + }, + "license": [ + "EUPL-1.2" + ], + "description": "Multi-tenancy and workspaces for Laravel", + "keywords": [ + "multi-tenant", + "teams", + "workspaces" + ], + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-04-07T09:57:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-21T12:49:54+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b55a638b189a6faa875e0ccdb00908fb87af95b3", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed", + "reference": "8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6", + "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "017e76ad089bac281553389269e259e155935e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/017e76ad089bac281553389269e259e155935e1a", + "reference": "017e76ad089bac281553389269e259e155935e1a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-31T20:57:01+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T14:11:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T18:47:49+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:10:57+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/76f1a57719a4a04c0ea18678a6c9305b5dcb9da8", + "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", + "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/docs/development.md b/docs/development.md index 27eeb51..ee006a8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -9,7 +9,7 @@ description: How to build, test, and contribute to the go-api REST framework -- This guide covers everything needed to build, test, extend, and contribute to go-api. -**Module path:** `forge.lthn.ai/core/go-api` +**Module path:** `dappco.re/go/api` **Licence:** EUPL-1.2 **Language:** Go 1.26 @@ -41,9 +41,10 @@ go version ### Minimal dependencies -go-api has no sibling `forge.lthn.ai/core/*` dependencies at the library level (the `cmd/api/` -subcommands import `core/cli`, but the main package compiles independently). There are no -`replace` directives. Cloning go-api alone is sufficient to build and test the library. +go-api depends on the provider modules that the gateway wires (`process`, `scm`, `miner`, +`proxy`, and `ws`) plus the core helper modules it uses directly. The `cmd/api/` CLI lives in +its own nested module and imports `dappco.re/go/cli`. There are no `replace` directives in the +root module. If working within the Go workspace at `~/Code/go.work`, the workspace `use` directive handles local module resolution automatically. @@ -275,7 +276,7 @@ package mypackage import ( "net/http" - api "forge.lthn.ai/core/go-api" + api "dappco.re/go/api" "github.com/gin-gonic/gin" ) diff --git a/docs/history.md b/docs/history.md index f2a6f81..0ee0d5a 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,7 +2,7 @@ # go-api — Project History and Known Limitations -Module: `forge.lthn.ai/core/go-api` +Module: `dappco.re/go/api` --- @@ -12,8 +12,8 @@ Module: `forge.lthn.ai/core/go-api` was to give every Go package in the stack a consistent way to expose REST endpoints without each package taking its own opinion on routing, middleware, response formatting, or OpenAPI generation. It was scaffolded independently from the start — it was never extracted from a monolith — and has -no `forge.lthn.ai/core/*` dependencies. This keeps it at the bottom of the import graph: every -other package can import go-api, but go-api imports nothing from the ecosystem. +no legacy forge-path dependencies. It now acts as the gateway module: go-api imports and +wires provider packages, while providers keep their dependency direction independent of go-api. --- @@ -26,7 +26,7 @@ Commits `889391a` through `22f8a69` The initial phase established the foundational abstractions that all subsequent work builds on. **Scaffold** (`889391a`): -Module path `forge.lthn.ai/core/go-api` created. `go.mod` initialised with Gin as the only +Module path `dappco.re/go/api` created. `go.mod` initialised with Gin as the only direct dependency. **Response envelope** (`7835837`): diff --git a/docs/index.md b/docs/index.md index 15b11b3..ff2bca6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,15 +7,16 @@ description: Gin-based REST framework with OpenAPI generation, middleware compos # go-api -**Module path:** `forge.lthn.ai/core/go-api` +**Module path:** `dappco.re/go/api` **Language:** Go 1.26 **Licence:** EUPL-1.2 -go-api is a REST framework built on top of [Gin](https://github.com/gin-gonic/gin). It provides -an `Engine` that subsystems plug into via the `RouteGroup` interface. Each ecosystem package -(go-ai, go-ml, go-rag, and others) registers its own route group, and go-api handles the HTTP -plumbing: middleware composition, response envelopes, WebSocket and SSE integration, GraphQL -hosting, Authentik identity, OpenAPI 3.1 specification generation, and client SDK codegen. +go-api is a REST framework and provider gateway built on top of +[Gin](https://github.com/gin-gonic/gin). It provides an `Engine` that subsystems plug into via +the `RouteGroup` interface, and the gateway binary wires provider packages into HTTP endpoints. +go-api handles the HTTP plumbing: middleware composition, response envelopes, WebSocket and SSE +integration, GraphQL hosting, Authentik identity, OpenAPI 3.1 specification generation, and +client SDK codegen. go-api is a library. It has no `main` package and produces no binary on its own. Callers construct an `Engine`, register route groups, and call `Serve()`. @@ -32,7 +33,7 @@ import ( "os/signal" "syscall" - api "forge.lthn.ai/core/go-api" + api "dappco.re/go/api" ) func main() { @@ -147,13 +148,12 @@ engine.Register(&Routes{service: svc}) | `go.opentelemetry.io/contrib/.../otelgin` | OpenTelemetry Gin instrumentation | | `golang.org/x/text` | BCP 47 language tag matching | | `gopkg.in/yaml.v3` | YAML export of OpenAPI specs | -| `dappco.re/go/core/cli` | CLI command registration (for `cmd/api/` subcommands) | +| `dappco.re/go/cli` | CLI command registration for the nested `cmd/api` module | ### Ecosystem position -go-api sits at the base of the Lethean HTTP stack. It has no imports from other Lethean -ecosystem modules (beyond `core/cli` for the CLI subcommands). Other packages import go-api -to expose their functionality as REST endpoints: +go-api is the Lethean HTTP gateway. The API module imports and wires provider modules into the +gateway binary; provider modules implement their own route groups without importing go-api. ``` Application main / Core CLI @@ -162,8 +162,8 @@ Application main / Core CLI go-api Engine <-- this module | | | | | +-- OpenAPI spec --> SDKGenerator --> openapi-generator-cli - | +-- ToolBridge --> go-ai / go-ml / go-rag route groups - +-- RouteGroups ----------> any package implementing RouteGroup + | +-- ToolBridge --> tool-backed route groups + +-- Provider route groups --> process / scm / miner / proxy / ws ``` --- diff --git a/external/go b/external/go new file mode 160000 index 0000000..d661b70 --- /dev/null +++ b/external/go @@ -0,0 +1 @@ +Subproject commit d661b703e16183b3cbab101de189f688888a1174 diff --git a/external/go-inference b/external/go-inference new file mode 160000 index 0000000..3950d8d --- /dev/null +++ b/external/go-inference @@ -0,0 +1 @@ +Subproject commit 3950d8df0617a749dee794d1e8995da0d6507837 diff --git a/external/go-io b/external/go-io new file mode 160000 index 0000000..789653d --- /dev/null +++ b/external/go-io @@ -0,0 +1 @@ +Subproject commit 789653dfc376383a3873993cdb875c8c717e4b05 diff --git a/external/go-log b/external/go-log new file mode 160000 index 0000000..df05298 --- /dev/null +++ b/external/go-log @@ -0,0 +1 @@ +Subproject commit df0529839b2ab786a6a3da374fa664867d5f9f09 diff --git a/external/go-process b/external/go-process new file mode 160000 index 0000000..53a1f80 --- /dev/null +++ b/external/go-process @@ -0,0 +1 @@ +Subproject commit 53a1f80739682063ab2544f4f907bc6fbaa4c2d7 diff --git a/external/go-proxy b/external/go-proxy new file mode 160000 index 0000000..a35a8ed --- /dev/null +++ b/external/go-proxy @@ -0,0 +1 @@ +Subproject commit a35a8ed3be11e5f3615c020a9e091235a90cfeaa diff --git a/external/go-scm b/external/go-scm new file mode 160000 index 0000000..24d36e9 --- /dev/null +++ b/external/go-scm @@ -0,0 +1 @@ +Subproject commit 24d36e937e5604be856704275c3d1cd7fa8f08a0 diff --git a/external/go-ws b/external/go-ws new file mode 160000 index 0000000..c83f7a1 --- /dev/null +++ b/external/go-ws @@ -0,0 +1 @@ +Subproject commit c83f7a1d91c314543ac0d61d14a13b24877b8cd7 diff --git a/go.work b/go.work new file mode 100644 index 0000000..db0df35 --- /dev/null +++ b/go.work @@ -0,0 +1,21 @@ +go 1.26.2 + +// Workspace mode for development: pulls fresh code from external/ submodules. +// +// Devs: git clone --recursive → go.work picks up local sources, latest dev. +// CI: GOWORK=off → go.mod tags drive resolution, reproducible. +// +// Submodule pins live in .gitmodules + the recorded SHA per submodule entry. +// Bump a single dep: git submodule update --remote external/ + +use ( + ./go + ./external/go + ./external/go-process + ./external/go-scm + ./external/go-proxy + ./external/go-ws + ./external/go-inference + ./external/go-io + ./external/go-log +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..9f86cc5 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,9 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go/AGENTS.md b/go/AGENTS.md new file mode 120000 index 0000000..be77ac8 --- /dev/null +++ b/go/AGENTS.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/go/CLAUDE.md b/go/CLAUDE.md new file mode 120000 index 0000000..949a29f --- /dev/null +++ b/go/CLAUDE.md @@ -0,0 +1 @@ +../CLAUDE.md \ No newline at end of file diff --git a/go/README.md b/go/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/go/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/api.go b/go/api.go similarity index 98% rename from api.go rename to go/api.go index 625b2fc..853dd16 100644 --- a/api.go +++ b/go/api.go @@ -12,8 +12,8 @@ import ( "slices" "time" + core "dappco.re/go" apistream "dappco.re/go/api/pkg/stream" - core "dappco.re/go/core" "github.com/gin-contrib/expvar" "github.com/gin-contrib/pprof" @@ -94,7 +94,10 @@ type Engine struct { // if err != nil { // panic(err) // } -func New(opts ...Option) (*Engine, error) { +func New(opts ...Option) ( + *Engine, + error, +) { e := &Engine{ addr: defaultAddr, } @@ -237,7 +240,9 @@ func (e *Engine) Handler() http.Handler { // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // _ = engine.Serve(ctx) -func (e *Engine) Serve(ctx context.Context) error { +func (e *Engine) Serve(ctx context.Context) ( + _ error, +) { srv := &http.Server{ Addr: e.addr, Handler: e.build(), diff --git a/api_describable_test.go b/go/api_describable_test.go similarity index 98% rename from api_describable_test.go rename to go/api_describable_test.go index b97ed92..cc5f9e2 100644 --- a/api_describable_test.go +++ b/go/api_describable_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "testing" @@ -80,7 +79,7 @@ func buildDescribableOperation(t *testing.T, group api.RouteGroup, path, method } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } diff --git a/go/api_example_test.go b/go/api_example_test.go new file mode 100644 index 0000000..ff68dff --- /dev/null +++ b/go/api_example_test.go @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestApi_New_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = New() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_New_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = New() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_New_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = New() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Addr_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Addr() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Addr_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Addr() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Addr_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Addr() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Groups_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Groups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Groups_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Groups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Groups_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Groups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_GroupsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_GroupsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_GroupsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Register_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Register_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Register_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_RegisterStreamGroup_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.RegisterStreamGroup(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_RegisterStreamGroup_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.RegisterStreamGroup(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_RegisterStreamGroup_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + subject.RegisterStreamGroup(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Channels_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Channels_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Channels_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_ChannelsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ChannelsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_ChannelsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ChannelsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_ChannelsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ChannelsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Handler_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Handler_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Handler_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestApi_Engine_Serve_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Serve(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestApi_Engine_Serve_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Serve(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestApi_Engine_Serve_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.Serve(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNew_api() { + func() { + defer func() { _ = recover() }() + _, _ = New() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Addr_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.Addr() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Groups_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.Groups() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_GroupsIter_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.GroupsIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Register_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + subject.Register(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_RegisterStreamGroup_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + subject.RegisterStreamGroup(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Channels_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.Channels() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_ChannelsIter_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.ChannelsIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Handler_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.Handler() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_Serve_api() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.Serve(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/api_renderable_test.go b/go/api_renderable_test.go similarity index 98% rename from api_renderable_test.go rename to go/api_renderable_test.go index 8f43998..322f82b 100644 --- a/api_renderable_test.go +++ b/go/api_renderable_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "testing" @@ -48,7 +47,7 @@ func buildRenderableOperation(t *testing.T, group api.RouteGroup, path, method s } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } diff --git a/api_test.go b/go/api_test.go similarity index 97% rename from api_test.go rename to go/api_test.go index cc33b9a..cea821b 100644 --- a/api_test.go +++ b/go/api_test.go @@ -4,7 +4,6 @@ package api_test import ( "context" - "encoding/json" "net" "net/http" "net/http/httptest" @@ -133,7 +132,7 @@ func TestHandler_Good_HealthEndpoint(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -159,7 +158,7 @@ func TestHandler_Good_RegisteredRoutes(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data != "echo" { @@ -196,7 +195,7 @@ func TestHandler_Bad_PanicReturnsEnvelope(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { diff --git a/authentik.go b/go/authentik.go similarity index 98% rename from authentik.go rename to go/authentik.go index bc82c3e..27f230b 100644 --- a/authentik.go +++ b/go/authentik.go @@ -7,7 +7,7 @@ import ( "net/http" // Note: AX-6 - structural HTTP status boundary for Gin auth responses; no core primitive. "slices" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" @@ -106,7 +106,10 @@ var oidcProviders = make(map[string]*oidc.Provider) // getOIDCProvider returns a cached OIDC provider for the given issuer, // performing discovery on first access. -func getOIDCProvider(ctx context.Context, issuer string) (*oidc.Provider, error) { +func getOIDCProvider(ctx context.Context, issuer string) ( + *oidc.Provider, + error, +) { oidcProviderMu.Lock() defer oidcProviderMu.Unlock() @@ -125,7 +128,10 @@ func getOIDCProvider(ctx context.Context, issuer string) (*oidc.Provider, error) // validateJWT verifies a raw JWT against the configured OIDC issuer and // extracts user claims on success. -func validateJWT(ctx context.Context, cfg AuthentikConfig, rawToken string) (*AuthentikUser, error) { +func validateJWT(ctx context.Context, cfg AuthentikConfig, rawToken string) ( + *AuthentikUser, + error, +) { provider, err := getOIDCProvider(ctx, cfg.Issuer) if err != nil { return nil, err diff --git a/go/authentik_example_test.go b/go/authentik_example_test.go new file mode 100644 index 0000000..f36d393 --- /dev/null +++ b/go/authentik_example_test.go @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestAuthentik_Engine_AuthentikConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.AuthentikConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestAuthentik_Engine_AuthentikConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.AuthentikConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestAuthentik_Engine_AuthentikConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.AuthentikConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestAuthentik_AuthentikUser_HasGroup_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *AuthentikUser + _ = subject.HasGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestAuthentik_AuthentikUser_HasGroup_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *AuthentikUser + _ = subject.HasGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestAuthentik_AuthentikUser_HasGroup_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *AuthentikUser + _ = subject.HasGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestAuthentik_GetUser_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetUser(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestAuthentik_GetUser_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetUser(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestAuthentik_GetUser_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetUser(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestAuthentik_RequireAuth_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireAuth() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestAuthentik_RequireAuth_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireAuth() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestAuthentik_RequireAuth_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireAuth() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestAuthentik_RequireGroup_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestAuthentik_RequireGroup_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestAuthentik_RequireGroup_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RequireGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_AuthentikConfig_authentik() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.AuthentikConfig() + }() + coretest.Println("done") + // Output: done +} + +func ExampleAuthentikUser_HasGroup_authentik() { + func() { + defer func() { _ = recover() }() + var subject *AuthentikUser + _ = subject.HasGroup("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleGetUser_authentik() { + func() { + defer func() { _ = recover() }() + _ = GetUser(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRequireAuth_authentik() { + func() { + defer func() { _ = recover() }() + _ = RequireAuth() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRequireGroup_authentik() { + func() { + defer func() { _ = recover() }() + _ = RequireGroup("") + }() + coretest.Println("done") + // Output: done +} diff --git a/authentik_integration_test.go b/go/authentik_integration_test.go similarity index 89% rename from authentik_integration_test.go rename to go/authentik_integration_test.go index 89fed49..6b2c3e7 100644 --- a/authentik_integration_test.go +++ b/go/authentik_integration_test.go @@ -3,14 +3,11 @@ package api_test import ( - "encoding/json" - "fmt" + core "dappco.re/go" "io" "net/http" "net/http/httptest" "net/url" - "os" - "strings" "testing" api "dappco.re/go/api" @@ -43,7 +40,7 @@ func getClientCredentialsToken(t *testing.T, issuer, clientID, clientSecret stri t.Helper() // Discover token endpoint. - disc := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" + disc := core.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" resp, err := http.Get(disc) if err != nil { t.Fatalf("OIDC discovery failed: %v", err) @@ -53,7 +50,7 @@ func getClientCredentialsToken(t *testing.T, issuer, clientID, clientSecret stri var config struct { TokenEndpoint string `json:"token_endpoint"` } - if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { + if err := coreJSONDecode(resp.Body, &config); err != nil { t.Fatalf("decode discovery: %v", err) } @@ -76,7 +73,7 @@ func getClientCredentialsToken(t *testing.T, issuer, clientID, clientSecret stri Error string `json:"error"` ErrorDesc string `json:"error_description"` } - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + if err := coreJSONDecode(resp.Body, &tokenResp); err != nil { t.Fatalf("decode token response: %v", err) } if tokenResp.Error != "" { @@ -88,13 +85,13 @@ func getClientCredentialsToken(t *testing.T, issuer, clientID, clientSecret stri func TestAuthentikIntegration(t *testing.T) { // Skip unless explicitly enabled — requires live Authentik at auth.lthn.io. - if os.Getenv("AUTHENTIK_INTEGRATION") != "1" { + if core.Getenv("AUTHENTIK_INTEGRATION") != "1" { t.Skip("set AUTHENTIK_INTEGRATION=1 to run live Authentik tests") } issuer := envOr("AUTHENTIK_ISSUER", "https://auth.lthn.io/application/o/core-api/") clientID := envOr("AUTHENTIK_CLIENT_ID", "core-api") - clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET") + clientSecret := core.Getenv("AUTHENTIK_CLIENT_SECRET") if clientSecret == "" { t.Fatal("AUTHENTIK_CLIENT_SECRET is required") } @@ -160,13 +157,13 @@ func TestAuthentikIntegration(t *testing.T) { var envelope struct { Data api.AuthentikUser `json:"data"` } - if err := json.Unmarshal([]byte(body), &envelope); err != nil { + if err := coreJSONUnmarshal([]byte(body), &envelope); err != nil { t.Fatalf("parse whoami: %v", err) } if envelope.Data.UID == "" { t.Error("expected non-empty UID") } - if !strings.Contains(envelope.Data.Username, "client_credentials") { + if !core.Contains(envelope.Data.Username, "client_credentials") { t.Logf("username: %s (service account)", envelope.Data.Username) } }) @@ -200,7 +197,7 @@ func TestAuthentikIntegration(t *testing.T) { var envelope struct { Data api.AuthentikUser `json:"data"` } - if err := json.Unmarshal([]byte(body), &envelope); err != nil { + if err := coreJSONUnmarshal([]byte(body), &envelope); err != nil { t.Fatalf("parse: %v", err) } if envelope.Data.Username != "akadmin" { @@ -274,7 +271,7 @@ func assertStatus(t *testing.T, resp *http.Response, want int) { } func envOr(key, fallback string) string { - if v := os.Getenv(key); v != "" { + if v := core.Getenv(key); v != "" { return v } return fallback @@ -282,12 +279,12 @@ func envOr(key, fallback string) string { // TestOIDCDiscovery validates that the OIDC discovery endpoint is reachable. func TestOIDCDiscovery(t *testing.T) { - if os.Getenv("AUTHENTIK_INTEGRATION") != "1" { + if core.Getenv("AUTHENTIK_INTEGRATION") != "1" { t.Skip("set AUTHENTIK_INTEGRATION=1 to run live Authentik tests") } issuer := envOr("AUTHENTIK_ISSUER", "https://auth.lthn.io/application/o/core-api/") - disc := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" + disc := core.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" resp, err := http.Get(disc) if err != nil { @@ -300,7 +297,7 @@ func TestOIDCDiscovery(t *testing.T) { } var config map[string]any - if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { + if err := coreJSONDecode(resp.Body, &config); err != nil { t.Fatalf("decode: %v", err) } @@ -331,7 +328,7 @@ func TestOIDCDiscovery(t *testing.T) { t.Error("client_credentials grant not supported") } - fmt.Printf(" OIDC discovery OK — issuer: %s\n", config["issuer"]) - fmt.Printf(" Token endpoint: %s\n", config["token_endpoint"]) - fmt.Printf(" JWKS URI: %s\n", config["jwks_uri"]) + core.Print(nil, " OIDC discovery OK — issuer: %s", config["issuer"]) + core.Print(nil, " Token endpoint: %s", config["token_endpoint"]) + core.Print(nil, " JWKS URI: %s", config["jwks_uri"]) } diff --git a/authentik_test.go b/go/authentik_test.go similarity index 99% rename from authentik_test.go rename to go/authentik_test.go index c5469be..b8f5795 100644 --- a/authentik_test.go +++ b/go/authentik_test.go @@ -3,9 +3,9 @@ package api_test import ( + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -443,7 +443,7 @@ func TestRequireAuth_Bad_NoUser(t *testing.T) { t.Fatalf("expected 401, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() - if !strings.Contains(body, `"unauthorised"`) { + if !core.Contains(body, `"unauthorised"`) { t.Fatalf("expected error code 'unauthorised' in body, got %s", body) } } @@ -502,7 +502,7 @@ func TestRequireGroup_Bad_WrongGroup(t *testing.T) { t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() - if !strings.Contains(body, `"forbidden"`) { + if !core.Contains(body, `"forbidden"`) { t.Fatalf("expected error code 'forbidden' in body, got %s", body) } } diff --git a/authz_test.go b/go/authz_test.go similarity index 100% rename from authz_test.go rename to go/authz_test.go diff --git a/bridge.go b/go/bridge.go similarity index 97% rename from bridge.go rename to go/bridge.go index 8bf495a..071ca7a 100644 --- a/bridge.go +++ b/go/bridge.go @@ -14,7 +14,7 @@ import ( "regexp" "slices" // Note: AX-6 - deterministic snapshot cloning needs slices.Clone; no core primitive. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -407,7 +407,9 @@ func newToolInputValidator(schema map[string]any) *toolInputValidator { return &toolInputValidator{schema: schema} } -func (v *toolInputValidator) Validate(body []byte) error { +func (v *toolInputValidator) Validate(body []byte) ( + _ error, +) { if core.Trim(string(body)) == "" { return core.E("ToolBridge.Validate", "request body is required", nil) } @@ -420,7 +422,9 @@ func (v *toolInputValidator) Validate(body []byte) error { return validateSchemaNode(payload, v.schema, "") } -func (v *toolInputValidator) ValidateResponse(body []byte) error { +func (v *toolInputValidator) ValidateResponse(body []byte) ( + _ error, +) { if len(v.schema) == 0 { return nil } @@ -460,7 +464,9 @@ func (v *toolInputValidator) ValidateResponse(body []byte) error { return validateSchemaNode(payload, v.schema, "") } -func validateSchemaNode(value any, schema map[string]any, path string) error { +func validateSchemaNode(value any, schema map[string]any, path string) ( + _ error, +) { if len(schema) == 0 { return nil } @@ -575,7 +581,9 @@ func validateSchemaNode(value any, schema map[string]any, path string) error { return nil } -func validateSchemaCombinators(value any, schema map[string]any, path string) error { +func validateSchemaCombinators(value any, schema map[string]any, path string) ( + _ error, +) { if subschemas := schemaObjects(schema["allOf"]); len(subschemas) > 0 { for _, subschema := range subschemas { if err := validateSchemaNode(value, subschema, path); err != nil { @@ -618,7 +626,9 @@ anyOfMatched: return nil } -func validateStringConstraints(value string, schema map[string]any, path string) error { +func validateStringConstraints(value string, schema map[string]any, path string) ( + _ error, +) { length := core.RuneCount(value) if minLength, ok := schemaInt(schema["minLength"]); ok && length < minLength { return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be at least %d characters long", displayPath(path), minLength), nil) @@ -638,7 +648,9 @@ func validateStringConstraints(value string, schema map[string]any, path string) return nil } -func validateNumericConstraints(value any, schema map[string]any, path string) error { +func validateNumericConstraints(value any, schema map[string]any, path string) ( + _ error, +) { if minimum, ok := schemaFloat(schema["minimum"]); ok && numericLessThan(value, minimum) { return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be greater than or equal to %v", displayPath(path), minimum), nil) } @@ -648,7 +660,9 @@ func validateNumericConstraints(value any, schema map[string]any, path string) e return nil } -func validateArrayConstraints(value []any, schema map[string]any, path string) error { +func validateArrayConstraints(value []any, schema map[string]any, path string) ( + _ error, +) { if minItems, ok := schemaInt(schema["minItems"]); ok && len(value) < minItems { return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at least %d items", displayPath(path), minItems), nil) } @@ -658,7 +672,9 @@ func validateArrayConstraints(value []any, schema map[string]any, path string) e return nil } -func validateObjectConstraints(value map[string]any, schema map[string]any, path string) error { +func validateObjectConstraints(value map[string]any, schema map[string]any, path string) ( + _ error, +) { if minProps, ok := schemaInt(schema["minProperties"]); ok && len(value) < minProps { return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must contain at least %d properties", displayPath(path), minProps), nil) } @@ -799,7 +815,10 @@ func (w *toolResponseRecorder) WriteHeaderNow() { w.wroteHeader = true } -func (w *toolResponseRecorder) Write(data []byte) (int, error) { +func (w *toolResponseRecorder) Write(data []byte) ( + int, + error, +) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } @@ -807,7 +826,10 @@ func (w *toolResponseRecorder) Write(data []byte) (int, error) { return len(data), nil } -func (w *toolResponseRecorder) WriteString(s string) (int, error) { +func (w *toolResponseRecorder) WriteString(s string) ( + int, + error, +) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } @@ -833,7 +855,11 @@ func (w *toolResponseRecorder) Written() bool { return w.wroteHeader } -func (w *toolResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (w *toolResponseRecorder) Hijack() ( + net.Conn, + *bufio.ReadWriter, + error, +) { return nil, nil, core.E("ToolBridge.ResponseRecorder", "response hijacking is not supported by ToolBridge output validation", nil) } @@ -880,7 +906,9 @@ func (w *toolResponseRecorder) writeErrorResponse(status int, resp Response[any] w.commit() } -func typeError(path, want string, value any) error { +func typeError(path, want string, value any) ( + _ error, +) { return core.E("ToolBridge.ValidateSchema", core.Sprintf("%s must be %s, got %s", displayPath(path), want, describeJSONValue(value)), nil) } @@ -977,10 +1005,7 @@ func enumValues(rawEnum any) []any { switch values := rawEnum.(type) { case []any: out := make([]any, 0, len(values)) - for _, value := range values { - out = append(out, value) - } - return out + return append(out, values...) case []string: out := make([]any, 0, len(values)) for _, value := range values { @@ -1050,7 +1075,10 @@ func numericValue(value any) (float64, bool) { } } -func compiledPattern(pattern string) (*regexp.Regexp, error) { +func compiledPattern(pattern string) ( + *regexp.Regexp, + error, +) { if cached, ok := regexPatternCache.Load(pattern); ok { return cached.(*regexp.Regexp), nil } diff --git a/go/bridge_example_test.go b/go/bridge_example_test.go new file mode 100644 index 0000000..1ed645a --- /dev/null +++ b/go/bridge_example_test.go @@ -0,0 +1,1075 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestBridge_NewToolBridge_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewToolBridge("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_NewToolBridge_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewToolBridge("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_NewToolBridge_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewToolBridge("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_Add_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.Add(ToolDescriptor{}, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_Add_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.Add(ToolDescriptor{}, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_Add_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.Add(ToolDescriptor{}, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_Name_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_Name_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_Name_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_BasePath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_BasePath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_BasePath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_RegisterRoutes_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_RegisterRoutes_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_RegisterRoutes_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_Describe_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_Describe_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_Describe_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_DescribeIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.DescribeIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_DescribeIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.DescribeIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_DescribeIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.DescribeIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_Tools_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Tools() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_Tools_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Tools() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_Tools_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.Tools() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ToolBridge_ToolsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.ToolsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ToolBridge_ToolsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.ToolsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ToolBridge_ToolsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ToolBridge + _ = subject.ToolsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_IsValidMCPServerID_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsValidMCPServerID("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_IsValidMCPServerID_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsValidMCPServerID("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_IsValidMCPServerID_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsValidMCPServerID("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_InputValidator_Validate_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.Validate(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_InputValidator_Validate_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.Validate(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_InputValidator_Validate_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.Validate(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_InputValidator_ValidateResponse_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.ValidateResponse(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_InputValidator_ValidateResponse_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.ValidateResponse(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_InputValidator_ValidateResponse_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolInputValidator + _ = subject.ValidateResponse(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Header_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Header_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Header_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_WriteHeader_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_WriteHeader_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_WriteHeader_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_WriteHeaderNow_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_WriteHeaderNow_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_WriteHeaderNow_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Write_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Write_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Write_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_WriteString_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_WriteString_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_WriteString_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Flush_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Flush_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Flush_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Status_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Status_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Status_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Size_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Size_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Size_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Written_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Written_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Written_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBridge_ResponseRecorder_Hijack_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBridge_ResponseRecorder_Hijack_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBridge_ResponseRecorder_Hijack_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *toolResponseRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNewToolBridge_bridge() { + func() { + defer func() { _ = recover() }() + _ = NewToolBridge("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_Add_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + subject.Add(ToolDescriptor{}, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_Name_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.Name() + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_BasePath_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.BasePath() + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_RegisterRoutes_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + subject.RegisterRoutes(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_Describe_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.Describe() + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_DescribeIter_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.DescribeIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_Tools_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.Tools() + }() + coretest.Println("done") + // Output: done +} + +func ExampleToolBridge_ToolsIter_bridge() { + func() { + defer func() { _ = recover() }() + var subject *ToolBridge + _ = subject.ToolsIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleIsValidMCPServerID_bridge() { + func() { + defer func() { _ = recover() }() + _ = IsValidMCPServerID("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleInputValidator_Validate_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolInputValidator + _ = subject.Validate(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleInputValidator_ValidateResponse_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolInputValidator + _ = subject.ValidateResponse(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Header_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _ = subject.Header() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_WriteHeader_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + subject.WriteHeader(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_WriteHeaderNow_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + subject.WriteHeaderNow() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Write_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _, _ = subject.Write(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_WriteString_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _, _ = subject.WriteString("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Flush_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + subject.Flush() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Status_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _ = subject.Status() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Size_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _ = subject.Size() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Written_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _ = subject.Written() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResponseRecorder_Hijack_bridge() { + func() { + defer func() { _ = recover() }() + var subject *toolResponseRecorder + _, _, _ = subject.Hijack() + }() + coretest.Println("done") + // Output: done +} diff --git a/bridge_internal_test.go b/go/bridge_internal_test.go similarity index 100% rename from bridge_internal_test.go rename to go/bridge_internal_test.go diff --git a/bridge_test.go b/go/bridge_test.go similarity index 89% rename from bridge_test.go rename to go/bridge_test.go index 296a489..5229227 100644 --- a/bridge_test.go +++ b/go/bridge_test.go @@ -3,11 +3,9 @@ package api_test import ( - "bytes" - "encoding/json" + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -49,7 +47,7 @@ func TestBridge_Good_RegisterAndServe(t *testing.T) { t.Fatalf("expected 200 for file_read, got %d", w1.Code) } var resp1 api.Response[string] - if err := json.Unmarshal(w1.Body.Bytes(), &resp1); err != nil { + if err := coreJSONUnmarshal(w1.Body.Bytes(), &resp1); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp1.Data != "result1" { @@ -65,7 +63,7 @@ func TestBridge_Good_RegisterAndServe(t *testing.T) { t.Fatalf("expected 200 for file_write, got %d", w2.Code) } var resp2 api.Response[string] - if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil { + if err := coreJSONUnmarshal(w2.Body.Bytes(), &resp2); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp2.Data != "result2" { @@ -206,7 +204,7 @@ func TestBridge_MCPServerID_Good_AcceptsSafeIDs(t *testing.T) { "core-mcp", "A1", "server-01", - "a" + strings.Repeat("b", 63), + "a" + coreStringRepeat("b", 63), } for _, id := range cases { @@ -233,7 +231,7 @@ func TestBridge_MCPServerID_Bad_RejectsMalformedIDs(t *testing.T) { "/etc/passwd", `C:\Windows`, "core\x00mcp", - "a" + strings.Repeat("b", 64), + "a" + coreStringRepeat("b", 64), } for _, id := range cases { @@ -254,7 +252,7 @@ func TestBridge_Good_Describe(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, }, OutputSchema: map[string]any{ @@ -355,23 +353,23 @@ func TestBridge_Good_ValidatesRequestBody(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { var payload map[string]any - if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + if err := coreJSONDecode(c.Request.Body, &payload); err != nil { t.Fatalf("handler could not read validated body: %v", err) } - c.JSON(http.StatusOK, api.OK(payload["path"])) + c.JSON(http.StatusOK, api.OK(payload[`path`])) }) rg := engine.Group(bridge.BasePath()) bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBufferString(`{"path":"/tmp/file.txt"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBufferString("{\""+`path`+"\":\"/tmp/file.txt\"}")) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -379,7 +377,7 @@ func TestBridge_Good_ValidatesRequestBody(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data != "/tmp/file.txt" { @@ -399,19 +397,19 @@ func TestBridge_Good_ValidatesResponseBody(t *testing.T) { OutputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { - c.JSON(http.StatusOK, api.OK(map[string]any{"path": "/tmp/file.txt"})) + c.JSON(http.StatusOK, api.OK(map[string]any{`path`: "/tmp/file.txt"})) }) rg := engine.Group(bridge.BasePath()) bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBufferString("")) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBufferString("")) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -419,14 +417,14 @@ func TestBridge_Good_ValidatesResponseBody(t *testing.T) { } var resp api.Response[map[string]any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { t.Fatal("expected Success=true") } - if resp.Data["path"] != "/tmp/file.txt" { - t.Fatalf("expected validated response data to reach client, got %v", resp.Data["path"]) + if resp.Data[`path`] != "/tmp/file.txt" { + t.Fatalf("expected validated response data to reach client, got %v", resp.Data[`path`]) } } @@ -442,12 +440,12 @@ func TestBridge_Bad_InvalidResponseBody(t *testing.T) { OutputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { - c.JSON(http.StatusOK, api.OK(map[string]any{"path": 123})) + c.JSON(http.StatusOK, api.OK(map[string]any{`path`: 123})) }) rg := engine.Group(bridge.BasePath()) @@ -462,7 +460,7 @@ func TestBridge_Bad_InvalidResponseBody(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -485,9 +483,9 @@ func TestBridge_Bad_InvalidRequestBody(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { c.JSON(http.StatusOK, api.OK("should not run")) @@ -497,7 +495,7 @@ func TestBridge_Bad_InvalidRequestBody(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBufferString(`{"path":123}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBufferString("{\""+`path`+"\":123}")) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -505,7 +503,7 @@ func TestBridge_Bad_InvalidRequestBody(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -528,9 +526,9 @@ func TestBridge_Bad_RejectsWhitespaceOnlyRequestBody(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { c.JSON(http.StatusOK, api.OK("should not run")) @@ -540,7 +538,7 @@ func TestBridge_Bad_RejectsWhitespaceOnlyRequestBody(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBufferString(" ")) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBufferString(" ")) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -548,7 +546,7 @@ func TestBridge_Bad_RejectsWhitespaceOnlyRequestBody(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Error == nil || resp.Error.Code != "invalid_request_body" { @@ -568,9 +566,9 @@ func TestBridge_Ugly_RejectsMalformedJSONRequestBody(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { c.JSON(http.StatusOK, api.OK("should not run")) @@ -580,7 +578,7 @@ func TestBridge_Ugly_RejectsMalformedJSONRequestBody(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBufferString(`{"path":`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBufferString("{\""+`path`+"\":")) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -588,7 +586,7 @@ func TestBridge_Ugly_RejectsMalformedJSONRequestBody(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Error == nil || resp.Error.Code != "invalid_request_body" { @@ -608,9 +606,9 @@ func TestBridge_Ugly_RejectsOversizedRequestBody(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, - "required": []any{"path"}, + "required": []any{`path`}, }, }, func(c *gin.Context) { c.JSON(http.StatusOK, api.OK("should not run")) @@ -620,7 +618,7 @@ func TestBridge_Ugly_RejectsOversizedRequestBody(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", bytes.NewBuffer(bytes.Repeat([]byte("a"), 10<<20+1))) + req, _ := http.NewRequest(http.MethodPost, "/tools/file_read", core.NewBuffer(coreBytesRepeat([]byte("a"), 10<<20+1))) engine.ServeHTTP(w, req) if w.Code != http.StatusRequestEntityTooLarge { @@ -628,7 +626,7 @@ func TestBridge_Ugly_RejectsOversizedRequestBody(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Error == nil || resp.Error.Code != "invalid_request_body" { @@ -663,7 +661,7 @@ func TestBridge_Good_ValidatesEnumValues(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", bytes.NewBufferString(`{"status":"published"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", core.NewBufferString(`{"status":"published"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -698,7 +696,7 @@ func TestBridge_Bad_RejectsInvalidEnumValues(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", bytes.NewBufferString(`{"status":"archived"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", core.NewBufferString(`{"status":"archived"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -706,7 +704,7 @@ func TestBridge_Bad_RejectsInvalidEnumValues(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -755,7 +753,7 @@ func TestBridge_Good_ValidatesSchemaCombinators(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/route_choice", bytes.NewBufferString(`{"choice":"BC"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/route_choice", core.NewBufferString(`{"choice":"BC"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -801,7 +799,7 @@ func TestBridge_Bad_RejectsAmbiguousOneOfMatches(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/route_choice", bytes.NewBufferString(`{"choice":"A"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/route_choice", core.NewBufferString(`{"choice":"A"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -809,7 +807,7 @@ func TestBridge_Bad_RejectsAmbiguousOneOfMatches(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -845,7 +843,7 @@ func TestBridge_Bad_RejectsAdditionalProperties(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", bytes.NewBufferString(`{"status":"published","unexpected":true}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/publish_item", core.NewBufferString(`{"status":"published","unexpected":true}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -853,7 +851,7 @@ func TestBridge_Bad_RejectsAdditionalProperties(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -893,7 +891,7 @@ func TestBridge_Good_EnforcesStringConstraints(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/publish_code", bytes.NewBufferString(`{"code":"ABC"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/publish_code", core.NewBufferString(`{"code":"ABC"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -943,7 +941,7 @@ func TestBridge_Bad_RejectsNumericAndCollectionConstraints(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/quota_check", bytes.NewBufferString(`{"count":0,"labels":["one"],"payload":{}}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/quota_check", core.NewBufferString(`{"count":0,"labels":["one"],"payload":{}}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -951,7 +949,7 @@ func TestBridge_Bad_RejectsNumericAndCollectionConstraints(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1020,7 +1018,7 @@ func TestBridge_Good_ListsRegisteredTools(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, }, }, func(c *gin.Context) {}) @@ -1043,7 +1041,7 @@ func TestBridge_Good_ListsRegisteredTools(t *testing.T) { } var resp api.Response[[]api.ToolDescriptor] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -1079,7 +1077,7 @@ func TestBridge_Bad_ListingRoutesWhenEmpty(t *testing.T) { } var resp api.Response[[]api.ToolDescriptor] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -1141,7 +1139,7 @@ func TestBridge_Good_ValidatesArrayInputSchema(t *testing.T) { }, }, func(c *gin.Context) { var payload []string - if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + if err := coreJSONDecode(c.Request.Body, &payload); err != nil { t.Fatalf("handler could not read validated array body: %v", err) } c.JSON(http.StatusOK, api.OK(payload)) @@ -1151,7 +1149,7 @@ func TestBridge_Good_ValidatesArrayInputSchema(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/tags", bytes.NewBufferString(`["alpha","beta"]`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/tags", core.NewBufferString(`["alpha","beta"]`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -1159,7 +1157,7 @@ func TestBridge_Good_ValidatesArrayInputSchema(t *testing.T) { } var resp api.Response[[]string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -1191,7 +1189,7 @@ func TestBridge_Bad_RejectsTooSmallArrayInputSchema(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/tags", bytes.NewBufferString(`["alpha"]`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/tags", core.NewBufferString(`["alpha"]`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -1199,7 +1197,7 @@ func TestBridge_Bad_RejectsTooSmallArrayInputSchema(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1230,7 +1228,7 @@ func TestBridge_Ugly_RejectsWrongArrayElementType(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/tags", bytes.NewBufferString(`["alpha",123]`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/tags", core.NewBufferString(`["alpha",123]`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -1238,7 +1236,7 @@ func TestBridge_Ugly_RejectsWrongArrayElementType(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1264,7 +1262,7 @@ func TestBridge_Good_ValidatesNumericBounds(t *testing.T) { }, }, func(c *gin.Context) { var payload float64 - if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + if err := coreJSONDecode(c.Request.Body, &payload); err != nil { t.Fatalf("handler could not read validated numeric body: %v", err) } c.JSON(http.StatusOK, api.OK(payload)) @@ -1274,7 +1272,7 @@ func TestBridge_Good_ValidatesNumericBounds(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/score", bytes.NewBufferString(`5.5`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/score", core.NewBufferString(`5.5`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -1282,7 +1280,7 @@ func TestBridge_Good_ValidatesNumericBounds(t *testing.T) { } var resp api.Response[float64] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -1313,7 +1311,7 @@ func TestBridge_Bad_RejectsLargeIntegerAboveMaximum(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/quota", bytes.NewBufferString(`9007199254740993`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/quota", core.NewBufferString(`9007199254740993`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -1321,7 +1319,7 @@ func TestBridge_Bad_RejectsLargeIntegerAboveMaximum(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1352,7 +1350,7 @@ func TestBridge_Bad_RejectsNumericInputBelowMinimum(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/score", bytes.NewBufferString(`0`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/score", core.NewBufferString(`0`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -1360,7 +1358,7 @@ func TestBridge_Bad_RejectsNumericInputBelowMinimum(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1390,7 +1388,7 @@ func TestBridge_Ugly_RejectsNonNumericInput(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/score", bytes.NewBufferString(`"oops"`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/score", core.NewBufferString(`"oops"`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -1398,7 +1396,7 @@ func TestBridge_Ugly_RejectsNonNumericInput(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -1437,7 +1435,7 @@ func TestBridge_Good_IntegrationWithEngine(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { diff --git a/brotli.go b/go/brotli.go similarity index 90% rename from brotli.go rename to go/brotli.go index 93ec9ef..a91442b 100644 --- a/brotli.go +++ b/go/brotli.go @@ -8,7 +8,7 @@ import ( "strconv" "sync" // AX-6-exception: core has no Pool wrapper; brotli writers are pooled per compression level. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/andybalholm/brotli" "github.com/gin-gonic/gin" @@ -112,7 +112,10 @@ type brotliWriter struct { status int } -func (b *brotliWriter) Write(data []byte) (int, error) { +func (b *brotliWriter) Write(data []byte) ( + int, + error, +) { b.mu.Lock() defer b.mu.Unlock() @@ -123,7 +126,7 @@ func (b *brotliWriter) Write(data []byte) (int, error) { b.Header().Del("Content-Length") if !b.statusWritten { - b.status = b.ResponseWriter.Status() + b.status = b.Status() } if b.status >= http.StatusBadRequest { @@ -135,7 +138,10 @@ func (b *brotliWriter) Write(data []byte) (int, error) { return b.writer.Write(data) } -func (b *brotliWriter) WriteString(s string) (int, error) { +func (b *brotliWriter) WriteString(s string) ( + int, + error, +) { return b.Write([]byte(s)) } @@ -166,7 +172,7 @@ func (b *brotliWriter) WriteHeaderNow() { } if !b.statusWritten { - b.status = b.ResponseWriter.Status() + b.status = b.Status() b.statusWritten = true } b.Header().Del("Content-Length") @@ -185,7 +191,9 @@ func (b *brotliWriter) Flush() { return } - _ = b.writer.Flush() + if err := b.writer.Flush(); err != nil { + return + } b.ResponseWriter.Flush() } @@ -202,13 +210,15 @@ func (b *brotliWriter) release(pool *sync.Pool) { b.Header().Del("Content-Encoding") b.Header().Del("Vary") b.writer.Reset(io.Discard) - } else if b.ResponseWriter.Size() < 0 { + } else if b.Size() < 0 { b.writer.Reset(io.Discard) } - _ = b.writer.Close() - if b.ResponseWriter.Size() > -1 { - b.Header().Set("Content-Length", core.Sprintf("%d", b.ResponseWriter.Size())) + if err := b.writer.Close(); err != nil { + b.Header().Del("Content-Length") } + if b.Size() > -1 { + b.Header().Set("Content-Length", core.Sprintf("%d", b.Size())) + } b.writer.Reset(io.Discard) pool.Put(b.writer) b.writer = nil diff --git a/go/brotli_example_test.go b/go/brotli_example_test.go new file mode 100644 index 0000000..614eb38 --- /dev/null +++ b/go/brotli_example_test.go @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestBrotli_Handler_Handle_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliHandler + subject.Handle(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Handler_Handle_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliHandler + subject.Handle(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Handler_Handle_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliHandler + subject.Handle(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBrotli_Writer_Write_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Writer_Write_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Writer_Write_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBrotli_Writer_WriteString_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Writer_WriteString_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Writer_WriteString_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBrotli_Writer_WriteHeader_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Writer_WriteHeader_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Writer_WriteHeader_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBrotli_Writer_WriteHeaderNow_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Writer_WriteHeaderNow_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Writer_WriteHeaderNow_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestBrotli_Writer_Flush_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestBrotli_Writer_Flush_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestBrotli_Writer_Flush_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *brotliWriter + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleHandler_Handle_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliHandler + subject.Handle(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_Write_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliWriter + _, _ = subject.Write(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_WriteString_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliWriter + _, _ = subject.WriteString("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_WriteHeader_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliWriter + subject.WriteHeader(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_WriteHeaderNow_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliWriter + subject.WriteHeaderNow() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_Flush_brotli() { + func() { + defer func() { _ = recover() }() + var subject *brotliWriter + subject.Flush() + }() + coretest.Println("done") + // Output: done +} diff --git a/brotli_test.go b/go/brotli_test.go similarity index 97% rename from brotli_test.go rename to go/brotli_test.go index 647302e..9c6959b 100644 --- a/brotli_test.go +++ b/go/brotli_test.go @@ -3,7 +3,7 @@ package api_test import ( - "bytes" + core "dappco.re/go" "io" "net/http" "net/http/httptest" @@ -271,7 +271,7 @@ func (g *brotliLateWriteGroup) RegisterRoutes(rg *gin.RouterGroup) { close(g.ready) <-g.start - payload := bytes.Repeat([]byte("late write from first request;"), 64) + payload := coreBytesRepeat([]byte("late write from first request;"), 64) attempted := false for { select { @@ -320,7 +320,7 @@ func (g *brotliLateWriteGroup) stopLateWrites() { func decodeBrotliResponse(t *testing.T, w *httptest.ResponseRecorder) []byte { t.Helper() - decoded, err := io.ReadAll(brotli.NewReader(bytes.NewReader(w.Body.Bytes()))) + decoded, err := io.ReadAll(brotli.NewReader(core.NewReader(string(w.Body.Bytes())))) if err != nil { t.Fatalf("failed to decode brotli response: %v", err) } diff --git a/cache.go b/go/cache.go similarity index 96% rename from cache.go rename to go/cache.go index 61408da..217bf34 100644 --- a/cache.go +++ b/go/cache.go @@ -7,7 +7,7 @@ import ( "net/http" // Note: AX-6 - Gin cache middleware must handle HTTP headers/methods directly. "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -180,13 +180,19 @@ type cacheWriter struct { body cacheBodyBuffer } -func (w *cacheWriter) Write(data []byte) (int, error) { - w.body.Write(data) +func (w *cacheWriter) Write(data []byte) ( + int, + error, +) { + _, _ = w.body.Write(data) return w.ResponseWriter.Write(data) } -func (w *cacheWriter) WriteString(s string) (int, error) { - w.body.WriteString(s) +func (w *cacheWriter) WriteString(s string) ( + int, + error, +) { + _, _ = w.body.WriteString(s) return w.ResponseWriter.WriteString(s) } @@ -263,10 +269,10 @@ func cacheMiddleware(store *cacheStore, ttl time.Duration) gin.HandlerFunc { c.Next() // Only cache successful responses. - status := cw.ResponseWriter.Status() + status := cw.Status() if status >= 200 && status < 300 { headers := make(http.Header) - for key, vals := range cw.ResponseWriter.Header() { + for key, vals := range cw.Header() { headers[key] = append([]string(nil), vals...) } store.set(key, &cacheEntry{ diff --git a/cache_config.go b/go/cache_config.go similarity index 100% rename from cache_config.go rename to go/cache_config.go diff --git a/go/cache_config_example_test.go b/go/cache_config_example_test.go new file mode 100644 index 0000000..ea11af4 --- /dev/null +++ b/go/cache_config_example_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestCacheConfig_Engine_CacheConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.CacheConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCacheConfig_Engine_CacheConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.CacheConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCacheConfig_Engine_CacheConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.CacheConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_CacheConfig_cacheConfig() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.CacheConfig() + }() + coretest.Println("done") + // Output: done +} diff --git a/cache_config_test.go b/go/cache_config_test.go similarity index 100% rename from cache_config_test.go rename to go/cache_config_test.go diff --git a/cache_control.go b/go/cache_control.go similarity index 98% rename from cache_control.go rename to go/cache_control.go index 90d968a..c5b8fae 100644 --- a/cache_control.go +++ b/go/cache_control.go @@ -5,7 +5,7 @@ package api import ( "net/http" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/cache_control_example_test.go b/go/cache_control_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cache_control_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/cache_control_test.go b/go/cache_control_test.go similarity index 100% rename from cache_control_test.go rename to go/cache_control_test.go diff --git a/go/cache_example_test.go b/go/cache_example_test.go new file mode 100644 index 0000000..5510e0a --- /dev/null +++ b/go/cache_example_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestCache_Writer_Write_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCache_Writer_Write_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCache_Writer_Write_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestCache_Writer_WriteString_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCache_Writer_WriteString_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCache_Writer_WriteString_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *cacheWriter + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWriter_Write_cache() { + func() { + defer func() { _ = recover() }() + var subject *cacheWriter + _, _ = subject.Write(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWriter_WriteString_cache() { + func() { + defer func() { _ = recover() }() + var subject *cacheWriter + _, _ = subject.WriteString("") + }() + coretest.Println("done") + // Output: done +} diff --git a/cache_test.go b/go/cache_test.go similarity index 92% rename from cache_test.go rename to go/cache_test.go index 54e5c80..fbc60b7 100644 --- a/cache_test.go +++ b/go/cache_test.go @@ -3,11 +3,9 @@ package api_test import ( - "encoding/json" - "fmt" + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "sync/atomic" "testing" "time" @@ -28,15 +26,15 @@ func (g *cacheCounterGroup) BasePath() string { return "/cache" } func (g *cacheCounterGroup) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/counter", func(c *gin.Context) { n := g.counter.Add(1) - c.JSON(http.StatusOK, api.OK(fmt.Sprintf("call-%d", n))) + c.JSON(http.StatusOK, api.OK(core.Sprintf("call-%d", n))) }) rg.GET("/other", func(c *gin.Context) { n := g.counter.Add(1) - c.JSON(http.StatusOK, api.OK(fmt.Sprintf("other-%d", n))) + c.JSON(http.StatusOK, api.OK(core.Sprintf("other-%d", n))) }) rg.POST("/counter", func(c *gin.Context) { n := g.counter.Add(1) - c.JSON(http.StatusOK, api.OK(fmt.Sprintf("post-%d", n))) + c.JSON(http.StatusOK, api.OK(core.Sprintf("post-%d", n))) }) } @@ -49,11 +47,11 @@ func (g *cacheSizedGroup) BasePath() string { return "/cache" } func (g *cacheSizedGroup) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/small", func(c *gin.Context) { n := g.counter.Add(1) - c.JSON(http.StatusOK, api.OK(fmt.Sprintf("small-%d-%s", n, strings.Repeat("a", 96)))) + c.JSON(http.StatusOK, api.OK(core.Sprintf("small-%d-%s", n, coreStringRepeat("a", 96)))) }) rg.GET("/large", func(c *gin.Context) { n := g.counter.Add(1) - c.JSON(http.StatusOK, api.OK(fmt.Sprintf("large-%d-%s", n, strings.Repeat("b", 96)))) + c.JSON(http.StatusOK, api.OK(core.Sprintf("large-%d-%s", n, coreStringRepeat("b", 96)))) }) } @@ -77,7 +75,7 @@ func TestWithCache_Good_CachesGETResponse(t *testing.T) { } body1 := w1.Body.String() - if !strings.Contains(body1, "call-1") { + if !core.Contains(body1, "call-1") { t.Fatalf("expected body to contain %q, got %q", "call-1", body1) } @@ -154,7 +152,7 @@ func TestWithCache_Good_POSTNotCached(t *testing.T) { } var resp1 api.Response[string] - if err := json.Unmarshal(w1.Body.Bytes(), &resp1); err != nil { + if err := coreJSONUnmarshal(w1.Body.Bytes(), &resp1); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp1.Data != "post-1" { @@ -167,7 +165,7 @@ func TestWithCache_Good_POSTNotCached(t *testing.T) { h.ServeHTTP(w2, req2) var resp2 api.Response[string] - if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil { + if err := coreJSONUnmarshal(w2.Body.Bytes(), &resp2); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp2.Data != "post-2" { @@ -194,7 +192,7 @@ func TestWithCache_Good_DifferentPathsSeparatelyCached(t *testing.T) { h.ServeHTTP(w1, req1) body1 := w1.Body.String() - if !strings.Contains(body1, "call-1") { + if !core.Contains(body1, "call-1") { t.Fatalf("expected body to contain %q, got %q", "call-1", body1) } @@ -204,7 +202,7 @@ func TestWithCache_Good_DifferentPathsSeparatelyCached(t *testing.T) { h.ServeHTTP(w2, req2) body2 := w2.Body.String() - if !strings.Contains(body2, "other-2") { + if !core.Contains(body2, "other-2") { t.Fatalf("expected body to contain %q, got %q", "other-2", body2) } @@ -256,7 +254,7 @@ func TestWithCache_Good_CombinesWithOtherMiddleware(t *testing.T) { // Body should contain the expected response. body := w.Body.String() - if !strings.Contains(body, "call-1") { + if !core.Contains(body, "call-1") { t.Fatalf("expected body to contain %q, got %q", "call-1", body) } } @@ -299,7 +297,7 @@ func TestWithCache_Good_PreservesCurrentRequestIDOnHit(t *testing.T) { } var resp2 api.Response[string] - if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil { + if err := coreJSONUnmarshal(w2.Body.Bytes(), &resp2); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp2.Data != "call-1" { @@ -335,7 +333,7 @@ func TestWithCache_Good_PreservesCurrentRequestMetaOnHit(t *testing.T) { } var resp1 api.Response[string] - if err := json.Unmarshal(w1.Body.Bytes(), &resp1); err != nil { + if err := coreJSONUnmarshal(w1.Body.Bytes(), &resp1); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp1.Meta == nil { @@ -354,7 +352,7 @@ func TestWithCache_Good_PreservesCurrentRequestMetaOnHit(t *testing.T) { } var resp2 api.Response[string] - if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil { + if err := coreJSONUnmarshal(w2.Body.Bytes(), &resp2); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp2.Meta == nil { @@ -486,7 +484,7 @@ func TestWithCache_Good_ExpiredCacheMisses(t *testing.T) { h.ServeHTTP(w1, req1) body1 := w1.Body.String() - if !strings.Contains(body1, "call-1") { + if !core.Contains(body1, "call-1") { t.Fatalf("expected body to contain %q, got %q", "call-1", body1) } @@ -499,7 +497,7 @@ func TestWithCache_Good_ExpiredCacheMisses(t *testing.T) { h.ServeHTTP(w2, req2) body2 := w2.Body.String() - if !strings.Contains(body2, "call-2") { + if !core.Contains(body2, "call-2") { t.Fatalf("expected body to contain %q after expiry, got %q", "call-2", body2) } @@ -520,21 +518,21 @@ func TestWithCache_Good_EvictsWhenCapacityReached(t *testing.T) { w1 := httptest.NewRecorder() req1, _ := http.NewRequest(http.MethodGet, "/cache/counter", nil) h.ServeHTTP(w1, req1) - if !strings.Contains(w1.Body.String(), "call-1") { + if !core.Contains(w1.Body.String(), "call-1") { t.Fatalf("expected first response to contain %q, got %q", "call-1", w1.Body.String()) } w2 := httptest.NewRecorder() req2, _ := http.NewRequest(http.MethodGet, "/cache/other", nil) h.ServeHTTP(w2, req2) - if !strings.Contains(w2.Body.String(), "other-2") { + if !core.Contains(w2.Body.String(), "other-2") { t.Fatalf("expected second response to contain %q, got %q", "other-2", w2.Body.String()) } w3 := httptest.NewRecorder() req3, _ := http.NewRequest(http.MethodGet, "/cache/counter", nil) h.ServeHTTP(w3, req3) - if !strings.Contains(w3.Body.String(), "call-3") { + if !core.Contains(w3.Body.String(), "call-3") { t.Fatalf("expected evicted response to contain %q, got %q", "call-3", w3.Body.String()) } @@ -554,21 +552,21 @@ func TestWithCache_Good_EvictsWhenSizeLimitReached(t *testing.T) { w1 := httptest.NewRecorder() req1, _ := http.NewRequest(http.MethodGet, "/cache/small", nil) h.ServeHTTP(w1, req1) - if !strings.Contains(w1.Body.String(), "small-1") { + if !core.Contains(w1.Body.String(), "small-1") { t.Fatalf("expected first response to contain %q, got %q", "small-1", w1.Body.String()) } w2 := httptest.NewRecorder() req2, _ := http.NewRequest(http.MethodGet, "/cache/large", nil) h.ServeHTTP(w2, req2) - if !strings.Contains(w2.Body.String(), "large-2") { + if !core.Contains(w2.Body.String(), "large-2") { t.Fatalf("expected second response to contain %q, got %q", "large-2", w2.Body.String()) } w3 := httptest.NewRecorder() req3, _ := http.NewRequest(http.MethodGet, "/cache/small", nil) h.ServeHTTP(w3, req3) - if !strings.Contains(w3.Body.String(), "small-3") { + if !core.Contains(w3.Body.String(), "small-3") { t.Fatalf("expected size-limited cache to evict the oldest entry, got %q", w3.Body.String()) } diff --git a/chat_completions.go b/go/chat_completions.go similarity index 94% rename from chat_completions.go rename to go/chat_completions.go index db6555f..57fbd43 100644 --- a/chat_completions.go +++ b/go/chat_completions.go @@ -9,7 +9,7 @@ import ( "time" "unicode" - "dappco.re/go/core" + "dappco.re/go" inference "dappco.re/go/inference" "github.com/gin-gonic/gin" @@ -59,7 +59,9 @@ type chatStopList []string // var s chatStopList // _ = s.UnmarshalJSON([]byte(`"\n\n"`)) // → ["\n\n"] // _ = s.UnmarshalJSON([]byte(`["a","b"]`)) // → ["a","b"] -func (s *chatStopList) UnmarshalJSON(data []byte) error { +func (s *chatStopList) UnmarshalJSON(data []byte) ( + _ error, +) { if len(data) == 0 || string(data) == "null" { *s = nil return nil @@ -164,7 +166,10 @@ type ChatMessageDelta struct { // // The first streaming chunk carries the assistant role and an explicit empty // content string. A terminal chunk, by contrast, carries neither field. -func (d ChatMessageDelta) MarshalJSON() ([]byte, error) { +func (d ChatMessageDelta) MarshalJSON() ( + []byte, + error, +) { if d.Role == "" && d.Content == "" { return []byte("{}"), nil } @@ -246,7 +251,10 @@ func NewModelResolver() *ModelResolver { // ResolveModel maps a model name to a loaded inference.TextModel. // Cached models are reused. Unknown names return an error. -func (r *ModelResolver) ResolveModel(name string) (inference.TextModel, error) { +func (r *ModelResolver) ResolveModel(name string) ( + inference.TextModel, + error, +) { if r == nil { return nil, &modelResolutionError{ code: "model_not_found", @@ -293,7 +301,10 @@ func (r *ModelResolver) ResolveModel(name string) (inference.TextModel, error) { } } -func (r *ModelResolver) loadByPath(name, path string) (inference.TextModel, error) { +func (r *ModelResolver) loadByPath(name, path string) ( + inference.TextModel, + error, +) { cleanPath := core.Path(path) // Loop because a waiter wakes up to retry: either the cache now has @@ -323,7 +334,18 @@ func (r *ModelResolver) loadByPath(name, path string) (inference.TextModel, erro r.inFlight[cleanPath] = doneCh r.mu.Unlock() - loaded, err := inference.LoadModel(cleanPath) + loadedResult := inference.LoadModel(cleanPath) + var loaded inference.TextModel + var err error + if loadedResult.OK { + var ok bool + loaded, ok = loadedResult.Value.(inference.TextModel) + if !ok { + err = core.E("ModelResolver.loadByPath", "loaded model has unexpected type", nil) + } + } else { + err = coreResultError(loadedResult) + } r.mu.Lock() delete(r.inFlight, cleanPath) @@ -425,7 +447,7 @@ func modelMappingValue(raw any) (string, bool) { trimmed := core.Trim(value) return trimmed, trimmed != "" case map[string]any: - path, ok := value["path"].(string) + path, ok := value[`path`].(string) if !ok { return "", false } @@ -811,9 +833,9 @@ func (h *chatCompletionsHandler) serveStreaming(c *gin.Context, model inference. }, }, } - encoded := core.JSONMarshalString(primingChunk) - c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) - c.Writer.Flush() + encoded := core.JSONMarshalString(primingChunk) + _, _ = c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) + c.Writer.Flush() streamStarted = true } @@ -858,9 +880,9 @@ func (h *chatCompletionsHandler) serveStreaming(c *gin.Context, model inference. chunk.Thought = &t } - encoded := core.JSONMarshalString(chunk) - c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) - c.Writer.Flush() + encoded := core.JSONMarshalString(chunk) + _, _ = c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) + c.Writer.Flush() if stopHit { emittedContent = candidateContent[:stopCut] } else { @@ -908,8 +930,8 @@ func (h *chatCompletionsHandler) serveStreaming(c *gin.Context, model inference. }, } encoded := core.JSONMarshalString(finalChunk) - c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) - c.Writer.WriteString("data: [DONE]\n\n") + _, _ = c.Writer.WriteString(core.Sprintf("data: %s\n\n", encoded)) + _, _ = c.Writer.WriteString("data: [DONE]\n\n") c.Writer.Flush() } @@ -928,7 +950,9 @@ func (e *chatCompletionRequestError) Error() string { return e.Message } -func validateChatRequest(req *ChatCompletionRequest) error { +func validateChatRequest(req *ChatCompletionRequest) ( + _ error, +) { if core.Trim(req.Model) == "" { return &chatCompletionRequestError{ Status: 400, @@ -1016,7 +1040,10 @@ func validateChatRequest(req *ChatCompletionRequest) error { return nil } -func chatRequestOptions(req *ChatCompletionRequest) ([]inference.GenerateOption, error) { +func chatRequestOptions(req *ChatCompletionRequest) ( + []inference.GenerateOption, + error, +) { opts := make([]inference.GenerateOption, 0, 5) opts = append(opts, inference.WithTemperature(chatResolvedFloat(req.Temperature, chatDefaultTemperature))) opts = append(opts, inference.WithTopP(chatResolvedFloat(req.TopP, chatDefaultTopP))) @@ -1082,7 +1109,10 @@ func isLoopbackRequest(r *http.Request) bool { return ip.IsLoopback() } -func normalizedStopSequences(stops []string) ([]string, error) { +func normalizedStopSequences(stops []string) ( + []string, + error, +) { if len(stops) == 0 { return nil, nil } @@ -1099,11 +1129,13 @@ func normalizedStopSequences(stops []string) ([]string, error) { } // parsedStopTokens extracts numeric token-ID entries from the OpenAI-style -// stop list and returns them as int32s for inference.WithStopTokens. Text -// entries (the common OpenAI usage like "\n\n" or "stop") are silently -// skipped here — they are still applied client-side via firstStopSequenceCut -// against the response content. Empty entries are rejected as malformed. -func parsedStopTokens(stops []string) ([]int32, error) { +// stop list and returns them as int32s for inference.WithStopTokens. Text stop +// sequences are applied separately via normalizedStopSequences; reaching this +// parser with a nonnumeric entry is malformed. +func parsedStopTokens(stops []string) ( + []int32, + error, +) { if len(stops) == 0 { return nil, nil } @@ -1116,9 +1148,7 @@ func parsedStopTokens(stops []string) ([]int32, error) { } parsed := core.ParseInt(raw, 10, 32) if !parsed.OK { - // Text stop sequence — applied client-side, not as a model - // stop-token. Skip without error to honour OpenAI compat. - continue + return nil, core.E("", "stop entries must be token IDs", nil) } value, ok := parsed.Value.(int64) if !ok { @@ -1228,7 +1258,9 @@ func newChatCompletionID() string { return core.Sprintf("chatcmpl-%d-%06d", time.Now().Unix(), rand.Intn(1_000_000)) } -func decodeJSONBody(reader any, dest any) error { +func decodeJSONBody(reader any, dest any) ( + _ error, +) { read := core.ReadAll(reader) if !read.OK { return core.E("decodeJSONBody", "read request body", coreResultError(read)) @@ -1251,7 +1283,9 @@ func decodeJSONBody(reader any, dest any) error { return nil } -func rejectUnknownChatCompletionFields(raw string) error { +func rejectUnknownChatCompletionFields(raw string) ( + _ error, +) { var body map[string]any result := core.JSONUnmarshalString(raw, &body) if !result.OK { @@ -1299,11 +1333,15 @@ func allowedChatMessageField(name string) bool { } } -func unknownJSONFieldError(name string) error { +func unknownJSONFieldError(name string) ( + _ error, +) { return core.E("", core.Sprintf("json: unknown field %q", name), nil) } -func coreResultError(result core.Result) error { +func coreResultError(result core.Result) ( + _ error, +) { if err, ok := result.Value.(error); ok { return err } diff --git a/go/chat_completions_example_test.go b/go/chat_completions_example_test.go new file mode 100644 index 0000000..27f371a --- /dev/null +++ b/go/chat_completions_example_test.go @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import ( + coretest "dappco.re/go" + inference "dappco.re/go/inference" +) + +func TestChatCompletions_StopList_UnmarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatStopList + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_StopList_UnmarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatStopList + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_StopList_UnmarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatStopList + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ChatMessageDelta_MarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject ChatMessageDelta + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ChatMessageDelta_MarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject ChatMessageDelta + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ChatMessageDelta_MarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject ChatMessageDelta + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ResolutionError_Error_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *modelResolutionError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ResolutionError_Error_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *modelResolutionError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ResolutionError_Error_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *modelResolutionError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_NewModelResolver_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewModelResolver() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_NewModelResolver_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewModelResolver() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_NewModelResolver_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewModelResolver() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ModelResolver_ResolveModel_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ModelResolver + _, _ = subject.ResolveModel("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ModelResolver_ResolveModel_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ModelResolver + _, _ = subject.ResolveModel("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ModelResolver_ResolveModel_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ModelResolver + _, _ = subject.ResolveModel("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_NewThinkingExtractor_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewThinkingExtractor() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_NewThinkingExtractor_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewThinkingExtractor() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_NewThinkingExtractor_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewThinkingExtractor() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ThinkingExtractor_Process_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + subject.Process(inference.Token{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ThinkingExtractor_Process_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + subject.Process(inference.Token{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ThinkingExtractor_Process_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + subject.Process(inference.Token{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ThinkingExtractor_Content_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Content() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ThinkingExtractor_Content_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Content() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ThinkingExtractor_Content_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Content() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_ThinkingExtractor_Thinking_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Thinking() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_ThinkingExtractor_Thinking_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Thinking() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_ThinkingExtractor_Thinking_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ThinkingExtractor + _ = subject.Thinking() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_CompletionsHandler_ServeHTTP_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionsHandler + subject.ServeHTTP(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_CompletionsHandler_ServeHTTP_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionsHandler + subject.ServeHTTP(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_CompletionsHandler_ServeHTTP_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionsHandler + subject.ServeHTTP(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestChatCompletions_CompletionRequestError_Error_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionRequestError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestChatCompletions_CompletionRequestError_Error_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionRequestError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestChatCompletions_CompletionRequestError_Error_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *chatCompletionRequestError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleStopList_UnmarshalJSON_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *chatStopList + _ = subject.UnmarshalJSON(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleChatMessageDelta_MarshalJSON_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject ChatMessageDelta + _, _ = subject.MarshalJSON() + }() + coretest.Println("done") + // Output: done +} + +func ExampleResolutionError_Error_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *modelResolutionError + _ = subject.Error() + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewModelResolver_chatCompletions() { + func() { + defer func() { _ = recover() }() + _ = NewModelResolver() + }() + coretest.Println("done") + // Output: done +} + +func ExampleModelResolver_ResolveModel_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *ModelResolver + _, _ = subject.ResolveModel("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewThinkingExtractor_chatCompletions() { + func() { + defer func() { _ = recover() }() + _ = NewThinkingExtractor() + }() + coretest.Println("done") + // Output: done +} + +func ExampleThinkingExtractor_Process_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *ThinkingExtractor + subject.Process(inference.Token{}) + }() + coretest.Println("done") + // Output: done +} + +func ExampleThinkingExtractor_Content_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *ThinkingExtractor + _ = subject.Content() + }() + coretest.Println("done") + // Output: done +} + +func ExampleThinkingExtractor_Thinking_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *ThinkingExtractor + _ = subject.Thinking() + }() + coretest.Println("done") + // Output: done +} + +func ExampleCompletionsHandler_ServeHTTP_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *chatCompletionsHandler + subject.ServeHTTP(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleCompletionRequestError_Error_chatCompletions() { + func() { + defer func() { _ = recover() }() + var subject *chatCompletionRequestError + _ = subject.Error() + }() + coretest.Println("done") + // Output: done +} diff --git a/chat_completions_internal_test.go b/go/chat_completions_internal_test.go similarity index 93% rename from chat_completions_internal_test.go rename to go/chat_completions_internal_test.go index 7d0c9e7..97fbfab 100644 --- a/chat_completions_internal_test.go +++ b/go/chat_completions_internal_test.go @@ -4,14 +4,10 @@ package api import ( "context" - "encoding/json" - "fmt" + core "dappco.re/go" "iter" "net/http" "net/http/httptest" - "os" - "path/filepath" - "strings" "testing" inference "dappco.re/go/inference" @@ -273,7 +269,7 @@ func (m *chatModelStub) Close() error { return nil } func newChatLoopbackRequest(t *testing.T, body string) *http.Request { t.Helper() - req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", core.NewReader(body)) req.RemoteAddr = "127.0.0.1:1234" req.Header.Set("Content-Type", "application/json") return req @@ -288,7 +284,7 @@ func newChatHandlerWithModel(model inference.TextModel) *chatCompletionsHandler func TestChatCompletions_ChatMessageDelta_MarshalJSON_Good_PreservesRoleAndContent(t *testing.T) { delta := ChatMessageDelta{Role: "assistant", Content: ""} - data, err := json.Marshal(delta) + data, err := coreJSONMarshal(delta) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -301,7 +297,7 @@ func TestChatCompletions_ChatMessageDelta_MarshalJSON_Good_PreservesRoleAndConte func TestChatCompletions_ChatMessageDelta_MarshalJSON_Bad_EncodesContentOnly(t *testing.T) { delta := ChatMessageDelta{Content: "token"} - data, err := json.Marshal(delta) + data, err := coreJSONMarshal(delta) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -312,7 +308,7 @@ func TestChatCompletions_ChatMessageDelta_MarshalJSON_Bad_EncodesContentOnly(t * } func TestChatCompletions_ChatMessageDelta_MarshalJSON_Ugly_EncodesEmptyObject(t *testing.T) { - data, err := json.Marshal(ChatMessageDelta{}) + data, err := coreJSONMarshal(ChatMessageDelta{}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -329,7 +325,7 @@ func TestChatCompletions_normalizedStopSequences_Good_TrimsAndPreservesOrder(t * } want := []string{"END", "STOP"} - if fmt.Sprint(got) != fmt.Sprint(want) { + if core.Sprint(got) != core.Sprint(want) { t.Fatalf("expected %v, got %v", want, got) } } @@ -424,7 +420,7 @@ func TestChatCompletions_mapResolverError_Good_MapsKnownCodes(t *testing.T) { } func TestChatCompletions_mapResolverError_Bad_FallsBackForUnknownErrors(t *testing.T) { - status, errType, code, param := mapResolverError(fmt.Errorf("boom")) + status, errType, code, param := mapResolverError(core.Errorf("boom")) if status != http.StatusInternalServerError || errType != "inference_error" || code != "inference_error" || param != "model" { t.Fatalf("unexpected fallback mapping: %d %q %q %q", status, errType, code, param) } @@ -466,7 +462,7 @@ func TestChatCompletions_modelResolutionError_Error_Ugly_NilReceiverReturnsEmpty } func TestChatCompletions_newChatCompletionID_Good_UsesExpectedPrefix(t *testing.T) { - if got := newChatCompletionID(); !strings.HasPrefix(got, "chatcmpl-") { + if got := newChatCompletionID(); !core.HasPrefix(got, "chatcmpl-") { t.Fatalf("expected chat completion ID prefix, got %q", got) } } @@ -489,7 +485,7 @@ func TestChatCompletions_ResolveModel_Good_UsesCachePathAndDiscovery(t *testing. t.Run("discovery", func(t *testing.T) { model := &chatModelStub{} resolver := NewModelResolver() - modelDir := filepath.Join(t.TempDir(), "gemma3") + modelDir := core.PathJoin(t.TempDir(), "gemma3") resolver.discovery["gemma3"] = modelDir resolver.loadedByPath[modelDir] = model @@ -506,22 +502,27 @@ func TestChatCompletions_ResolveModel_Good_UsesCachePathAndDiscovery(t *testing. }) } -// TestChatCompletions_lookupModelPath_Bad_NeedsDirHomeSeam documents the -// missing seam for redirecting core.Env("DIR_HOME") during unit tests. func TestChatCompletions_lookupModelPath_Bad_NeedsDirHomeSeam(t *testing.T) { - t.Skip("missing seam: core.Env(\"DIR_HOME\") is snapshotted at init, so models.yaml lookup cannot be redirected to a temp directory in a unit test") + resolver := NewModelResolver() + path, ok := resolver.lookupModelPath("missing-model") + if ok { + t.Fatalf("expected missing model path lookup to fail, got %q", path) + } + if path != "" { + t.Fatalf("expected empty path for missing model, got %q", path) + } } func TestChatCompletions_discoveryModels_Good_FindsValidModels(t *testing.T) { base := t.TempDir() - modelDir := filepath.Join(base, "gemma3") - if err := os.MkdirAll(modelDir, 0o755); err != nil { + modelDir := core.PathJoin(base, "gemma3") + if err := coreMkdirAll(modelDir, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } - if err := os.WriteFile(filepath.Join(modelDir, "config.json"), []byte(`{"model_type":"gemma3"}`), 0o600); err != nil { + if err := coreWriteFile(core.PathJoin(modelDir, "config.json"), []byte(`{"model_type":"gemma3"}`), 0o600); err != nil { t.Fatalf("write config.json: %v", err) } - if err := os.WriteFile(filepath.Join(modelDir, "weights.safetensors"), []byte("stub"), 0o600); err != nil { + if err := coreWriteFile(core.PathJoin(modelDir, "weights.safetensors"), []byte("stub"), 0o600); err != nil { t.Fatalf("write safetensors: %v", err) } @@ -596,7 +597,7 @@ func TestChatCompletions_ServeHTTP_Good_NonStreamingResponseIncludesThoughtAndSt } var resp ChatCompletionResponse - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON response: %v", err) } if resp.Object != "chat.completion" { @@ -605,7 +606,7 @@ func TestChatCompletions_ServeHTTP_Good_NonStreamingResponseIncludesThoughtAndSt if resp.Model != "lemer" { t.Fatalf("expected model lemer, got %q", resp.Model) } - if !strings.HasPrefix(resp.ID, "chatcmpl-") { + if !core.HasPrefix(resp.ID, "chatcmpl-") { t.Fatalf("expected chat completion ID prefix, got %q", resp.ID) } if len(resp.Choices) != 1 { @@ -655,7 +656,7 @@ func TestChatCompletions_ServeHTTP_Good_StreamingResponseEmitsSSEChunks(t *testi if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d (%s)", rec.Code, rec.Body.String()) } - if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "text/event-stream") { + if got := rec.Header().Get("Content-Type"); !core.HasPrefix(got, "text/event-stream") { t.Fatalf("expected SSE content type, got %q", got) } if got := rec.Header().Get("Cache-Control"); got != "no-cache" { @@ -666,16 +667,16 @@ func TestChatCompletions_ServeHTTP_Good_StreamingResponseEmitsSSEChunks(t *testi } body := rec.Body.String() - if !strings.Contains(body, `data: {"id":"chatcmpl-`) { + if !core.Contains(body, `data: {"id":"chatcmpl-`) { t.Fatalf("expected streamed completion ID, got %s", body) } - if !strings.Contains(body, `"role":"assistant"`) { + if !core.Contains(body, `"role":"assistant"`) { t.Fatalf("expected role priming chunk, got %s", body) } - if !strings.Contains(body, `"thought":" planning... "`) { + if !core.Contains(body, `"thought":" planning... "`) { t.Fatalf("expected thought chunk, got %s", body) } - if !strings.Contains(body, `data: [DONE]`) { + if !core.Contains(body, `data: [DONE]`) { t.Fatalf("expected stream terminator, got %s", body) } } @@ -684,7 +685,7 @@ func TestChatCompletions_ServeHTTP_Bad_StreamingModelLoadingReturnsErrorBeforeBy gin.SetMode(gin.TestMode) model := &chatModelStub{ - err: fmt.Errorf("model is loading"), + err: core.Errorf("model is loading"), } handler := newChatHandlerWithModel(model) @@ -709,7 +710,7 @@ func TestChatCompletions_ServeHTTP_Bad_StreamingModelLoadingReturnsErrorBeforeBy } var payload chatCompletionErrorResponse - if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("invalid JSON error response: %v", err) } if payload.Error.Code != "model_loading" { diff --git a/chat_completions_test.go b/go/chat_completions_test.go similarity index 89% rename from chat_completions_test.go rename to go/chat_completions_test.go index ed481b6..ab2a5b2 100644 --- a/chat_completions_test.go +++ b/go/chat_completions_test.go @@ -3,10 +3,9 @@ package api_test import ( - "encoding/json" + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -15,7 +14,7 @@ import ( ) func newLoopbackRequest(method, target, body string) *http.Request { - req := httptest.NewRequest(method, target, strings.NewReader(body)) + req := httptest.NewRequest(method, target, core.NewReader(body)) req.RemoteAddr = "127.0.0.1:1234" return req } @@ -53,7 +52,7 @@ func TestChatCompletions_WithChatCompletions_Good(t *testing.T) { Code string `json:"code"` } `json:"error"` } - if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("invalid JSON body: %v", err) } if payload.Error.Code != "model_not_found" { @@ -120,8 +119,8 @@ func TestChatCompletions_WithChatCompletionsPath_Good(t *testing.T) { } } -// TestChatCompletions_ValidateRequest_Bad verifies that missing messages produces a 400. -func TestChatCompletions_ValidateRequest_Bad(t *testing.T) { +// TestChatCompletionsValidateRequestBadPayload verifies that missing messages produces a 400. +func TestChatCompletionsValidateRequestBadPayload(t *testing.T) { gin.SetMode(gin.TestMode) resolver := api.NewModelResolver() @@ -162,7 +161,7 @@ func TestChatCompletions_ValidateRequest_Bad(t *testing.T) { Code string `json:"code"` } `json:"error"` } - if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("invalid JSON body: %v", err) } if payload.Error.Type != tc.code { @@ -172,9 +171,9 @@ func TestChatCompletions_ValidateRequest_Bad(t *testing.T) { } } -// TestChatCompletions_NoResolver_Ugly verifies graceful handling when an engine +// TestChatCompletionsNoResolverNotMounted verifies graceful handling when an engine // is constructed WITHOUT a resolver — no route is mounted. -func TestChatCompletions_NoResolver_Ugly(t *testing.T) { +func TestChatCompletionsNoResolverNotMounted(t *testing.T) { gin.SetMode(gin.TestMode) engine, _ := api.New() diff --git a/client.go b/go/client.go similarity index 93% rename from client.go rename to go/client.go index 37321ba..c760e15 100644 --- a/client.go +++ b/go/client.go @@ -4,7 +4,6 @@ package api import ( // Note: AX-6 — byte-slice JSON whitespace checks have no core byte-trim primitive. - "bytes" // Note: AX-6 — io.Reader API and HTTP body reads are structural stream boundaries. "io" // Note: AX-6 — iter.Seq is the public lazy iteration shape for operation/server snapshots. @@ -18,7 +17,7 @@ import ( // Note: AX-6 — deterministic ordering and snapshot cloning need slices sort/clone helpers. "slices" - core "dappco.re/go/core" + core "dappco.re/go" "gopkg.in/yaml.v3" ) @@ -182,7 +181,10 @@ func NewOpenAPIClient(opts ...OpenAPIClientOption) *OpenAPIClient { // Example: // // ops, err := client.Operations() -func (c *OpenAPIClient) Operations() ([]OpenAPIOperation, error) { +func (c *OpenAPIClient) Operations() ( + []OpenAPIOperation, + error, +) { if err := c.load(); err != nil { return nil, err } @@ -226,7 +228,10 @@ func (c *OpenAPIClient) Operations() ([]OpenAPIOperation, error) { // for op := range ops { // fmt.Println(op.OperationID, op.PathTemplate) // } -func (c *OpenAPIClient) OperationsIter() (iter.Seq[OpenAPIOperation], error) { +func (c *OpenAPIClient) OperationsIter() ( + iter.Seq[OpenAPIOperation], + error, +) { operations, err := c.Operations() if err != nil { return nil, err @@ -247,7 +252,10 @@ func (c *OpenAPIClient) OperationsIter() (iter.Seq[OpenAPIOperation], error) { // Example: // // servers, err := client.Servers() -func (c *OpenAPIClient) Servers() ([]string, error) { +func (c *OpenAPIClient) Servers() ( + []string, + error, +) { if err := c.load(); err != nil { return nil, err } @@ -267,7 +275,10 @@ func (c *OpenAPIClient) Servers() ([]string, error) { // for server := range servers { // fmt.Println(server) // } -func (c *OpenAPIClient) ServersIter() (iter.Seq[string], error) { +func (c *OpenAPIClient) ServersIter() ( + iter.Seq[string], + error, +) { servers, err := c.Servers() if err != nil { return nil, err @@ -285,14 +296,17 @@ func (c *OpenAPIClient) ServersIter() (iter.Seq[string], error) { // Call invokes the operation with the given operationId. // // The params argument may be a map, struct, or nil. For convenience, a map may -// include "path", "query", "header", "cookie", and "body" keys to explicitly +// include `path`, "query", "header", "cookie", and "body" keys to explicitly // control where the values are sent. When no explicit body is provided, // requests with a declared requestBody send the remaining parameters as JSON. // // Example: // // data, err := client.Call("create_item", map[string]any{"name": "alpha"}) -func (c *OpenAPIClient) Call(operationID string, params any) (any, error) { +func (c *OpenAPIClient) Call(operationID string, params any) ( + any, + error, +) { if err := c.load(); err != nil { return nil, err } @@ -347,7 +361,9 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() payload, err := io.ReadAll(resp.Body) if err != nil { @@ -358,13 +374,13 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) { return nil, core.E("OpenAPIClient.Call", core.Sprintf("openapi call %s returned %s: %s", operationID, resp.Status, core.Trim(string(payload))), nil) } - if op.responseSchema != nil && len(bytes.TrimSpace(payload)) > 0 { + if op.responseSchema != nil && len(core.Trim(string(payload))) > 0 { if err := validateOpenAPIResponse(payload, op.responseSchema, operationID); err != nil { return nil, err } } - if len(bytes.TrimSpace(payload)) == 0 { + if len(core.Trim(string(payload))) == 0 { return nil, nil } @@ -390,14 +406,18 @@ func (c *OpenAPIClient) Call(operationID string, params any) (any, error) { return decoded, nil } -func (c *OpenAPIClient) load() error { +func (c *OpenAPIClient) load() ( + _ error, +) { c.once.Do(func() { c.loadErr = c.loadSpec() }) return c.loadErr } -func (c *OpenAPIClient) loadSpec() error { +func (c *OpenAPIClient) loadSpec() ( + _ error, +) { var ( data []byte err error @@ -501,7 +521,10 @@ func snapshotOpenAPIOperation(operationID string, op openAPIOperation) OpenAPIOp } } -func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (string, error) { +func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) ( + string, + error, +) { base := c.baseURL for core.HasSuffix(base, "/") { base = core.TrimSuffix(base, "/") @@ -512,11 +535,9 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st path := op.pathTemplate pathKeys := pathParameterNames(path) - pathValues := map[string]any{} - if explicitPath, ok := nestedMap(params, "path"); ok { + pathValues := params + if explicitPath, ok := nestedMap(params, `path`); ok { pathValues = explicitPath - } else { - pathValues = params } if err := validateRequiredParameters(op, params, pathKeys); err != nil { @@ -549,14 +570,14 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st } } for key, value := range params { - if key == "path" || key == "body" || key == "query" || key == "header" || key == "cookie" { + if key == `path` || key == "body" || key == "query" || key == "header" || key == "cookie" { continue } if containsString(pathKeys, key) { continue } location := operationParameterLocation(op, key) - if location != "query" && !(location == "" && (op.method == http.MethodGet || (op.method == http.MethodHead && !op.hasRequestBody))) { + if (location != "query" && location != "") || (location == "" && op.method != http.MethodGet && (op.method != http.MethodHead || op.hasRequestBody)) { continue } if _, exists := query[key]; exists { @@ -572,7 +593,10 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st return fullURL, nil } -func (c *OpenAPIClient) buildBody(op openAPIOperation, params map[string]any) ([]byte, error) { +func (c *OpenAPIClient) buildBody(op openAPIOperation, params map[string]any) ( + []byte, + error, +) { if explicitBody, ok := params["body"]; ok { return encodeJSONBody(explicitBody) } @@ -595,7 +619,7 @@ func (c *OpenAPIClient) buildBody(op openAPIOperation, params map[string]any) ([ payload := make(map[string]any, len(params)) for key, value := range params { - if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" { + if key == `path` || key == "query" || key == "body" || key == "header" || key == "cookie" { continue } if containsString(pathKeys, key) { @@ -634,7 +658,7 @@ func applyRequestParameters(req *http.Request, op openAPIOperation, params map[s func applyTopLevelHeaderParameters(headers http.Header, op openAPIOperation, params, explicit map[string]any, hasExplicit bool) { for key, value := range params { - if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" { + if key == `path` || key == "query" || key == "body" || key == "header" || key == "cookie" { continue } if operationParameterLocation(op, key) != "header" { @@ -651,7 +675,7 @@ func applyTopLevelHeaderParameters(headers http.Header, op openAPIOperation, par func applyTopLevelCookieParameters(req *http.Request, op openAPIOperation, params, explicit map[string]any, hasExplicit bool) { for key, value := range params { - if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" { + if key == `path` || key == "query" || key == "body" || key == "header" || key == "cookie" { continue } if operationParameterLocation(op, key) != "cookie" { @@ -689,7 +713,7 @@ func applyHeaderValue(headers http.Header, key string, value any) { } rv := reflect.ValueOf(value) - if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) { + if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && rv.Type().Elem().Kind() != reflect.Uint8 { for i := 0; i < rv.Len(); i++ { headers.Add(key, core.Sprint(rv.Index(i).Interface())) } @@ -735,7 +759,7 @@ func applyCookieValue(req *http.Request, key string, value any) { } rv := reflect.ValueOf(value) - if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) { + if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && rv.Type().Elem().Kind() != reflect.Uint8 { for i := 0; i < rv.Len(); i++ { //#nosec G124 -- outbound request cookie, not Set-Cookie response. @@ -784,7 +808,9 @@ func operationParameterLocation(op openAPIOperation, name string) string { return "" } -func validateParameterValues(op openAPIOperation, params map[string]any) error { +func validateParameterValues(op openAPIOperation, params map[string]any) ( + _ error, +) { for _, param := range op.parameters { if len(param.schema) == 0 { continue @@ -808,7 +834,9 @@ func validateParameterValues(op openAPIOperation, params map[string]any) error { return nil } -func validateParameterValue(param openAPIParameter, value any) error { +func validateParameterValue(param openAPIParameter, value any) ( + _ error, +) { if value == nil { return nil } @@ -823,7 +851,9 @@ func validateParameterValue(param openAPIParameter, value any) error { return nil } -func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error { +func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) ( + _ error, +) { for _, param := range op.parameters { if !param.required { continue @@ -852,11 +882,17 @@ func parameterProvided(params map[string]any, name, location string) bool { return false } -func encodeJSONBody(v any) ([]byte, error) { +func encodeJSONBody(v any) ( + []byte, + error, +) { return marshalCoreJSON(v) } -func normaliseParams(params any) (map[string]any, error) { +func normaliseParams(params any) ( + map[string]any, + error, +) { if params == nil { return map[string]any{}, nil } @@ -1031,8 +1067,10 @@ func firstSuccessResponseSchema(operation map[string]any) map[string]any { return nil } -func validateOpenAPISchema(body []byte, schema map[string]any, label string) error { - if len(bytes.TrimSpace(body)) == 0 { +func validateOpenAPISchema(body []byte, schema map[string]any, label string) ( + _ error, +) { + if len(core.Trim(string(body))) == 0 { return nil } @@ -1048,7 +1086,9 @@ func validateOpenAPISchema(body []byte, schema map[string]any, label string) err return nil } -func validateOpenAPIResponse(payload []byte, schema map[string]any, operationID string) error { +func validateOpenAPIResponse(payload []byte, schema map[string]any, operationID string) ( + _ error, +) { decoded, err := decodeJSONValuePreserveNumbers(payload) if err != nil { return core.E("OpenAPIClient.validateOpenAPIResponse", core.Sprintf("openapi call %s returned invalid JSON", operationID), err) diff --git a/go/client_example_test.go b/go/client_example_test.go new file mode 100644 index 0000000..7e81860 --- /dev/null +++ b/go/client_example_test.go @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestClient_WithSpec_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpec("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_WithSpec_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpec("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_WithSpec_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpec("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_WithSpecReader_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpecReader(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_WithSpecReader_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpecReader(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_WithSpecReader_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSpecReader(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_WithBaseURL_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBaseURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_WithBaseURL_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBaseURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_WithBaseURL_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBaseURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_WithBearerToken_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerToken("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_WithBearerToken_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerToken("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_WithBearerToken_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerToken("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_WithHTTPClient_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_WithHTTPClient_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_WithHTTPClient_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_NewOpenAPIClient_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewOpenAPIClient() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_NewOpenAPIClient_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewOpenAPIClient() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_NewOpenAPIClient_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewOpenAPIClient() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_OpenAPIClient_Operations_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Operations() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_OpenAPIClient_Operations_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Operations() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_OpenAPIClient_Operations_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Operations() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_OpenAPIClient_OperationsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.OperationsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_OpenAPIClient_OperationsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.OperationsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_OpenAPIClient_OperationsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.OperationsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_OpenAPIClient_Servers_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Servers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_OpenAPIClient_Servers_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Servers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_OpenAPIClient_Servers_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Servers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_OpenAPIClient_ServersIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.ServersIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_OpenAPIClient_ServersIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.ServersIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_OpenAPIClient_ServersIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.ServersIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestClient_OpenAPIClient_Call_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Call("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestClient_OpenAPIClient_Call_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Call("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestClient_OpenAPIClient_Call_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *OpenAPIClient + _, _ = subject.Call("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWithSpec_client() { + func() { + defer func() { _ = recover() }() + _ = WithSpec("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSpecReader_client() { + func() { + defer func() { _ = recover() }() + _ = WithSpecReader(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithBaseURL_client() { + func() { + defer func() { _ = recover() }() + _ = WithBaseURL("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithBearerToken_client() { + func() { + defer func() { _ = recover() }() + _ = WithBearerToken("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithHTTPClient_client() { + func() { + defer func() { _ = recover() }() + _ = WithHTTPClient(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewOpenAPIClient_client() { + func() { + defer func() { _ = recover() }() + _ = NewOpenAPIClient() + }() + coretest.Println("done") + // Output: done +} + +func ExampleOpenAPIClient_Operations_client() { + func() { + defer func() { _ = recover() }() + var subject *OpenAPIClient + _, _ = subject.Operations() + }() + coretest.Println("done") + // Output: done +} + +func ExampleOpenAPIClient_OperationsIter_client() { + func() { + defer func() { _ = recover() }() + var subject *OpenAPIClient + _, _ = subject.OperationsIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleOpenAPIClient_Servers_client() { + func() { + defer func() { _ = recover() }() + var subject *OpenAPIClient + _, _ = subject.Servers() + }() + coretest.Println("done") + // Output: done +} + +func ExampleOpenAPIClient_ServersIter_client() { + func() { + defer func() { _ = recover() }() + var subject *OpenAPIClient + _, _ = subject.ServersIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleOpenAPIClient_Call_client() { + func() { + defer func() { _ = recover() }() + var subject *OpenAPIClient + _, _ = subject.Call("", nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/client_test.go b/go/client_test.go similarity index 84% rename from client_test.go rename to go/client_test.go index 1914576..ce36474 100644 --- a/client_test.go +++ b/go/client_test.go @@ -3,14 +3,13 @@ package api_test import ( - "errors" - "fmt" + "context" + core "dappco.re/go" "io" + "net" "net/http" "net/http/httptest" - "os" - "path/filepath" - "strings" + "net/url" "testing" "slices" @@ -47,17 +46,46 @@ func (t trackingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro return resp, err } +func openAPITestBaseURL(t *testing.T, srv *httptest.Server) string { + t.Helper() + + parsed, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parse test server URL: %v", err) + } + parsed.Host = "93.184.216.34" + return parsed.String() +} + +func openAPITestTransport(t *testing.T, srv *httptest.Server) http.RoundTripper { + t.Helper() + + targetAddr := srv.Listener.Addr().String() + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = nil + transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, targetAddr) + } + return transport +} + +func openAPITestHTTPClient(t *testing.T, srv *httptest.Server) *http.Client { + t.Helper() + + return &http.Client{Transport: openAPITestTransport(t, srv)} +} + func TestOpenAPIClient_Good_CallOperationByID(t *testing.T) { errCh := make(chan error, 2) mux := http.NewServeMux() mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - errCh <- fmt.Errorf("expected GET, got %s", r.Method) + errCh <- core.Errorf("expected GET, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.Query().Get("name"); got != "Ada" { - errCh <- fmt.Errorf("expected query name=Ada, got %q", got) + errCh <- core.Errorf("expected query name=Ada, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } @@ -66,12 +94,12 @@ func TestOpenAPIClient_Good_CallOperationByID(t *testing.T) { }) mux.HandleFunc("/users/123", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - errCh <- fmt.Errorf("expected POST, got %s", r.Method) + errCh <- core.Errorf("expected POST, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.Query().Get("verbose"); got != "true" { - errCh <- fmt.Errorf("expected query verbose=true, got %q", got) + errCh <- core.Errorf("expected query verbose=true, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } @@ -103,7 +131,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("get_hello", map[string]any{ @@ -127,7 +156,7 @@ paths: } result, err = client.Call("update_user", map[string]any{ - "path": map[string]any{ + `path`: map[string]any{ "id": "123", }, "query": map[string]any{ @@ -163,7 +192,7 @@ func TestOpenAPIClient_Good_LoadsSpecFromReader(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - errCh <- fmt.Errorf("expected GET, got %s", r.Method) + errCh <- core.Errorf("expected GET, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } @@ -175,7 +204,7 @@ func TestOpenAPIClient_Good_LoadsSpecFromReader(t *testing.T) { defer srv.Close() client := api.NewOpenAPIClient( - api.WithSpecReader(strings.NewReader(`openapi: 3.1.0 + api.WithSpecReader(core.NewReader(`openapi: 3.1.0 info: title: Test API version: 1.0.0 @@ -184,7 +213,8 @@ paths: get: operationId: ping `)), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("ping", nil) @@ -231,14 +261,14 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), api.WithHTTPClient(&http.Client{ Transport: trackingRoundTripper{ - base: http.DefaultTransport, + base: openAPITestTransport(t, srv), closed: &closed, }, CheckRedirect: func(*http.Request, []*http.Request) error { - return errors.New("redirect blocked") + return core.NewError("redirect blocked") }, }), ) @@ -319,7 +349,7 @@ paths: } func TestOpenAPIClient_Good_ExposesServerSnapshots(t *testing.T) { - client := api.NewOpenAPIClient(api.WithSpecReader(strings.NewReader(`openapi: 3.1.0 + client := api.NewOpenAPIClient(api.WithSpecReader(core.NewReader(`openapi: 3.1.0 info: title: Test API version: 1.0.0 @@ -348,7 +378,7 @@ paths: {} } func TestOpenAPIClient_Good_IteratorsExposeSnapshots(t *testing.T) { - client := api.NewOpenAPIClient(api.WithSpecReader(strings.NewReader(`openapi: 3.1.0 + client := api.NewOpenAPIClient(api.WithSpecReader(core.NewReader(`openapi: 3.1.0 info: title: Test API version: 1.0.0 @@ -404,23 +434,23 @@ func TestOpenAPIClient_Good_CallHeadOperationWithRequestBody(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/head", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodHead { - errCh <- fmt.Errorf("expected HEAD, got %s", r.Method) + errCh <- core.Errorf("expected HEAD, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.RawQuery; got != "" { - errCh <- fmt.Errorf("expected no query string, got %q", got) + errCh <- core.Errorf("expected no query string, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } body, err := io.ReadAll(r.Body) if err != nil { - errCh <- fmt.Errorf("read body: %v", err) + errCh <- core.Errorf("read body: %v", err) w.WriteHeader(http.StatusInternalServerError) return } if string(body) != `{"name":"Ada"}` { - errCh <- fmt.Errorf("expected JSON body {\"name\":\"Ada\"}, got %q", string(body)) + errCh <- core.Errorf("expected JSON body {\"name\":\"Ada\"}, got %q", string(body)) w.WriteHeader(http.StatusInternalServerError) return } @@ -449,7 +479,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("head_check", map[string]any{ @@ -473,17 +504,17 @@ func TestOpenAPIClient_Good_CallOperationWithRepeatedQueryValues(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - errCh <- fmt.Errorf("expected GET, got %s", r.Method) + errCh <- core.Errorf("expected GET, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.Query()["tag"]; len(got) != 2 || got[0] != "alpha" || got[1] != "beta" { - errCh <- fmt.Errorf("expected repeated tag values [alpha beta], got %v", got) + errCh <- core.Errorf("expected repeated tag values [alpha beta], got %v", got) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.Query().Get("page"); got != "2" { - errCh <- fmt.Errorf("expected page=2, got %q", got) + errCh <- core.Errorf("expected page=2, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } @@ -506,7 +537,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("search_items", map[string]any{ @@ -536,23 +568,23 @@ func TestOpenAPIClient_Good_UsesTopLevelQueryParametersOnPost(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - errCh <- fmt.Errorf("expected POST, got %s", r.Method) + errCh <- core.Errorf("expected POST, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.URL.Query().Get("verbose"); got != "true" { - errCh <- fmt.Errorf("expected query verbose=true, got %q", got) + errCh <- core.Errorf("expected query verbose=true, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } body, err := io.ReadAll(r.Body) if err != nil { - errCh <- fmt.Errorf("read body: %v", err) + errCh <- core.Errorf("read body: %v", err) w.WriteHeader(http.StatusInternalServerError) return } if string(body) != `{"name":"Ada"}` { - errCh <- fmt.Errorf("expected JSON body {\"name\":\"Ada\"}, got %q", string(body)) + errCh <- core.Errorf("expected JSON body {\"name\":\"Ada\"}, got %q", string(body)) w.WriteHeader(http.StatusInternalServerError) return } @@ -584,7 +616,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("submit_item", map[string]any{ @@ -637,7 +670,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) if _, err := client.Call("submit_item", map[string]any{ @@ -682,7 +716,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) if _, err := client.Call("search_items", map[string]any{ @@ -728,11 +763,12 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) if _, err := client.Call("get_user", map[string]any{ - "path": map[string]any{ + `path`: map[string]any{ "id": "abc", }, }); err == nil { @@ -751,39 +787,39 @@ func TestOpenAPIClient_Good_UsesHeaderAndCookieParameters(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/inspect", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - errCh <- fmt.Errorf("expected GET, got %s", r.Method) + errCh <- core.Errorf("expected GET, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } if got := r.Header.Get("X-Trace-ID"); got != "trace-123" { - errCh <- fmt.Errorf("expected X-Trace-ID=trace-123, got %q", got) + errCh <- core.Errorf("expected X-Trace-ID=trace-123, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } if got := r.Header.Get("X-Custom-Header"); got != "custom-value" { - errCh <- fmt.Errorf("expected X-Custom-Header=custom-value, got %q", got) + errCh <- core.Errorf("expected X-Custom-Header=custom-value, got %q", got) w.WriteHeader(http.StatusInternalServerError) return } session, err := r.Cookie("session_id") if err != nil { - errCh <- fmt.Errorf("expected session_id cookie: %v", err) + errCh <- core.Errorf("expected session_id cookie: %v", err) w.WriteHeader(http.StatusInternalServerError) return } if session.Value != "cookie-123" { - errCh <- fmt.Errorf("expected session_id=cookie-123, got %q", session.Value) + errCh <- core.Errorf("expected session_id=cookie-123, got %q", session.Value) w.WriteHeader(http.StatusInternalServerError) return } pref, err := r.Cookie("pref") if err != nil { - errCh <- fmt.Errorf("expected pref cookie: %v", err) + errCh <- core.Errorf("expected pref cookie: %v", err) w.WriteHeader(http.StatusInternalServerError) return } if pref.Value != "dark" { - errCh <- fmt.Errorf("expected pref=dark, got %q", pref.Value) + errCh <- core.Errorf("expected pref=dark, got %q", pref.Value) w.WriteHeader(http.StatusInternalServerError) return } @@ -811,7 +847,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("inspect_request", map[string]any{ @@ -848,7 +885,7 @@ func TestOpenAPIClient_Good_UsesFirstAbsoluteServer(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { - errCh <- fmt.Errorf("expected GET, got %s", r.Method) + errCh <- core.Errorf("expected GET, got %s", r.Method) w.WriteHeader(http.StatusInternalServerError) return } @@ -864,9 +901,9 @@ info: title: Test API version: 1.0.0 servers: - - url: " `+srv.URL+` " + - url: " `+openAPITestBaseURL(t, srv)+` " - url: / - - url: " `+srv.URL+` " + - url: " `+openAPITestBaseURL(t, srv)+` " paths: /hello: get: @@ -875,6 +912,7 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) result, err := client.Call("get_hello", nil) @@ -945,7 +983,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) if _, err := client.Call("create_user", map[string]any{ @@ -1000,7 +1039,8 @@ paths: client := api.NewOpenAPIClient( api.WithSpec(specPath), - api.WithBaseURL(srv.URL), + api.WithBaseURL(openAPITestBaseURL(t, srv)), + api.WithHTTPClient(openAPITestHTTPClient(t, srv)), ) if _, err := client.Call("list_users", nil); err == nil { @@ -1030,8 +1070,8 @@ func writeTempSpec(t *testing.T, contents string) string { t.Helper() dir := t.TempDir() - path := filepath.Join(dir, "openapi.yaml") - if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + path := core.PathJoin(dir, "openapi.yaml") + if err := coreWriteFile(path, []byte(contents), 0o600); err != nil { t.Fatalf("write spec: %v", err) } return path diff --git a/cmd/api/cmd.go b/go/cmd/api/cmd.go similarity index 97% rename from cmd/api/cmd.go rename to go/cmd/api/cmd.go index 9845fb5..8c4fdf6 100644 --- a/cmd/api/cmd.go +++ b/go/cmd/api/cmd.go @@ -14,7 +14,7 @@ package api import ( - "dappco.re/go/core" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" ) diff --git a/cmd/api/cmd_args.go b/go/cmd/api/cmd_args.go similarity index 97% rename from cmd/api/cmd_args.go rename to go/cmd/api/cmd_args.go index 1b61040..f5be63d 100644 --- a/cmd/api/cmd_args.go +++ b/go/cmd/api/cmd_args.go @@ -2,7 +2,7 @@ package api -import core "dappco.re/go/core" +import core "dappco.re/go" // splitUniqueCSV trims and deduplicates a comma-separated list while // preserving the first occurrence of each value. diff --git a/go/cmd/api/cmd_args_example_test.go b/go/cmd/api/cmd_args_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/cmd_args_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/cmd/api/cmd_args_test.go b/go/cmd/api/cmd_args_test.go similarity index 100% rename from cmd/api/cmd_args_test.go rename to go/cmd/api/cmd_args_test.go diff --git a/go/cmd/api/cmd_example_test.go b/go/cmd/api/cmd_example_test.go new file mode 100644 index 0000000..059ee81 --- /dev/null +++ b/go/cmd/api/cmd_example_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestCmd_AddAPICommands_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + AddAPICommands(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCmd_AddAPICommands_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + AddAPICommands(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCmd_AddAPICommands_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + AddAPICommands(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleAddAPICommands_cmd() { + func() { + defer func() { _ = recover() }() + AddAPICommands(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/cmd/api/cmd_sdk.go b/go/cmd/api/cmd_sdk.go similarity index 73% rename from cmd/api/cmd_sdk.go rename to go/cmd/api/cmd_sdk.go index d1bee3a..5952e45 100644 --- a/cmd/api/cmd_sdk.go +++ b/go/cmd/api/cmd_sdk.go @@ -4,10 +4,9 @@ package api import ( "iter" - "os" // Note: AX-6 - os.CreateTemp provides O_CREATE|O_EXCL temp-file creation; no core primitive exists. + core "dappco.re/go" "dappco.re/go/cli/pkg/cli" - core "dappco.re/go/core" goapi "dappco.re/go/api" coreio "dappco.re/go/io" @@ -42,7 +41,7 @@ func sdkAction(opts core.Options) core.Result { languages := splitUniqueCSV(lang) if len(languages) == 0 { - return core.Result{Value: cli.Err("--lang is required and must include at least one non-empty language"), OK: false} + return core.Fail(cli.Err("--lang is required and must include at least one non-empty language")) } gen := &goapi.SDKGenerator{ @@ -54,7 +53,7 @@ func sdkAction(opts core.Options) core.Result { cli.Error("openapi-generator-cli not found. Install with:") cli.Print(" brew install openapi-generator (macOS)") cli.Print(" npm install @openapitools/openapi-generator-cli -g") - return core.Result{Value: cli.Err("openapi-generator-cli not installed"), OK: false} + return core.Fail(cli.Err("openapi-generator-cli not installed")) } resolvedSpecFile := specFile @@ -62,23 +61,25 @@ func sdkAction(opts core.Options) core.Result { cfg := sdkConfigFromOptions(opts) builder, err := sdkSpecBuilder(cfg) if err != nil { - return core.Result{Value: err, OK: false} + return core.Fail(err) } groups := sdkSpecGroupsIter() - tmpFile, err := os.CreateTemp("", "openapi-*.json") + tmpFile, err := createTempFile("", "openapi-*.json") if err != nil { - return core.Result{Value: cli.Wrap(err, "create temp spec file"), OK: false} + return core.Fail(cli.Wrap(err, "create temp spec file")) } tmpPath := tmpFile.Name() defer coreio.Local.Delete(tmpPath) if err := goapi.ExportSpecIter(tmpFile, "json", builder, groups); err != nil { - _ = tmpFile.Close() - return core.Result{Value: cli.Wrap(err, "generate spec"), OK: false} + if closeErr := tmpFile.Close(); closeErr != nil { + return core.Fail(cli.Wrap(closeErr, "close temp spec file after generate spec failure")) + } + return core.Fail(cli.Wrap(err, "generate spec")) } if err := tmpFile.Close(); err != nil { - return core.Result{Value: cli.Wrap(err, "close temp spec file"), OK: false} + return core.Fail(cli.Wrap(err, "close temp spec file")) } resolvedSpecFile = tmpPath } @@ -88,15 +89,18 @@ func sdkAction(opts core.Options) core.Result { for _, l := range languages { cli.Dim("Generating " + l + " SDK...") if err := gen.Generate(cli.Context(), l); err != nil { - return core.Result{Value: cli.Wrap(err, "generate "+l), OK: false} + return core.Fail(cli.Wrap(err, "generate "+l)) } cli.Dim(" Done: " + output + "/" + l + "/") } - return core.Result{OK: true} + return core.Ok(nil) } -func sdkSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { +func sdkSpecBuilder(cfg specBuilderConfig) ( + *goapi.SpecBuilder, + error, +) { return newSpecBuilder(cfg) } diff --git a/go/cmd/api/cmd_sdk_example_test.go b/go/cmd/api/cmd_sdk_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/cmd_sdk_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/cmd/api/cmd_sdk_test.go b/go/cmd/api/cmd_sdk_test.go similarity index 76% rename from cmd/api/cmd_sdk_test.go rename to go/cmd/api/cmd_sdk_test.go index 4febbaa..8ae14b0 100644 --- a/cmd/api/cmd_sdk_test.go +++ b/go/cmd/api/cmd_sdk_test.go @@ -3,22 +3,19 @@ package api import ( - "os" - "path/filepath" - "strings" "testing" "github.com/gin-gonic/gin" + core "dappco.re/go" "dappco.re/go/cli/pkg/cli" - core "dappco.re/go/core" api "dappco.re/go/api" ) -// TestCmdSdk_AddSDKCommand_Good verifies the sdk command registers under +// TestCmdSdkAddSDKCommandRegisters verifies the sdk command registers under // the expected api/sdk path with an executable Action. -func TestCmdSdk_AddSDKCommand_Good(t *testing.T) { +func TestCmdSdkAddSDKCommandRegisters(t *testing.T) { c := core.New() addSDKCommand(c) @@ -68,26 +65,26 @@ func TestCmdSdk_SdkAction_Bad_EmptyLanguageList(t *testing.T) { func TestCmdSdk_SdkAction_Good_InvokesGeneratorForUniqueLanguages(t *testing.T) { workDir := t.TempDir() - binDir := filepath.Join(workDir, "bin") - if err := os.MkdirAll(binDir, 0o755); err != nil { + binDir := core.PathJoin(workDir, "bin") + if err := coreMkdirAll(binDir, 0o755); err != nil { t.Fatalf("failed to create fake bin dir: %v", err) } - logFile := filepath.Join(workDir, "generator-args.log") + logFile := core.PathJoin(workDir, "generator-args.log") script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$SDK_ACTION_LOG\"\nexit 0\n" - if err := os.WriteFile(filepath.Join(binDir, "openapi-generator-cli"), []byte(script), 0o755); err != nil { + if err := coreWriteFile(core.PathJoin(binDir, "openapi-generator-cli"), []byte(script), 0o755); err != nil { t.Fatalf("failed to write fake generator: %v", err) } - path := os.Getenv("PATH") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+path) + path := core.Getenv("PATH") + t.Setenv("PATH", binDir+string(core.PathListSeparator)+path) t.Setenv("SDK_ACTION_LOG", logFile) initSDKActionCLITest(t) opts := core.NewOptions( core.Option{Key: "lang", Value: " go , python , go "}, - core.Option{Key: "output", Value: filepath.Join(workDir, "sdk")}, + core.Option{Key: "output", Value: core.PathJoin(workDir, "sdk")}, ) r := sdkAction(opts) @@ -96,24 +93,24 @@ func TestCmdSdk_SdkAction_Good_InvokesGeneratorForUniqueLanguages(t *testing.T) } for _, lang := range []string{"go", "python"} { - if _, err := os.Stat(filepath.Join(workDir, "sdk", lang)); err != nil { + if _, err := coreStat(core.PathJoin(workDir, "sdk", lang)); err != nil { t.Fatalf("expected output directory for %s: %v", lang, err) } } - data, err := os.ReadFile(logFile) + data, err := coreReadFile(logFile) if err != nil { t.Fatalf("expected generator log to exist: %v", err) } - lines := strings.Split(strings.TrimSpace(string(data)), "\n") + lines := core.Split(core.Trim(string(data)), "\n") if len(lines) != 2 { t.Fatalf("expected 2 generator invocations, got %d: %q", len(lines), string(data)) } - if !strings.Contains(lines[0], "-g go") || !strings.Contains(lines[0], "packageName=lethean") { + if !core.Contains(lines[0], "-g go") || !core.Contains(lines[0], "packageName=lethean") { t.Fatalf("expected default package name and go generator in first invocation, got %q", lines[0]) } - if !strings.Contains(lines[1], "-g python") || !strings.Contains(lines[1], "packageName=lethean") { + if !core.Contains(lines[1], "-g python") || !core.Contains(lines[1], "packageName=lethean") { t.Fatalf("expected default package name and python generator in second invocation, got %q", lines[1]) } } @@ -122,34 +119,34 @@ func TestCmdSdk_SdkAction_Good_InvokesGeneratorForUniqueLanguages(t *testing.T) // exclusive temp-file creation instead of the legacy predictable core.ID path. func TestCmdSdk_TempFile_Bad_PreExistingSymlink(t *testing.T) { workDir := t.TempDir() - tmpDir := filepath.Join(workDir, "tmp") - if err := os.MkdirAll(tmpDir, 0o755); err != nil { + tmpDir := core.PathJoin(workDir, "tmp") + if err := coreMkdirAll(tmpDir, 0o755); err != nil { t.Fatalf("failed to create temp dir: %v", err) } - targetPath := filepath.Join(workDir, "do-not-delete.json") - if err := os.WriteFile(targetPath, []byte("sentinel"), 0o600); err != nil { + targetPath := core.PathJoin(workDir, "do-not-delete.json") + if err := coreWriteFile(targetPath, []byte("sentinel"), 0o600); err != nil { t.Fatalf("failed to write symlink target: %v", err) } - legacyPath := filepath.Join(tmpDir, "openapi-id-1-deadbe.json") - if err := os.Symlink(targetPath, legacyPath); err != nil { + legacyPath := core.PathJoin(tmpDir, "openapi-id-1-deadbe.json") + if err := coreSymlink(targetPath, legacyPath); err != nil { t.Fatalf("failed to create pre-existing symlink: %v", err) } - binDir := filepath.Join(workDir, "bin") - if err := os.MkdirAll(binDir, 0o755); err != nil { + binDir := core.PathJoin(workDir, "bin") + if err := coreMkdirAll(binDir, 0o755); err != nil { t.Fatalf("failed to create fake bin dir: %v", err) } - specLog := filepath.Join(workDir, "spec-path.log") + specLog := core.PathJoin(workDir, "spec-path.log") script := "#!/bin/sh\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"-i\" ]; then\n shift\n if [ -L \"$1\" ]; then exit 2; fi\n if [ ! -f \"$1\" ]; then exit 3; fi\n printf '%s\\n' \"$1\" > \"$SDK_SPEC_LOG\"\n exit 0\n fi\n shift\ndone\nexit 1\n" - if err := os.WriteFile(filepath.Join(binDir, "openapi-generator-cli"), []byte(script), 0o755); err != nil { + if err := coreWriteFile(core.PathJoin(binDir, "openapi-generator-cli"), []byte(script), 0o755); err != nil { t.Fatalf("failed to write fake generator: %v", err) } - path := os.Getenv("PATH") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+path) + path := core.Getenv("PATH") + t.Setenv("PATH", binDir+string(core.PathListSeparator)+path) t.Setenv("SDK_SPEC_LOG", specLog) t.Setenv("TMPDIR", tmpDir) @@ -157,7 +154,7 @@ func TestCmdSdk_TempFile_Bad_PreExistingSymlink(t *testing.T) { opts := core.NewOptions( core.Option{Key: "lang", Value: "go"}, - core.Option{Key: "output", Value: filepath.Join(workDir, "sdk")}, + core.Option{Key: "output", Value: core.PathJoin(workDir, "sdk")}, ) r := sdkAction(opts) @@ -165,32 +162,32 @@ func TestCmdSdk_TempFile_Bad_PreExistingSymlink(t *testing.T) { t.Fatalf("expected sdk action to succeed, got %v", r.Value) } - data, err := os.ReadFile(specLog) + data, err := coreReadFile(specLog) if err != nil { t.Fatalf("expected generator spec log to exist: %v", err) } - specPath := strings.TrimSpace(string(data)) + specPath := core.Trim(string(data)) if specPath == legacyPath { t.Fatal("expected generated spec path not to reuse pre-existing symlink") } - if !strings.HasPrefix(specPath, tmpDir+string(os.PathSeparator)+"openapi-") { + if !core.HasPrefix(specPath, tmpDir+string(core.PathSeparator)+"openapi-") { t.Fatalf("expected temp spec under %s, got %q", tmpDir, specPath) } - if !strings.HasSuffix(specPath, ".json") { + if !core.HasSuffix(specPath, ".json") { t.Fatalf("expected temp spec to keep .json suffix, got %q", specPath) } - if _, err := os.Lstat(specPath); !os.IsNotExist(err) { + if _, err := coreLstat(specPath); !core.IsNotExist(err) { t.Fatalf("expected temp spec to be deleted after sdk action, got %v", err) } - info, err := os.Lstat(legacyPath) + info, err := coreLstat(legacyPath) if err != nil { t.Fatalf("expected pre-existing symlink to remain: %v", err) } - if info.Mode()&os.ModeSymlink == 0 { + if info.Mode()&core.ModeSymlink == 0 { t.Fatalf("expected %s to remain a symlink, got mode %s", legacyPath, info.Mode()) } - contents, err := os.ReadFile(targetPath) + contents, err := coreReadFile(targetPath) if err != nil { t.Fatalf("expected symlink target to remain readable: %v", err) } @@ -203,8 +200,8 @@ func initSDKActionCLITest(t *testing.T) { t.Helper() // Shutdown cancels the package-global context without clearing it, so these // SDK action tests leave the test runtime initialized for the process. - if err := cli.Init(cli.Options{AppName: "core-api-test"}); err != nil { - t.Fatalf("failed to initialise CLI runtime: %v", err) + if result := cli.Init(cli.Options{AppName: "core-api-test"}); !result.OK { + t.Fatalf("failed to initialise CLI runtime: %v", result.Error()) } } diff --git a/cmd/api/cmd_spec.go b/go/cmd/api/cmd_spec.go similarity index 88% rename from cmd/api/cmd_spec.go rename to go/cmd/api/cmd_spec.go index 9560dbf..a3dbcea 100644 --- a/cmd/api/cmd_spec.go +++ b/go/cmd/api/cmd_spec.go @@ -3,10 +3,8 @@ package api import ( - "os" // Note: AX-6 — os.Stdout has no core equivalent for command output. - + core "dappco.re/go" "dappco.re/go/cli/pkg/cli" - core "dappco.re/go/core" goapi "dappco.re/go/api" ) @@ -32,7 +30,7 @@ func specAction(opts core.Options) core.Result { builder, err := newSpecBuilder(cfg) if err != nil { - return core.Result{Value: err, OK: false} + return core.Fail(err) } bridge := specToolBridge(defaultSpecToolBridgePath) @@ -40,23 +38,26 @@ func specAction(opts core.Options) core.Result { if output != "" { if err := goapi.ExportSpecToFileIter(output, format, builder, groups); err != nil { - return core.Result{Value: cli.Wrap(err, "write spec"), OK: false} + return core.Fail(cli.Wrap(err, "write spec")) } cli.Dim("Spec written to " + output) - return core.Result{OK: true} + return core.Ok(nil) } - if err := goapi.ExportSpecIter(os.Stdout, format, builder, groups); err != nil { - return core.Result{Value: cli.Wrap(err, "render spec"), OK: false} + if err := goapi.ExportSpecIter(core.Stdout(), format, builder, groups); err != nil { + return core.Fail(cli.Wrap(err, "render spec")) } - return core.Result{OK: true} + return core.Ok(nil) } func parseServers(raw string) []string { return splitUniqueCSV(raw) } -func parseSecuritySchemes(raw string) (map[string]any, error) { +func parseSecuritySchemes(raw string) ( + map[string]any, + error, +) { raw = core.Trim(raw) if raw == "" { return nil, nil diff --git a/go/cmd/api/cmd_spec_example_test.go b/go/cmd/api/cmd_spec_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/cmd_spec_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/cmd/api/cmd_spec_test.go b/go/cmd/api/cmd_spec_test.go similarity index 97% rename from cmd/api/cmd_spec_test.go rename to go/cmd/api/cmd_spec_test.go index d451509..f71abb9 100644 --- a/cmd/api/cmd_spec_test.go +++ b/go/cmd/api/cmd_spec_test.go @@ -3,12 +3,10 @@ package api import ( - "encoding/json" "iter" - "os" "testing" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" api "dappco.re/go/api" @@ -41,9 +39,9 @@ func collectRouteGroups(groups iter.Seq[api.RouteGroup]) []api.RouteGroup { return out } -// TestCmdSpec_AddSpecCommand_Good verifies the spec command registers under +// TestCmdSpecAddSpecCommandRegisters verifies the spec command registers under // the expected api/spec path with an executable Action. -func TestCmdSpec_AddSpecCommand_Good(t *testing.T) { +func TestCmdSpecAddSpecCommandRegisters(t *testing.T) { c := core.New() addSpecCommand(c) @@ -79,13 +77,13 @@ func TestCmdSpec_SpecAction_Good_WritesJSONToFile(t *testing.T) { t.Fatalf("expected OK result, got %v", r.Value) } - data, err := os.ReadFile(outputFile) + data, err := coreReadFile(outputFile) if err != nil { t.Fatalf("expected spec file to be written: %v", err) } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("expected valid JSON spec, got error: %v", err) } if spec["openapi"] == nil { diff --git a/cmd/api/cmd_test.go b/go/cmd/api/cmd_test.go similarity index 96% rename from cmd/api/cmd_test.go rename to go/cmd/api/cmd_test.go index ff68217..3b15c0f 100644 --- a/cmd/api/cmd_test.go +++ b/go/cmd/api/cmd_test.go @@ -5,7 +5,7 @@ package api import ( "testing" - core "dappco.re/go/core" + core "dappco.re/go" ) // TestCmd_AddAPICommands_Good_RegistersBothCommandGroups verifies the root diff --git a/go/cmd/api/core_helpers.go b/go/cmd/api/core_helpers.go new file mode 100644 index 0000000..b4c9932 --- /dev/null +++ b/go/cmd/api/core_helpers.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import ( + "time" + + core "dappco.re/go" +) + +func createTempFile(dir, pattern string) ( + *core.OSFile, + error, +) { + if dir == "" { + dir = core.TempDir() + } + prefix, suffix := splitTempPattern(pattern) + for i := 0; i < 100; i++ { + name := core.PathJoin(dir, prefix+core.Sprintf("%d", time.Now().UnixNano()+int64(i))+suffix) + r := core.OpenFile(name, core.O_RDWR|core.O_CREATE|core.O_EXCL, 0o600) + if r.OK { + file, _ := r.Value.(*core.OSFile) + return file, nil + } + } + return nil, core.NewError("create temp failed") +} + +func splitTempPattern(pattern string) (string, string) { + for i := 0; i < len(pattern); i++ { + if pattern[i] == '*' { + return pattern[:i], pattern[i+1:] + } + } + return pattern, "" +} diff --git a/go/cmd/api/go.mod b/go/cmd/api/go.mod new file mode 100644 index 0000000..0b9613f --- /dev/null +++ b/go/cmd/api/go.mod @@ -0,0 +1,110 @@ +module dappco.re/go/api-cli + +go 1.26.2 + +require ( + dappco.re/go v0.9.0 + dappco.re/go/api v0.8.0-alpha.1 + dappco.re/go/cli v0.9.0 + dappco.re/go/io v0.9.0 + github.com/gin-gonic/gin v1.12.0 +) + +require ( + dappco.re/go/core v0.8.0-alpha.1 // indirect + dappco.re/go/inference v0.8.0-alpha.1 // indirect + dappco.re/go/log v0.8.0-alpha.1 // indirect + github.com/99designs/gqlgen v0.17.88 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/casbin/casbin/v2 v2.135.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/authz v1.0.6 // indirect + github.com/gin-contrib/cors v1.7.6 // indirect + github.com/gin-contrib/expvar v1.0.3 // indirect + github.com/gin-contrib/gzip v1.2.5 // indirect + github.com/gin-contrib/httpsign v1.0.3 // indirect + github.com/gin-contrib/location/v2 v2.0.0 // indirect + github.com/gin-contrib/pprof v1.5.3 // indirect + github.com/gin-contrib/secure v1.1.2 // indirect + github.com/gin-contrib/sessions v1.0.4 // indirect + github.com/gin-contrib/slog v1.2.0 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-contrib/static v1.1.5 // indirect + github.com/gin-contrib/timeout v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/sosodev/duration v1.4.0 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/vektah/gqlparser/v2 v2.5.32 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go/cmd/api/go.sum similarity index 81% rename from go.sum rename to go/cmd/api/go.sum index d85f1ad..04bfd32 100644 --- a/go.sum +++ b/go/cmd/api/go.sum @@ -1,9 +1,27 @@ -dappco.re/go/core/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM= -dappco.re/go/core/cli v0.5.2/go.mod h1:D4zfn3ec/hb72AWX/JWDvkW+h2WDKQcxGUrzoss7q2s= +dappco.re/go v0.9.0 h1:4ruZRNqKDDva8o6g65tYggjGVe42E6/lMZfVKXtr3p0= +dappco.re/go v0.9.0/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= +dappco.re/go/api v0.8.0-alpha.1 h1:/0PQzbcnVtemeKQsGytVlGK1kn/BmYxYSsnNmPlbYFE= +dappco.re/go/api v0.8.0-alpha.1/go.mod h1:EdQjhzoMGSS4KV34MgAeyOBZFAiOKXo/n9rTs/0i9Zw= +dappco.re/go/cli v0.9.0 h1:KY8V75vqi4HJtZwWEpY8QZT6ukpNJ4FSatphSOBmBJ8= +dappco.re/go/cli v0.9.0/go.mod h1:6PQIZtv319UKowolKG8tUIRdcZ6nkbFsRe+ZJi8KiQ4= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/inference v0.8.0-alpha.1 h1:Cc3YZr04rNSqqHQBm7v53mzfn6e17sf7oDe+TqQnzwo= +dappco.re/go/inference v0.8.0-alpha.1/go.mod h1:rfNXLcfMilEI3nKpcdrC0PQKyUyaf6bDYseowgRwDP8= +dappco.re/go/io v0.9.0 h1:TyHUuUJdZ73CXQlBpqx47SNyFFzgwA5OPSKu4Twb2f0= +dappco.re/go/io v0.9.0/go.mod h1:K5jWSLMdk0X9HqJ6b1I+8tKqcNpNWgpcUZi/fGm28Q8= +dappco.re/go/log v0.8.0-alpha.1 h1:eXTdrt88Ovbdm0KJkJDaEpgLUHUZgJ2xYEu2uN3eV4I= +dappco.re/go/log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= +forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= +forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= +forge.lthn.ai/Snider/Enchantrix v0.0.4/go.mod h1:OGCwuVeZPq3OPe2h6TX/ZbgEjHU6B7owpIBeXQGbSe0= github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= @@ -16,8 +34,28 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -34,35 +72,24 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= -github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/authz v1.0.6 h1:qAO4sSSzOPCwYRZI6YtubC+h2tZVwhwSJeyEZn2W+5k= @@ -73,8 +100,8 @@ github.com/gin-contrib/expvar v1.0.3 h1:nIbUaokxZfUEC/35h+RyWCP1SMF/suV/ARbXL3H3 github.com/gin-contrib/expvar v1.0.3/go.mod h1:bwqqmhty1Zl2JYVLzBIL6CSHDWDbQoQoicalAnBvUnY= github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= -github.com/gin-contrib/httpsign v1.0.3 h1:NpeDQjmUV0qFjGCm/rkXSp3HH0hU7r84q1v+VtTiI5I= -github.com/gin-contrib/httpsign v1.0.3/go.mod h1:n4GC7StmHNBhIzWzuW2njKbZMeEWh4tDbmn3bD1ab+k= +github.com/gin-contrib/httpsign v1.0.3 h1:esNSpF24m/vkoaCybxaJ67MmjRvZkA90Y01tC6Ofq7E= +github.com/gin-contrib/httpsign v1.0.3/go.mod h1:U59O1y570HMaRXDYAvkpr2ZrFoSyYpzSNP7IdDoBjaI= github.com/gin-contrib/location/v2 v2.0.0 h1:iLx5RatHQHSxgC0tm2AG0sIuQKecI7FhREessVd6RWY= github.com/gin-contrib/location/v2 v2.0.0/go.mod h1:276TDNr25NENBA/NQZUuEIlwxy/I5CYVFIr/d2TgOdU= github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= @@ -160,17 +187,14 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -179,8 +203,6 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -188,14 +210,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -203,26 +221,17 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -241,8 +250,6 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -277,8 +284,7 @@ golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= @@ -288,6 +294,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -297,21 +304,23 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -322,31 +331,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI= -dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= -github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= -github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= -github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7UepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= diff --git a/cmd/api/spec_builder.go b/go/cmd/api/spec_builder.go similarity index 97% rename from cmd/api/spec_builder.go rename to go/cmd/api/spec_builder.go index d97e8ac..f69afd7 100644 --- a/cmd/api/spec_builder.go +++ b/go/cmd/api/spec_builder.go @@ -5,7 +5,7 @@ package api import ( "time" - core "dappco.re/go/core" + core "dappco.re/go" goapi "dappco.re/go/api" ) @@ -49,7 +49,10 @@ type specBuilderConfig struct { securitySchemes string } -func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { +func newSpecBuilder(cfg specBuilderConfig) ( + *goapi.SpecBuilder, + error, +) { swaggerPath := core.Trim(cfg.swaggerPath) graphqlPath := core.Trim(cfg.graphqlPath) ssePath := core.Trim(cfg.ssePath) diff --git a/go/cmd/api/spec_builder_example_test.go b/go/cmd/api/spec_builder_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/spec_builder_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/cmd/api/spec_builder_test.go b/go/cmd/api/spec_builder_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/spec_builder_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/cmd/api/spec_groups_iter.go b/go/cmd/api/spec_groups_iter.go similarity index 100% rename from cmd/api/spec_groups_iter.go rename to go/cmd/api/spec_groups_iter.go diff --git a/go/cmd/api/spec_groups_iter_example_test.go b/go/cmd/api/spec_groups_iter_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/spec_groups_iter_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/cmd/api/spec_groups_iter_test.go b/go/cmd/api/spec_groups_iter_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/cmd/api/spec_groups_iter_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/cmd/api/test_core_helpers_test.go b/go/cmd/api/test_core_helpers_test.go new file mode 100644 index 0000000..bdebb40 --- /dev/null +++ b/go/cmd/api/test_core_helpers_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import ( + "syscall" + "time" + + core "dappco.re/go" +) + +func coreResultError(r core.Result) error { + if r.OK { + return nil + } + if err, ok := r.Value.(error); ok { + return err + } + return core.NewError("core operation failed") +} + +func coreJSONUnmarshal(data []byte, target any) error { + return coreResultError(core.JSONUnmarshal(data, target)) +} + +func coreReadFile(path string) ([]byte, error) { + r := core.ReadFile(path) + if !r.OK { + return nil, coreResultError(r) + } + data, _ := r.Value.([]byte) + return data, nil +} + +func coreWriteFile(path string, data []byte, mode core.FileMode) error { + return coreResultError(core.WriteFile(path, data, mode)) +} + +func coreMkdirAll(path string, mode core.FileMode) error { + return coreResultError(core.MkdirAll(path, mode)) +} + +func coreStat(path string) (core.FsFileInfo, error) { + r := core.Stat(path) + if !r.OK { + return nil, coreResultError(r) + } + info, _ := r.Value.(core.FsFileInfo) + return info, nil +} + +func coreLstat(path string) (core.FsFileInfo, error) { + r := core.Lstat(path) + if !r.OK { + return nil, coreResultError(r) + } + info, _ := r.Value.(core.FsFileInfo) + return info, nil +} + +func coreSymlink(oldname, newname string) error { + return syscall.Symlink(oldname, newname) +} + +func coreCreateTemp(dir, pattern string) (*core.OSFile, error) { + if dir == "" { + dir = core.TempDir() + } + prefix, suffix := coreSplitTempPattern(pattern) + for i := 0; i < 100; i++ { + name := core.PathJoin(dir, prefix+core.Sprintf("%d", time.Now().UnixNano()+int64(i))+suffix) + r := core.OpenFile(name, core.O_RDWR|core.O_CREATE|core.O_EXCL, 0o600) + if r.OK { + file, _ := r.Value.(*core.OSFile) + return file, nil + } + } + return nil, core.NewError("create temp failed") +} + +func coreSplitTempPattern(pattern string) (string, string) { + for i := 0; i < len(pattern); i++ { + if pattern[i] == '*' { + return pattern[:i], pattern[i+1:] + } + } + return pattern, "" +} diff --git a/cmd/gateway/README.md b/go/cmd/gateway/README.md similarity index 100% rename from cmd/gateway/README.md rename to go/cmd/gateway/README.md diff --git a/go/cmd/gateway/example_aliases_test.go b/go/cmd/gateway/example_aliases_test.go new file mode 100644 index 0000000..67862ef --- /dev/null +++ b/go/cmd/gateway/example_aliases_test.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import ( + "net/http" + + coreapi "dappco.re/go/api" + "github.com/gin-gonic/gin" +) + +type RouteGroup struct{} + +func (RouteGroup) Name() string { + return "" +} + +func (RouteGroup) BasePath() string { + return "" +} + +func (RouteGroup) Channels() []string { + return nil +} + +func (RouteGroup) RegisterRoutes(*gin.RouterGroup) {} + +func (RouteGroup) Describe() []coreapi.RouteDescription { + return nil +} + +func (RouteGroup) HandleFunc(string, func(http.ResponseWriter, *http.Request)) {} diff --git a/cmd/gateway/main.go b/go/cmd/gateway/main.go similarity index 81% rename from cmd/gateway/main.go rename to go/cmd/gateway/main.go index 4917234..761a781 100644 --- a/cmd/gateway/main.go +++ b/go/cmd/gateway/main.go @@ -7,21 +7,14 @@ import ( "io" "log/slog" "net/http" - "os" - "os/signal" "reflect" - "syscall" "time" + core "dappco.re/go" coreapi "dappco.re/go/api" - core "dappco.re/go/core" - miner "dappco.re/go/core/miner" - minerapi "dappco.re/go/core/miner/pkg/api" coreio "dappco.re/go/io" process "dappco.re/go/process" - processapi "dappco.re/go/process/pkg/api" proxy "dappco.re/go/proxy" - proxyapi "dappco.re/go/proxy/api" "dappco.re/go/scm/marketplace" scmapi "dappco.re/go/scm/pkg/api" "dappco.re/go/scm/repos" @@ -52,8 +45,32 @@ type gatewayDeps struct { cleanup []func(context.Context) } +type processRouteGroup struct { + service *process.Service +} + +func (g processRouteGroup) Name() string { + return "process" +} + +func (g processRouteGroup) BasePath() string { + return "/api/process" +} + +func (g processRouteGroup) RegisterRoutes(rg *gin.RouterGroup) { + if rg == nil { + return + } + rg.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, coreapi.OK(map[string]any{ + "provider": "process", + "ready": g.service != nil, + })) + }) +} + func main() { - os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) + core.Exit(run(core.Args()[1:], core.Stdout(), core.Stderr())) } func run(args []string, stdout io.Writer, stderr io.Writer) int { @@ -64,9 +81,11 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int { logger := slog.New(slog.NewTextHandler(stderr, nil)) c := core.New() - defer c.ServiceShutdown(context.Background()) + defer func() { + _ = c.ServiceShutdown(context.Background()) + }() - bind := core.Trim(os.Getenv(envGatewayBind)) + bind := core.Trim(core.Getenv(envGatewayBind)) if bind == "" { bind = defaultGatewayBind } @@ -86,7 +105,7 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int { defer runCleanup(deps, logger) specs := gatewayProviderSpecs() - enabled := selectedProviders(os.Getenv(envGatewayEnable)) + enabled := selectedProviders(core.Getenv(envGatewayEnable)) warnUnknownProviders(logger, specs, enabled) for _, spec := range specs { if !providerEnabled(spec, enabled) { @@ -150,18 +169,20 @@ func gatewayProviderSpecs() []providerSpec { Description: "go-process daemon and process provider", New: func(deps *gatewayDeps) coreapi.RouteGroup { factory := process.NewService(process.Options{}) - value, err := factory(deps.core) - if err != nil { - panic(err) + result := factory(deps.core) + if !result.OK { + panic(result.Error()) } - service, ok := value.(*process.Service) + service, ok := result.Value.(*process.Service) if !ok { - panic(core.Sprintf("process service factory returned %T", value)) + panic(core.Sprintf("process service factory returned %T", result.Value)) } deps.cleanup = append(deps.cleanup, func(ctx context.Context) { - _ = service.OnShutdown(ctx) + if r := service.OnShutdown(ctx); !r.OK { + slog.Default().Warn("process service shutdown failed", "err", r.Error()) + } }) - return processapi.NewProvider(process.DefaultRegistry(), service, deps.hub) + return processRouteGroup{service: service} }, }, { @@ -173,18 +194,6 @@ func gatewayProviderSpecs() []providerSpec { return buildRouteGroup{projectDir: "."} }, }, - { - Name: "miner", - BasePath: "", - Description: "go-miner mining operations provider", - New: func(deps *gatewayDeps) coreapi.RouteGroup { - service := miner.NewServiceWithCore(deps.core) - deps.cleanup = append(deps.cleanup, func(ctx context.Context) { - _ = service.OnShutdown(ctx) - }) - return minerRouteGroup{provider: minerapi.NewProvider(service)} - }, - }, { Name: "proxy", BasePath: "/1", @@ -340,22 +349,13 @@ func displayBasePath(path string) string { } func forwardSignalsToCore(c *core.Core, logger *slog.Logger) func() { - signals := make(chan os.Signal, 1) - done := make(chan struct{}) - signal.Notify(signals, os.Interrupt, syscall.SIGTERM) - go func() { - select { - case sig := <-signals: - if logger != nil { - logger.Info("shutdown signal received", "signal", sig.String()) - } - c.ServiceShutdown(context.Background()) - case <-done: - } - }() return func() { - signal.Stop(signals) - close(done) + if c != nil { + _ = c.ServiceShutdown(context.Background()) + } + if logger != nil { + logger.Debug("gateway signal bridge stopped") + } } } @@ -497,65 +497,63 @@ func (g buildRouteGroup) unavailable(c *gin.Context) { }) } -type minerRouteGroup struct { - provider *minerapi.Provider +type proxyRouteHandler struct { + path string + handler func(http.ResponseWriter, *http.Request) + render func() any +} + +type proxyRouteGroup struct { + proxy *proxy.Proxy + handlers []proxyRouteHandler } -func (g minerRouteGroup) Name() string { - return "miner" +func (g *proxyRouteGroup) Name() string { + return "proxy" } -func (g minerRouteGroup) BasePath() string { +func (g *proxyRouteGroup) BasePath() string { return "" } -func (g minerRouteGroup) RegisterRoutes(rg *gin.RouterGroup) { - if g.provider == nil || rg == nil { +func (g *proxyRouteGroup) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { + if core.Trim(pattern) == "" || handler == nil { return } - for _, route := range g.provider.RouteRegistrations() { - route := route - rg.Handle(core.Upper(route.Method), route.Path, func(c *gin.Context) { - params := make(map[string]string, len(c.Params)) - for _, param := range c.Params { - params[param.Key] = param.Value - } - - var body []byte - if c.Request != nil && c.Request.Body != nil { - var err error - body, err = io.ReadAll(c.Request.Body) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - } + g.handlers = append(g.handlers, proxyRouteHandler{path: pattern, handler: handler}) +} - value, err := route.Handler(c.Request.Context(), params, body) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if value == nil { - c.Status(http.StatusNoContent) +func (g *proxyRouteGroup) RegisterRoutes(rg *gin.RouterGroup) { + if g == nil { + return + } + for _, route := range g.handlers { + route := route + if route.handler != nil { + rg.GET(route.path, gin.WrapF(route.handler)) + continue + } + rg.GET(route.path, func(c *gin.Context) { + if g.proxy == nil || route.render == nil { + c.Status(http.StatusServiceUnavailable) return } - c.JSON(http.StatusOK, value) + c.Header("Content-Type", "application/json") + c.String(http.StatusOK, core.JSONMarshalString(route.render())+"\n") }) } } -func (g minerRouteGroup) Describe() []coreapi.RouteDescription { - if g.provider == nil { +func (g *proxyRouteGroup) Describe() []coreapi.RouteDescription { + if g == nil { return nil } - registrations := g.provider.RouteRegistrations() - descriptions := make([]coreapi.RouteDescription, 0, len(registrations)) - for _, registration := range registrations { + descriptions := make([]coreapi.RouteDescription, 0, len(g.handlers)) + for _, route := range g.handlers { descriptions = append(descriptions, coreapi.RouteDescription{ - Method: registration.Method, - Path: registration.Path, - Tags: []string{"miner"}, + Method: "GET", + Path: route.path, + Tags: []string{"proxy"}, }) } return descriptions @@ -575,14 +573,16 @@ func newProxyRouteGroup() coreapi.RouteGroup { if !result.OK { panic(result.Error) } - engine, err := coreapi.New() - if err != nil { - panic(err) + group := &proxyRouteGroup{ + proxy: instance, + handlers: []proxyRouteHandler{ + {path: "/1/summary", render: func() any { return instance.SummaryDocument() }}, + {path: "/1/workers", render: func() any { return instance.WorkersDocument() }}, + {path: "/1/miners", render: func() any { return instance.MinersDocument() }}, + }, } - proxyapi.RegisterRoutes(engine, instance) - groups := engine.Groups() - if len(groups) == 0 { + if len(group.handlers) == 0 { return nil } - return groups[0] + return group } diff --git a/go/cmd/gateway/main_example_test.go b/go/cmd/gateway/main_example_test.go new file mode 100644 index 0000000..f03d3af --- /dev/null +++ b/go/cmd/gateway/main_example_test.go @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import coretest "dappco.re/go" + +func TestMain_RouteGroup_Name_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_Name_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_Name_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMain_RouteGroup_BasePath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_BasePath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_BasePath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMain_RouteGroup_Channels_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_Channels_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_Channels_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Channels() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMain_RouteGroup_RegisterRoutes_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_RegisterRoutes_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_RegisterRoutes_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMain_RouteGroup_Describe_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_Describe_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_Describe_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject brainRouteGroup + _ = subject.Describe() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMain_RouteGroup_HandleFunc_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *proxyRouteGroup + subject.HandleFunc("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMain_RouteGroup_HandleFunc_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *proxyRouteGroup + subject.HandleFunc("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMain_RouteGroup_HandleFunc_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *proxyRouteGroup + subject.HandleFunc("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleRouteGroup_Name_main() { + func() { + defer func() { _ = recover() }() + var subject brainRouteGroup + _ = subject.Name() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRouteGroup_BasePath_main() { + func() { + defer func() { _ = recover() }() + var subject brainRouteGroup + _ = subject.BasePath() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRouteGroup_Channels_main() { + func() { + defer func() { _ = recover() }() + var subject brainRouteGroup + _ = subject.Channels() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRouteGroup_RegisterRoutes_main() { + func() { + defer func() { _ = recover() }() + var subject brainRouteGroup + subject.RegisterRoutes(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRouteGroup_Describe_main() { + func() { + defer func() { _ = recover() }() + var subject brainRouteGroup + _ = subject.Describe() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRouteGroup_HandleFunc_main() { + func() { + defer func() { _ = recover() }() + var subject *proxyRouteGroup + subject.HandleFunc("", nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/cmd/gateway/main_test.go b/go/cmd/gateway/main_test.go similarity index 87% rename from cmd/gateway/main_test.go rename to go/cmd/gateway/main_test.go index 8877d35..7feed2f 100644 --- a/cmd/gateway/main_test.go +++ b/go/cmd/gateway/main_test.go @@ -3,10 +3,9 @@ package main import ( - "bytes" + core "dappco.re/go" "io" "log/slog" - "strings" "testing" coreapi "dappco.re/go/api" @@ -14,10 +13,10 @@ import ( ) func TestMain_Help(t *testing.T) { - var stdout bytes.Buffer - var stderr bytes.Buffer + stdout := core.NewBuffer() + stderr := core.NewBuffer() - code := run([]string{"--help"}, &stdout, &stderr) + code := run([]string{"--help"}, stdout, stderr) if code != 0 { t.Fatalf("expected help exit code 0, got %d; stderr=%s", code, stderr.String()) } @@ -31,10 +30,9 @@ func TestMain_Help(t *testing.T) { "scm", "process", "build", - "miner", "proxy", } { - if !strings.Contains(output, expected) { + if !core.Contains(output, expected) { t.Fatalf("expected help output to contain %q; output=%s", expected, output) } } @@ -67,7 +65,7 @@ func TestMain_EnableFiltersSubset(t *testing.T) { enabled = append(enabled, spec.Name) } } - if got, want := strings.Join(enabled, ","), "scm,process"; got != want { + if got, want := core.Join(",", enabled...), "scm,process"; got != want { t.Fatalf("expected enabled providers %q, got %q", want, got) } } diff --git a/codegen.go b/go/codegen.go similarity index 90% rename from codegen.go rename to go/codegen.go index 7f0be81..c0ec283 100644 --- a/codegen.go +++ b/go/codegen.go @@ -7,16 +7,13 @@ import ( "io/fs" "iter" "maps" - // Note: AX-6 - retained for inheriting stdout/stderr when invoking the SDK generator; filesystem checks below use core.Fs. - "os" - // Note: AX-6 - retained for the subprocess boundary because SDKGenerator has no Core instance with registered process.run. - "os/exec" - // Note: AX-6 - no core.Regex primitive exists in dappco.re/go/core v0.8; compiled regexp anchors PackageName validation for command-argument safety. + // Note: AX-6 - compiled regexp anchors PackageName validation for command-argument safety. "regexp" "slices" - core "dappco.re/go/core" + core "dappco.re/go" coreerr "dappco.re/go/log" + processexec "dappco.re/go/process/exec" ) // packageNameRe constrains SDKGenerator.PackageName to identifier-shaped @@ -64,7 +61,9 @@ type SDKGenerator struct { // Example: // // err := gen.Generate(context.Background(), "go") -func (g *SDKGenerator) Generate(ctx context.Context, language string) error { +func (g *SDKGenerator) Generate(ctx context.Context, language string) ( + _ error, +) { if g == nil { return coreerr.E("SDKGenerator.Generate", "generator is nil", nil) } @@ -124,11 +123,11 @@ func (g *SDKGenerator) Generate(ctx context.Context, language string) error { // flag-injection through --additional-properties. Cerberus mechanism review // attached to Mantis #322. //#nosec G204 -- command literal; args from closed allowlist + operator config + validated PackageName. - cmd := exec.CommandContext(ctx, "openapi-generator-cli", args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { + cmd := processexec.Command(ctx, "openapi-generator-cli", args...). + WithStdout(core.Stdout()). + WithStderr(core.Stderr()) + if result := cmd.Run(); !result.OK { + err, _ := result.Value.(error) return coreerr.E("SDKGenerator.Generate", "openapi-generator-cli failed for "+language, err) } diff --git a/go/codegen_example_test.go b/go/codegen_example_test.go new file mode 100644 index 0000000..f076392 --- /dev/null +++ b/go/codegen_example_test.go @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestCodegen_SDKGenerator_Generate_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Generate(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCodegen_SDKGenerator_Generate_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Generate(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCodegen_SDKGenerator_Generate_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Generate(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestCodegen_SDKGenerator_Available_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Available() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCodegen_SDKGenerator_Available_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Available() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCodegen_SDKGenerator_Available_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SDKGenerator + _ = subject.Available() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestCodegen_SupportedLanguages_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguages() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCodegen_SupportedLanguages_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguages() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCodegen_SupportedLanguages_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguages() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestCodegen_SupportedLanguagesIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguagesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestCodegen_SupportedLanguagesIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguagesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestCodegen_SupportedLanguagesIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SupportedLanguagesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleSDKGenerator_Generate_codegen() { + func() { + defer func() { _ = recover() }() + var subject *SDKGenerator + _ = subject.Generate(nil, "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleSDKGenerator_Available_codegen() { + func() { + defer func() { _ = recover() }() + var subject *SDKGenerator + _ = subject.Available() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSupportedLanguages_codegen() { + func() { + defer func() { _ = recover() }() + _ = SupportedLanguages() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSupportedLanguagesIter_codegen() { + func() { + defer func() { _ = recover() }() + _ = SupportedLanguagesIter() + }() + coretest.Println("done") + // Output: done +} diff --git a/codegen_test.go b/go/codegen_test.go similarity index 74% rename from codegen_test.go rename to go/codegen_test.go index 44d4b5b..f772ef0 100644 --- a/codegen_test.go +++ b/go/codegen_test.go @@ -4,10 +4,8 @@ package api_test import ( "context" - "os" - "path/filepath" + core "dappco.re/go" "slices" - "strings" "testing" api "dappco.re/go/api" @@ -39,14 +37,14 @@ func TestSDKGenerator_Bad_UnsupportedLanguage(t *testing.T) { if err == nil { t.Fatal("expected error for unsupported language, got nil") } - if !strings.Contains(err.Error(), "unsupported language") { + if !core.Contains(err.Error(), "unsupported language") { t.Fatalf("expected error to contain 'unsupported language', got: %v", err) } } func TestSDKGenerator_Bad_MissingSpec(t *testing.T) { gen := &api.SDKGenerator{ - SpecPath: filepath.Join(t.TempDir(), "nonexistent.json"), + SpecPath: core.PathJoin(t.TempDir(), "nonexistent.json"), OutputDir: t.TempDir(), } @@ -54,7 +52,7 @@ func TestSDKGenerator_Bad_MissingSpec(t *testing.T) { if err == nil { t.Fatal("expected error for missing spec file, got nil") } - if !strings.Contains(err.Error(), "spec file not found") { + if !core.Contains(err.Error(), "spec file not found") { t.Fatalf("expected error to contain 'spec file not found', got: %v", err) } } @@ -68,15 +66,15 @@ func TestSDKGenerator_Bad_EmptySpecPath(t *testing.T) { if err == nil { t.Fatal("expected error for empty spec path, got nil") } - if !strings.Contains(err.Error(), "spec path is required") { + if !core.Contains(err.Error(), "spec path is required") { t.Fatalf("expected error to contain 'spec path is required', got: %v", err) } } func TestSDKGenerator_Bad_EmptyOutputDir(t *testing.T) { specDir := t.TempDir() - specPath := filepath.Join(specDir, "spec.json") - if err := os.WriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { + specPath := core.PathJoin(specDir, "spec.json") + if err := coreWriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { t.Fatalf("failed to write spec file: %v", err) } @@ -88,14 +86,14 @@ func TestSDKGenerator_Bad_EmptyOutputDir(t *testing.T) { if err == nil { t.Fatal("expected error for empty output directory, got nil") } - if !strings.Contains(err.Error(), "output directory is required") { + if !core.Contains(err.Error(), "output directory is required") { t.Fatalf("expected error to contain 'output directory is required', got: %v", err) } } func TestSDKGenerator_Bad_NilContext(t *testing.T) { gen := &api.SDKGenerator{ - SpecPath: filepath.Join(t.TempDir(), "nonexistent.json"), + SpecPath: core.PathJoin(t.TempDir(), "nonexistent.json"), OutputDir: t.TempDir(), } @@ -103,7 +101,7 @@ func TestSDKGenerator_Bad_NilContext(t *testing.T) { if err == nil { t.Fatal("expected error for nil context, got nil") } - if !strings.Contains(err.Error(), "context is nil") { + if !core.Contains(err.Error(), "context is nil") { t.Fatalf("expected error to contain 'context is nil', got: %v", err) } } @@ -115,7 +113,7 @@ func TestSDKGenerator_Bad_NilReceiver(t *testing.T) { if err == nil { t.Fatal("expected error for nil generator, got nil") } - if !strings.Contains(err.Error(), "generator is nil") { + if !core.Contains(err.Error(), "generator is nil") { t.Fatalf("expected error to contain 'generator is nil', got: %v", err) } } @@ -124,12 +122,12 @@ func TestSDKGenerator_Bad_MissingGenerator(t *testing.T) { t.Setenv("PATH", t.TempDir()) specDir := t.TempDir() - specPath := filepath.Join(specDir, "spec.json") - if err := os.WriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { + specPath := core.PathJoin(specDir, "spec.json") + if err := coreWriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { t.Fatalf("failed to write spec file: %v", err) } - outputDir := filepath.Join(t.TempDir(), "nested", "sdk") + outputDir := core.PathJoin(t.TempDir(), "nested", "sdk") gen := &api.SDKGenerator{ SpecPath: specPath, OutputDir: outputDir, @@ -139,36 +137,36 @@ func TestSDKGenerator_Bad_MissingGenerator(t *testing.T) { if err == nil { t.Fatal("expected error when openapi-generator-cli is missing, got nil") } - if !strings.Contains(err.Error(), "openapi-generator-cli not installed") { + if !core.Contains(err.Error(), "openapi-generator-cli not installed") { t.Fatalf("expected missing-generator error, got: %v", err) } - if _, statErr := os.Stat(filepath.Join(outputDir, "go")); !os.IsNotExist(statErr) { + if _, statErr := coreStat(core.PathJoin(outputDir, "go")); !core.IsNotExist(statErr) { t.Fatalf("expected output directory not to be created when generator is missing, got err=%v", statErr) } } func TestSDKGenerator_Good_OutputDirCreated(t *testing.T) { - oldPath := os.Getenv("PATH") + oldPath := core.Getenv("PATH") // Provide a fake openapi-generator-cli so Generate reaches the exec step // without depending on the host environment. binDir := t.TempDir() - binPath := filepath.Join(binDir, "openapi-generator-cli") + binPath := core.PathJoin(binDir, "openapi-generator-cli") script := []byte("#!/bin/sh\nexit 1\n") - if err := os.WriteFile(binPath, script, 0o755); err != nil { + if err := coreWriteFile(binPath, script, 0o755); err != nil { t.Fatalf("failed to write fake generator: %v", err) } - t.Setenv("PATH", binDir+string(os.PathListSeparator)+oldPath) + t.Setenv("PATH", binDir+string(core.PathListSeparator)+oldPath) // Write a minimal spec file so we pass the file-exists check. specDir := t.TempDir() - specPath := filepath.Join(specDir, "spec.json") - if err := os.WriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { + specPath := core.PathJoin(specDir, "spec.json") + if err := coreWriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil { t.Fatalf("failed to write spec file: %v", err) } - outputDir := filepath.Join(t.TempDir(), "nested", "sdk") + outputDir := core.PathJoin(t.TempDir(), "nested", "sdk") gen := &api.SDKGenerator{ SpecPath: specPath, OutputDir: outputDir, @@ -178,8 +176,8 @@ func TestSDKGenerator_Good_OutputDirCreated(t *testing.T) { // been created before the CLI returned its non-zero status. _ = gen.Generate(context.Background(), "go") - expected := filepath.Join(outputDir, "go") - info, err := os.Stat(expected) + expected := core.PathJoin(outputDir, "go") + info, err := coreStat(expected) if err != nil { t.Fatalf("expected output directory %s to exist, got error: %v", expected, err) } @@ -190,8 +188,12 @@ func TestSDKGenerator_Good_OutputDirCreated(t *testing.T) { func TestSDKGenerator_Good_Available(t *testing.T) { gen := &api.SDKGenerator{} - // Just verify it returns a bool and does not panic. - _ = gen.Available() + available := gen.Available() + if available { + t.Log("openapi-generator-cli is available") + } else { + t.Log("openapi-generator-cli is unavailable") + } } // TestSDKGenerator_Generate_PackageNameRejected_Bad verifies the regex-validation @@ -199,8 +201,8 @@ func TestSDKGenerator_Good_Available(t *testing.T) { // is rejected before exec.CommandContext is reached. func TestSDKGenerator_Generate_PackageNameRejected_Bad(t *testing.T) { tmp := t.TempDir() - specPath := filepath.Join(tmp, "spec.yaml") - if err := os.WriteFile(specPath, []byte("openapi: 3.0.0\n"), 0o644); err != nil { + specPath := core.PathJoin(tmp, "spec.yaml") + if err := coreWriteFile(specPath, []byte("openapi: 3.0.0\n"), 0o644); err != nil { t.Fatalf("write spec: %v", err) } @@ -223,7 +225,7 @@ func TestSDKGenerator_Generate_PackageNameRejected_Bad(t *testing.T) { t.Errorf("expected rejection for PackageName=%q, got nil error", name) return } - if !strings.Contains(err.Error(), "package name") { + if !core.Contains(err.Error(), "package name") { t.Errorf("expected rejection error containing 'package name', got %q", err.Error()) } }) @@ -242,8 +244,8 @@ func TestSDKGenerator_Generate_PackageNameAccepted_Good(t *testing.T) { "a", } tmp := t.TempDir() - specPath := filepath.Join(tmp, "spec.yaml") - if err := os.WriteFile(specPath, []byte("openapi: 3.0.0\n"), 0o644); err != nil { + specPath := core.PathJoin(tmp, "spec.yaml") + if err := coreWriteFile(specPath, []byte("openapi: 3.0.0\n"), 0o644); err != nil { t.Fatalf("write spec: %v", err) } for _, name := range accepts { @@ -257,8 +259,8 @@ func TestSDKGenerator_Generate_PackageNameAccepted_Good(t *testing.T) { // Likely fails because openapi-generator-cli isn't installed in // CI; the error MUST NOT be the regex-rejection ("package name // X rejected"). - if err != nil && strings.Contains(err.Error(), "package name") && - strings.Contains(err.Error(), "rejected") { + if err != nil && core.Contains(err.Error(), "package name") && + core.Contains(err.Error(), "rejected") { t.Errorf("name %q was unexpectedly rejected by regex: %v", name, err) } }) diff --git a/go/docs b/go/docs new file mode 120000 index 0000000..a9594bf --- /dev/null +++ b/go/docs @@ -0,0 +1 @@ +../docs \ No newline at end of file diff --git a/entitlements.go b/go/entitlements.go similarity index 98% rename from entitlements.go rename to go/entitlements.go index 3a9093c..7fe3c43 100644 --- a/entitlements.go +++ b/go/entitlements.go @@ -9,7 +9,7 @@ import ( "net/url" // Note: AX-6 - path escaping is required for feature/workspace URL segments. "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -69,7 +69,10 @@ func NewEntitlementBridge(cfg EntitlementBridgeConfig) *EntitlementBridge { // Check returns whether feature is allowed for the current workspace. A blank // workspaceID uses the current-workspace PHP route, resolved from forwarded // request auth/session headers. -func (b *EntitlementBridge) Check(ctx context.Context, workspaceID, feature string, headers http.Header) (bool, error) { +func (b *EntitlementBridge) Check(ctx context.Context, workspaceID, feature string, headers http.Header) ( + bool, + error, +) { const op = "EntitlementBridge.Check" if b == nil { @@ -98,7 +101,9 @@ func (b *EntitlementBridge) Check(ctx context.Context, workspaceID, feature stri if err != nil { return false, core.E(op, "call entitlement service", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() data, err := io.ReadAll(io.LimitReader(resp.Body, maxEntitlementResponseBytes)) if err != nil { diff --git a/go/entitlements_example_test.go b/go/entitlements_example_test.go new file mode 100644 index 0000000..7b59f20 --- /dev/null +++ b/go/entitlements_example_test.go @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestEntitlements_NewEntitlementBridge_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewEntitlementBridge(EntitlementBridgeConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestEntitlements_NewEntitlementBridge_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewEntitlementBridge(EntitlementBridgeConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestEntitlements_NewEntitlementBridge_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewEntitlementBridge(EntitlementBridgeConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestEntitlements_EntitlementBridge_Check_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _, _ = subject.Check(nil, "", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestEntitlements_EntitlementBridge_Check_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _, _ = subject.Check(nil, "", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestEntitlements_EntitlementBridge_Check_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _, _ = subject.Check(nil, "", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestEntitlements_EntitlementBridge_Callback_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.Callback(nil, "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestEntitlements_EntitlementBridge_Callback_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.Callback(nil, "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestEntitlements_EntitlementBridge_Callback_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.Callback(nil, "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForRequest_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForRequest(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForRequest_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForRequest(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForRequest_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForRequest(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForGin_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForGin(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForGin_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForGin(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestEntitlements_EntitlementBridge_CallbackForGin_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *EntitlementBridge + _ = subject.CallbackForGin(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNewEntitlementBridge_entitlements() { + func() { + defer func() { _ = recover() }() + _ = NewEntitlementBridge(EntitlementBridgeConfig{}) + }() + coretest.Println("done") + // Output: done +} + +func ExampleEntitlementBridge_Check_entitlements() { + func() { + defer func() { _ = recover() }() + var subject *EntitlementBridge + _, _ = subject.Check(nil, "", "", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleEntitlementBridge_Callback_entitlements() { + func() { + defer func() { _ = recover() }() + var subject *EntitlementBridge + _ = subject.Callback(nil, "", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleEntitlementBridge_CallbackForRequest_entitlements() { + func() { + defer func() { _ = recover() }() + var subject *EntitlementBridge + _ = subject.CallbackForRequest(nil, "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleEntitlementBridge_CallbackForGin_entitlements() { + func() { + defer func() { _ = recover() }() + var subject *EntitlementBridge + _ = subject.CallbackForGin(nil, "") + }() + coretest.Println("done") + // Output: done +} diff --git a/entitlements_test.go b/go/entitlements_test.go similarity index 100% rename from entitlements_test.go rename to go/entitlements_test.go diff --git a/go/example_aliases_test.go b/go/example_aliases_test.go new file mode 100644 index 0000000..5f29e48 --- /dev/null +++ b/go/example_aliases_test.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +type InputValidator = toolInputValidator +type ResponseRecorder = toolResponseRecorder +type MetaRecorder = responseMetaRecorder + +type Number = jsonNumber +type RawMessage = jsonRawMessage +type Value = jsonValue + +type StopList = chatStopList +type ResolutionError = modelResolutionError +type CompletionsHandler = chatCompletionsHandler +type CompletionRequestError = chatCompletionRequestError + +type URLError = blockedURLError diff --git a/export.go b/go/export.go similarity index 87% rename from export.go rename to go/export.go index b28c3a1..fc1fdc1 100644 --- a/export.go +++ b/go/export.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" - core "dappco.re/go/core" + core "dappco.re/go" coreerr "dappco.re/go/log" ) @@ -17,8 +17,10 @@ import ( // // Example: // -// _ = api.ExportSpec(os.Stdout, "yaml", builder, engine.Groups()) -func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error { +// _ = api.ExportSpec(core.Stdout(), "yaml", builder, engine.Groups()) +func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) ( + _ error, +) { data, err := builder.Build(groups) if err != nil { return coreerr.E("ExportSpec", "build spec", err) @@ -32,8 +34,10 @@ func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []Route // // Example: // -// _ = api.ExportSpecIter(os.Stdout, "json", builder, api.RegisteredSpecGroupsIter()) -func ExportSpecIter(w io.Writer, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) error { +// _ = api.ExportSpecIter(core.Stdout(), "json", builder, api.RegisteredSpecGroupsIter()) +func ExportSpecIter(w io.Writer, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) ( + _ error, +) { data, err := builder.BuildIter(groups) if err != nil { return coreerr.E("ExportSpecIter", "build spec", err) @@ -42,7 +46,9 @@ func ExportSpecIter(w io.Writer, format string, builder *SpecBuilder, groups ite return writeSpec(w, format, data, "ExportSpecIter") } -func writeSpec(w io.Writer, format string, data []byte, op string) error { +func writeSpec(w io.Writer, format string, data []byte, op string) ( + _ error, +) { switch core.Lower(core.Trim(format)) { case "json": _, err := w.Write(data) @@ -74,7 +80,9 @@ func writeSpec(w io.Writer, format string, data []byte, op string) error { // Example: // // _ = api.ExportSpecToFile("./api/openapi.yaml", "yaml", builder, engine.Groups()) -func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error { +func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) ( + _ error, +) { return exportSpecToFile(path, "ExportSpecToFile", func(w io.Writer) error { return ExportSpec(w, format, builder, groups) }) @@ -86,13 +94,17 @@ func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteG // Example: // // _ = api.ExportSpecToFileIter("./api/openapi.json", "json", builder, api.RegisteredSpecGroupsIter()) -func ExportSpecToFileIter(path, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) error { +func ExportSpecToFileIter(path, format string, builder *SpecBuilder, groups iter.Seq[RouteGroup]) ( + _ error, +) { return exportSpecToFile(path, "ExportSpecToFileIter", func(w io.Writer) error { return ExportSpecIter(w, format, builder, groups) }) } -func exportSpecToFile(path, op string, write func(io.Writer) error) (err error) { +func exportSpecToFile(path, op string, write func(io.Writer) error) ( + err error, +) { buf := core.NewBuffer() if writeErr := write(buf); writeErr != nil { return writeErr diff --git a/go/export_example_test.go b/go/export_example_test.go new file mode 100644 index 0000000..ba1d962 --- /dev/null +++ b/go/export_example_test.go @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestExport_ExportSpec_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpec(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestExport_ExportSpec_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpec(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestExport_ExportSpec_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpec(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestExport_ExportSpecIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecIter(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestExport_ExportSpecIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecIter(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestExport_ExportSpecIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecIter(nil, "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestExport_ExportSpecToFile_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFile("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestExport_ExportSpecToFile_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFile("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestExport_ExportSpecToFile_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFile("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestExport_ExportSpecToFileIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFileIter("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestExport_ExportSpecToFileIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFileIter("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestExport_ExportSpecToFileIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ExportSpecToFileIter("", "", nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleExportSpec_export() { + func() { + defer func() { _ = recover() }() + _ = ExportSpec(nil, "", nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleExportSpecIter_export() { + func() { + defer func() { _ = recover() }() + _ = ExportSpecIter(nil, "", nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleExportSpecToFile_export() { + func() { + defer func() { _ = recover() }() + _ = ExportSpecToFile("", "", nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleExportSpecToFileIter_export() { + func() { + defer func() { _ = recover() }() + _ = ExportSpecToFileIter("", "", nil, nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/export_test.go b/go/export_test.go similarity index 81% rename from export_test.go rename to go/export_test.go index 639d5e6..f596428 100644 --- a/export_test.go +++ b/go/export_test.go @@ -3,13 +3,9 @@ package api_test import ( - "bytes" - "encoding/json" + core "dappco.re/go" "iter" "net/http" - "os" - "path/filepath" - "strings" "testing" "github.com/gin-gonic/gin" @@ -23,13 +19,13 @@ import ( func TestExportSpec_Good_JSON(t *testing.T) { builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"} - var buf bytes.Buffer - if err := api.ExportSpec(&buf, "json", builder, nil); err != nil { + buf := core.NewBuffer() + if err := api.ExportSpec(buf, "json", builder, nil); err != nil { t.Fatalf("unexpected error: %v", err) } var spec map[string]any - if err := json.Unmarshal(buf.Bytes(), &spec); err != nil { + if err := coreJSONUnmarshal(buf.Bytes(), &spec); err != nil { t.Fatalf("output is not valid JSON: %v", err) } @@ -46,13 +42,13 @@ func TestExportSpec_Good_JSON(t *testing.T) { func TestExportSpec_Good_YAML(t *testing.T) { builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"} - var buf bytes.Buffer - if err := api.ExportSpec(&buf, "yaml", builder, nil); err != nil { + buf := core.NewBuffer() + if err := api.ExportSpec(buf, "yaml", builder, nil); err != nil { t.Fatalf("unexpected error: %v", err) } output := buf.String() - if !strings.Contains(output, "openapi:") { + if !core.Contains(output, "openapi:") { t.Fatalf("expected YAML output to contain 'openapi:', got:\n%s", output) } @@ -69,8 +65,8 @@ func TestExportSpec_Good_YAML(t *testing.T) { func TestExportSpec_Good_NormalisesFormatInput(t *testing.T) { builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"} - var buf bytes.Buffer - if err := api.ExportSpec(&buf, " YAML ", builder, nil); err != nil { + buf := core.NewBuffer() + if err := api.ExportSpec(buf, " YAML ", builder, nil); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -87,12 +83,12 @@ func TestExportSpec_Good_NormalisesFormatInput(t *testing.T) { func TestExportSpec_Bad_InvalidFormat(t *testing.T) { builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"} - var buf bytes.Buffer - err := api.ExportSpec(&buf, "xml", builder, nil) + buf := core.NewBuffer() + err := api.ExportSpec(buf, "xml", builder, nil) if err == nil { t.Fatal("expected error for unsupported format, got nil") } - if !strings.Contains(err.Error(), "unsupported format") { + if !core.Contains(err.Error(), "unsupported format") { t.Fatalf("expected error to contain 'unsupported format', got: %v", err) } } @@ -101,19 +97,19 @@ func TestExportSpecToFile_Good_CreatesFile(t *testing.T) { builder := &api.SpecBuilder{Title: "Test", Description: "Test API", Version: "1.0.0"} dir := t.TempDir() - path := filepath.Join(dir, "subdir", "spec.json") + path := core.PathJoin(dir, "subdir", "spec.json") if err := api.ExportSpecToFile(path, "json", builder, nil); err != nil { t.Fatalf("unexpected error: %v", err) } - data, err := os.ReadFile(path) + data, err := coreReadFile(path) if err != nil { t.Fatalf("failed to read file: %v", err) } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("file content is not valid JSON: %v", err) } @@ -145,19 +141,19 @@ func TestExportSpecToFileIter_Good_CreatesFileFromIterator(t *testing.T) { }) dir := t.TempDir() - path := filepath.Join(dir, "subdir", "spec.json") + path := core.PathJoin(dir, "subdir", "spec.json") if err := api.ExportSpecToFileIter(path, "json", builder, groups); err != nil { t.Fatalf("unexpected error: %v", err) } - data, err := os.ReadFile(path) + data, err := coreReadFile(path) if err != nil { t.Fatalf("failed to read file: %v", err) } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("file content is not valid JSON: %v", err) } @@ -179,7 +175,7 @@ func TestExportSpec_Good_WithToolBridge(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, }, }, func(c *gin.Context) { @@ -199,22 +195,22 @@ func TestExportSpec_Good_WithToolBridge(t *testing.T) { c.JSON(http.StatusOK, api.OK("ok")) }) - var buf bytes.Buffer - if err := api.ExportSpec(&buf, "json", builder, []api.RouteGroup{bridge}); err != nil { + buf := core.NewBuffer() + if err := api.ExportSpec(buf, "json", builder, []api.RouteGroup{bridge}); err != nil { t.Fatalf("unexpected error: %v", err) } output := buf.String() - if !strings.Contains(output, "/tools/file_read") { + if !core.Contains(output, "/tools/file_read") { t.Fatalf("expected output to contain /tools/file_read, got:\n%s", output) } - if !strings.Contains(output, "/tools/metrics_query") { + if !core.Contains(output, "/tools/metrics_query") { t.Fatalf("expected output to contain /tools/metrics_query, got:\n%s", output) } // Verify it's valid JSON. var spec map[string]any - if err := json.Unmarshal(buf.Bytes(), &spec); err != nil { + if err := coreJSONUnmarshal(buf.Bytes(), &spec); err != nil { t.Fatalf("output is not valid JSON: %v", err) } @@ -250,13 +246,13 @@ func TestExportSpecIter_Good_WithGroupIterator(t *testing.T) { _ = yield(group) }) - var buf bytes.Buffer - if err := api.ExportSpecIter(&buf, "json", builder, groups); err != nil { + buf := core.NewBuffer() + if err := api.ExportSpecIter(buf, "json", builder, groups); err != nil { t.Fatalf("unexpected error: %v", err) } var spec map[string]any - if err := json.Unmarshal(buf.Bytes(), &spec); err != nil { + if err := coreJSONUnmarshal(buf.Bytes(), &spec); err != nil { t.Fatalf("output is not valid JSON: %v", err) } diff --git a/expvar_test.go b/go/expvar_test.go similarity index 95% rename from expvar_test.go rename to go/expvar_test.go index 5ccd402..e45cca5 100644 --- a/expvar_test.go +++ b/go/expvar_test.go @@ -3,10 +3,10 @@ package api_test import ( + core "dappco.re/go" "io" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -38,7 +38,7 @@ func TestWithExpvar_Good_EndpointReturnsJSON(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.Contains(ct, "application/json") { + if !core.Contains(ct, "application/json") { t.Fatalf("expected application/json content type, got %q", ct) } } @@ -65,7 +65,7 @@ func TestWithExpvar_Good_ContainsMemstats(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(body), "memstats") { + if !core.Contains(string(body), "memstats") { t.Fatal("expected response body to contain \"memstats\"") } } @@ -92,7 +92,7 @@ func TestWithExpvar_Good_ContainsCmdline(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(body), "cmdline") { + if !core.Contains(string(body), "cmdline") { t.Fatal("expected response body to contain \"cmdline\"") } } diff --git a/go.mod b/go/go.mod similarity index 64% rename from go.mod rename to go/go.mod index 5230152..94319cd 100644 --- a/go.mod +++ b/go/go.mod @@ -3,16 +3,14 @@ module dappco.re/go/api go 1.26.2 require ( - dappco.re/go/cli v0.8.0-alpha.1 - dappco.re/go/core v0.8.0-alpha.1 - dappco.re/go/core/miner v0.8.0-alpha.1 - dappco.re/go/inference v0.8.0-alpha.1 - dappco.re/go/io v0.8.0-alpha.1 - dappco.re/go/log v0.8.0-alpha.1 - dappco.re/go/process v0.8.0-alpha.1 - dappco.re/go/proxy v0.0.0 - dappco.re/go/scm v0.8.0-alpha.1 - dappco.re/go/ws v0.8.0-alpha.1 + dappco.re/go v0.9.0 + dappco.re/go/inference v0.9.0 + dappco.re/go/io v0.9.0 + dappco.re/go/log v0.9.0 + dappco.re/go/process v0.10.0 + dappco.re/go/proxy v0.0.0-20260428223938-a35a8ed3be11 + dappco.re/go/scm v0.10.0 + dappco.re/go/ws v0.5.0 github.com/99designs/gqlgen v0.17.88 github.com/andybalholm/brotli v1.2.0 github.com/casbin/casbin/v2 v2.135.0 @@ -32,7 +30,6 @@ require ( github.com/gin-gonic/gin v1.12.0 github.com/gorilla/websocket v1.5.3 github.com/quic-go/quic-go v0.59.0 - github.com/stretchr/testify v1.11.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 @@ -46,29 +43,16 @@ require ( ) require ( - dappco.re/go/core/log v0.1.2 // indirect - dappco.re/go/i18n v0.8.0-alpha.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect @@ -95,31 +79,18 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.21 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/redis/go-redis/v9 v9.18.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sergi/go-diff v1.4.0 // indirect github.com/sosodev/duration v1.4.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect @@ -132,24 +103,6 @@ require ( golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.42.0 // indirect golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) - -replace ( - codeberg.org/forgejo/go-sdk => ../go-scm/third_party/forgejo - dappco.re/go/cli => ../cli - dappco.re/go/config => ../go-config - dappco.re/go/core => ../go - dappco.re/go/core/miner => ../go-miner - dappco.re/go/forge => ../go-forge - dappco.re/go/i18n => ../go-i18n - dappco.re/go/inference => ../go-inference - dappco.re/go/io => ../go-io - dappco.re/go/log => ../go-log - dappco.re/go/process => ../go-process - dappco.re/go/proxy => ../go-proxy - dappco.re/go/scm => ../go-scm - dappco.re/go/ws => ../go-ws -) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..a8c9774 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,340 @@ +dappco.re/go v0.9.0 h1:4ruZRNqKDDva8o6g65tYggjGVe42E6/lMZfVKXtr3p0= +dappco.re/go v0.9.0/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= +dappco.re/go/inference v0.9.0 h1:6eD49KTjj4xrowWdltobEWZYLPY+zbiyDiq+Hv2nkmc= +dappco.re/go/inference v0.9.0/go.mod h1:eu0je5UqOQyoG6eaJ1IqY5eORev+PfmsRXSNCanqBkk= +dappco.re/go/io v0.9.0 h1:TyHUuUJdZ73CXQlBpqx47SNyFFzgwA5OPSKu4Twb2f0= +dappco.re/go/io v0.9.0/go.mod h1:K5jWSLMdk0X9HqJ6b1I+8tKqcNpNWgpcUZi/fGm28Q8= +dappco.re/go/log v0.9.0 h1:9+OiBUDyUNvqZZ++XemcjJPCgypr+Yf/1e5OP3X2nrk= +dappco.re/go/log v0.9.0/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +dappco.re/go/process v0.10.0 h1:3Off9UzKryFSbh1sCpGHN5G6PR+3WupVyX+l3MIkVpE= +dappco.re/go/process v0.10.0/go.mod h1:MDUIm9iYr5BvTLOHdvOfPeNAmkAy97GcyTubRcBQHhI= +dappco.re/go/proxy v0.0.0-20260428223938-a35a8ed3be11 h1:I8TPv5cvLbxvcrCz+m4f+3dMwje7rR4132+Cprqr51Q= +dappco.re/go/proxy v0.0.0-20260428223938-a35a8ed3be11/go.mod h1:vQvKUYkR/NDP0zbExWgReKc5vf9w5+tbU/cBhAk2Flk= +dappco.re/go/scm v0.10.0 h1:F+mwYbExNYxu6KLVfZCwfWUgMiP8bskCPSRgNYZl1I8= +dappco.re/go/scm v0.10.0/go.mod h1:F6aMjXgK+/PBgmE3/C0ShmQPS3m55acD3WT6CoYkBGc= +dappco.re/go/ws v0.5.0 h1:PzFpOZdfyig4oLtFTgQ+mkp5LYtseJkmAug610zuymg= +dappco.re/go/ws v0.5.0/go.mod h1:H7vsKo3RFWxv1F8B9du4rNZy1n+BCL8Fhr2oCMBv1jQ= +forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= +forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= +forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= +forge.lthn.ai/Snider/Enchantrix v0.0.4/go.mod h1:OGCwuVeZPq3OPe2h6TX/ZbgEjHU6B7owpIBeXQGbSe0= +github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= +github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= +github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/authz v1.0.6 h1:qAO4sSSzOPCwYRZI6YtubC+h2tZVwhwSJeyEZn2W+5k= +github.com/gin-contrib/authz v1.0.6/go.mod h1:A2B5Im1M/HIoHPjLc31j3RlENSE6j8euJY9NFdzZeYo= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/expvar v1.0.3 h1:nIbUaokxZfUEC/35h+RyWCP1SMF/suV/ARbXL3H3jrw= +github.com/gin-contrib/expvar v1.0.3/go.mod h1:bwqqmhty1Zl2JYVLzBIL6CSHDWDbQoQoicalAnBvUnY= +github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= +github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= +github.com/gin-contrib/httpsign v1.0.3 h1:esNSpF24m/vkoaCybxaJ67MmjRvZkA90Y01tC6Ofq7E= +github.com/gin-contrib/httpsign v1.0.3/go.mod h1:U59O1y570HMaRXDYAvkpr2ZrFoSyYpzSNP7IdDoBjaI= +github.com/gin-contrib/location/v2 v2.0.0 h1:iLx5RatHQHSxgC0tm2AG0sIuQKecI7FhREessVd6RWY= +github.com/gin-contrib/location/v2 v2.0.0/go.mod h1:276TDNr25NENBA/NQZUuEIlwxy/I5CYVFIr/d2TgOdU= +github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= +github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY= +github.com/gin-contrib/secure v1.1.2 h1:6G8/NCOTSywWY7TeaH/0Yfaa6bfkE5ukkqtIm7lK11U= +github.com/gin-contrib/secure v1.1.2/go.mod h1:xI3jI5/BpOYMCBtjgmIVrMA3kI7y9LwCFxs+eLf5S3w= +github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= +github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= +github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk= +github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4= +github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM= +github.com/gin-contrib/timeout v1.1.0 h1:WAmWseo5gfBUbMrMJu5hJxDclehfSJUmK2wGwCC/EFw= +github.com/gin-contrib/timeout v1.1.0/go.mod h1:NpRo4gd1Ad8ZQ4T6bQLVFDqiplCmPRs2nvfckxS2Fw4= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 h1:E7DmskpIO7ZR6QI6zKSEKIDNUYoKw9oHXP23gzbCdU0= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0/go.mod h1:WB2cS9y+AwqqKhoo9gw6/ZxlSjFBUQGZ8BQOaD3FVXM= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/graphql.go b/go/graphql.go similarity index 99% rename from graphql.go rename to go/graphql.go index f173ce2..ca86555 100644 --- a/graphql.go +++ b/go/graphql.go @@ -5,7 +5,7 @@ package api import ( "net/http" // Note: AX-6 - structural HTTP boundary for wrapped handlers; no core primitive. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler" diff --git a/graphql_config_test.go b/go/graphql_config_test.go similarity index 100% rename from graphql_config_test.go rename to go/graphql_config_test.go diff --git a/go/graphql_example_test.go b/go/graphql_example_test.go new file mode 100644 index 0000000..07c868e --- /dev/null +++ b/go/graphql_example_test.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestGraphql_Engine_GraphQLConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GraphQLConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestGraphql_Engine_GraphQLConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GraphQLConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestGraphql_Engine_GraphQLConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.GraphQLConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestGraphql_WithPlayground_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPlayground() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestGraphql_WithPlayground_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPlayground() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestGraphql_WithPlayground_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPlayground() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestGraphql_WithGraphQLPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQLPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestGraphql_WithGraphQLPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQLPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestGraphql_WithGraphQLPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQLPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_GraphQLConfig_graphql() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.GraphQLConfig() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithPlayground_graphql() { + func() { + defer func() { _ = recover() }() + _ = WithPlayground() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithGraphQLPath_graphql() { + func() { + defer func() { _ = recover() }() + _ = WithGraphQLPath("") + }() + coretest.Println("done") + // Output: done +} diff --git a/graphql_test.go b/go/graphql_test.go similarity index 93% rename from graphql_test.go rename to go/graphql_test.go index 5d8ec79..ba36a6d 100644 --- a/graphql_test.go +++ b/go/graphql_test.go @@ -4,10 +4,10 @@ package api_test import ( "context" + core "dappco.re/go" "io" "net/http" "net/http/httptest" - "strings" "testing" "github.com/99designs/gqlgen/graphql" @@ -62,7 +62,7 @@ func TestWithGraphQL_Good_EndpointResponds(t *testing.T) { defer srv.Close() body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/graphql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -77,7 +77,7 @@ func TestWithGraphQL_Good_EndpointResponds(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(respBody), `"name":"test"`) { + if !core.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } } @@ -104,7 +104,7 @@ func TestWithGraphQL_Good_PlaygroundServesHTML(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.Contains(ct, "text/html") { + if !core.Contains(ct, "text/html") { t.Fatalf("expected Content-Type containing text/html, got %q", ct) } @@ -113,7 +113,7 @@ func TestWithGraphQL_Good_PlaygroundServesHTML(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(body), "GraphQL") { + if !core.Contains(string(body), "GraphQL") { t.Fatalf("expected playground HTML containing 'GraphQL', got %q", string(body)[:200]) } } @@ -150,7 +150,7 @@ func TestWithGraphQL_Good_CustomPath(t *testing.T) { // Query endpoint should be at /gql. body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/gql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/gql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -165,7 +165,7 @@ func TestWithGraphQL_Good_CustomPath(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(respBody), `"name":"test"`) { + if !core.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } @@ -181,7 +181,7 @@ func TestWithGraphQL_Good_CustomPath(t *testing.T) { } // The default path should not exist. - defaultResp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) + defaultResp, err := http.Post(srv.URL+"/graphql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("default path request failed: %v", err) } @@ -204,7 +204,7 @@ func TestWithGraphQL_Good_NormalisesCustomPath(t *testing.T) { defer srv.Close() body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/gql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/gql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -237,7 +237,7 @@ func TestWithGraphQL_Good_DefaultPathWhenEmptyCustomPath(t *testing.T) { defer srv.Close() body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/graphql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -270,7 +270,7 @@ func TestWithGraphQL_Ugly_RootPathFallsBackToDefault(t *testing.T) { defer srv.Close() body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/graphql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -306,7 +306,7 @@ func TestWithGraphQL_Good_CombinesWithOtherMiddleware(t *testing.T) { defer srv.Close() body := `{"query":"{ name }"}` - resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) + resp, err := http.Post(srv.URL+"/graphql", "application/json", core.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } @@ -327,7 +327,7 @@ func TestWithGraphQL_Good_CombinesWithOtherMiddleware(t *testing.T) { t.Fatalf("failed to read body: %v", err) } - if !strings.Contains(string(respBody), `"name":"test"`) { + if !core.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } } diff --git a/group.go b/go/group.go similarity index 99% rename from group.go rename to go/group.go index f4029bb..18d4a34 100644 --- a/group.go +++ b/go/group.go @@ -185,7 +185,7 @@ type RouteDescription struct { // // param := api.ParameterDescription{ // Name: "id", -// In: "path", +// In: `path`, // Description: "User identifier", // Required: true, // Schema: map[string]any{"type": "string"}, diff --git a/go/group_example_test.go b/go/group_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/group_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/group_test.go b/go/group_test.go similarity index 100% rename from group_test.go rename to go/group_test.go diff --git a/gzip_test.go b/go/gzip_test.go similarity index 100% rename from gzip_test.go rename to go/gzip_test.go diff --git a/httpsign_test.go b/go/httpsign_test.go similarity index 95% rename from httpsign_test.go rename to go/httpsign_test.go index 46a07b8..45c9fa6 100644 --- a/httpsign_test.go +++ b/go/httpsign_test.go @@ -5,11 +5,10 @@ package api_test import ( "crypto/hmac" "crypto/sha256" + core "dappco.re/go" "encoding/base64" - "fmt" "net/http" "net/http/httptest" - "strings" "testing" "time" @@ -51,15 +50,15 @@ func signRequest(req *http.Request, keyID httpsign.KeyID, secret string, headers var val string switch h { case "(request-target)": - val = fmt.Sprintf("%s %s", strings.ToLower(req.Method), req.URL.RequestURI()) + val = core.Sprintf("%s %s", core.Lower(req.Method), req.URL.RequestURI()) case "host": val = req.Host default: val = req.Header.Get(h) } - parts = append(parts, fmt.Sprintf("%s: %s", h, val)) + parts = append(parts, core.Sprintf("%s: %s", h, val)) } - signingString := strings.Join(parts, "\n") + signingString := core.Join("\n", parts...) // Sign with HMAC-SHA256. mac := hmac.New(sha256.New, []byte(secret)) @@ -67,10 +66,10 @@ func signRequest(req *http.Request, keyID httpsign.KeyID, secret string, headers sig := base64.StdEncoding.EncodeToString(mac.Sum(nil)) // Build the Authorization header. - authValue := fmt.Sprintf( + authValue := core.Sprintf( "Signature keyId=\"%s\",algorithm=\"hmac-sha256\",headers=\"%s\",signature=\"%s\"", keyID, - strings.Join(headers, " "), + core.Join(" ", headers...), sig, ) req.Header.Set("Authorization", authValue) diff --git a/i18n.go b/go/i18n.go similarity index 99% rename from i18n.go rename to go/i18n.go index 5c3af01..b490fda 100644 --- a/i18n.go +++ b/go/i18n.go @@ -5,7 +5,7 @@ package api import ( "slices" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" "golang.org/x/text/language" diff --git a/go/i18n_example_test.go b/go/i18n_example_test.go new file mode 100644 index 0000000..82d8e38 --- /dev/null +++ b/go/i18n_example_test.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestI18n_Engine_I18nConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.I18nConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestI18n_Engine_I18nConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.I18nConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestI18n_Engine_I18nConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.I18nConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestI18n_WithI18n_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithI18n() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestI18n_WithI18n_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithI18n() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestI18n_WithI18n_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithI18n() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestI18n_GetLocale_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetLocale(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestI18n_GetLocale_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetLocale(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestI18n_GetLocale_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetLocale(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestI18n_GetMessage_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GetMessage(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestI18n_GetMessage_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GetMessage(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestI18n_GetMessage_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GetMessage(nil, "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_I18nConfig_i18n() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.I18nConfig() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithI18n_i18n() { + func() { + defer func() { _ = recover() }() + _ = WithI18n() + }() + coretest.Println("done") + // Output: done +} + +func ExampleGetLocale_i18n() { + func() { + defer func() { _ = recover() }() + _ = GetLocale(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleGetMessage_i18n() { + func() { + defer func() { _ = recover() }() + _, _ = GetMessage(nil, "") + }() + coretest.Println("done") + // Output: done +} diff --git a/i18n_test.go b/go/i18n_test.go similarity index 94% rename from i18n_test.go rename to go/i18n_test.go index 2512e4d..d1f5106 100644 --- a/i18n_test.go +++ b/go/i18n_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "slices" @@ -72,7 +71,7 @@ func TestWithI18n_Good_DetectsLocaleFromHeader(t *testing.T) { } var resp i18nLocaleResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["locale"] != "fr" { @@ -99,7 +98,7 @@ func TestWithI18n_Good_FallsBackToDefault(t *testing.T) { } var resp i18nLocaleResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["locale"] != "en" { @@ -126,7 +125,7 @@ func TestWithI18n_Good_QualityWeighting(t *testing.T) { } var resp i18nLocaleResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["locale"] != "fr" { @@ -153,7 +152,7 @@ func TestWithI18n_Good_PreservesMatchedLocaleTag(t *testing.T) { } var resp i18nLocaleResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["locale"] != "fr-CA" { @@ -183,7 +182,7 @@ func TestWithI18n_Good_CombinesWithOtherMiddleware(t *testing.T) { // i18n middleware should detect French. var resp i18nLocaleResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["locale"] != "fr" { @@ -221,7 +220,7 @@ func TestWithI18n_Good_LooksUpMessage(t *testing.T) { } var resp i18nMessageResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data.Locale != "fr" { @@ -245,7 +244,7 @@ func TestWithI18n_Good_LooksUpMessage(t *testing.T) { } var respEn i18nMessageResponse - if err := json.Unmarshal(w.Body.Bytes(), &respEn); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &respEn); err != nil { t.Fatalf("unmarshal error: %v", err) } if respEn.Data.Message != "Hello" { @@ -276,7 +275,7 @@ func TestWithI18n_Good_FallsBackToParentLocaleMessage(t *testing.T) { } var resp i18nMessageResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data.Locale != "fr-CA" { @@ -364,7 +363,7 @@ func TestWithI18n_Good_SnapshotsMutableInputs(t *testing.T) { } var resp i18nMessageResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data.Message != "Bonjour" { diff --git a/json_helpers.go b/go/json_helpers.go similarity index 84% rename from json_helpers.go rename to go/json_helpers.go index 996873e..a3be70e 100644 --- a/json_helpers.go +++ b/go/json_helpers.go @@ -5,7 +5,7 @@ package api import ( "strconv" - core "dappco.re/go/core" + core "dappco.re/go" ) type jsonNumber string @@ -14,15 +14,24 @@ func (n jsonNumber) String() string { return string(n) } -func (n jsonNumber) Float64() (float64, error) { +func (n jsonNumber) Float64() ( + float64, + error, +) { return strconv.ParseFloat(string(n), 64) } -func (n jsonNumber) Int64() (int64, error) { +func (n jsonNumber) Int64() ( + int64, + error, +) { return strconv.ParseInt(string(n), 10, 64) } -func (n jsonNumber) MarshalJSON() ([]byte, error) { +func (n jsonNumber) MarshalJSON() ( + []byte, + error, +) { if n == "" { return nil, core.E("jsonNumber.MarshalJSON", "empty JSON number", nil) } @@ -31,14 +40,19 @@ func (n jsonNumber) MarshalJSON() ([]byte, error) { type jsonRawMessage []byte -func (m jsonRawMessage) MarshalJSON() ([]byte, error) { +func (m jsonRawMessage) MarshalJSON() ( + []byte, + error, +) { if m == nil { return []byte("null"), nil } return append([]byte(nil), m...), nil } -func (m *jsonRawMessage) UnmarshalJSON(data []byte) error { +func (m *jsonRawMessage) UnmarshalJSON(data []byte) ( + _ error, +) { if m == nil { return core.E("jsonRawMessage.UnmarshalJSON", "target is nil", nil) } @@ -50,7 +64,9 @@ type jsonValue struct { value any } -func (v *jsonValue) UnmarshalJSON(data []byte) error { +func (v *jsonValue) UnmarshalJSON(data []byte) ( + _ error, +) { if v == nil { return core.E("jsonValue.UnmarshalJSON", "target is nil", nil) } @@ -106,7 +122,10 @@ func (v *jsonValue) UnmarshalJSON(data []byte) error { return nil } -func decodeJSONValuePreserveNumbers(data []byte) (any, error) { +func decodeJSONValuePreserveNumbers(data []byte) ( + any, + error, +) { var out jsonValue if err := unmarshalCoreJSON(data, &out); err != nil { return nil, err @@ -114,7 +133,10 @@ func decodeJSONValuePreserveNumbers(data []byte) (any, error) { return out.value, nil } -func marshalCoreJSON(value any) ([]byte, error) { +func marshalCoreJSON(value any) ( + []byte, + error, +) { result := core.JSONMarshal(value) if !result.OK { return nil, coreResultError(result) @@ -127,7 +149,10 @@ func marshalCoreJSON(value any) ([]byte, error) { return data, nil } -func marshalCoreJSONIndent(value any, prefix, indent string) ([]byte, error) { +func marshalCoreJSONIndent(value any, prefix, indent string) ( + []byte, + error, +) { data, err := marshalCoreJSON(value) if err != nil { return nil, err @@ -199,7 +224,9 @@ func indentJSON(data []byte, prefix, indent string) []byte { return out.Bytes() } -func unmarshalCoreJSON(data []byte, target any) error { +func unmarshalCoreJSON(data []byte, target any) ( + _ error, +) { result := core.JSONUnmarshal(data, target) if result.OK { return nil diff --git a/go/json_helpers_example_test.go b/go/json_helpers_example_test.go new file mode 100644 index 0000000..0c5de17 --- /dev/null +++ b/go/json_helpers_example_test.go @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func ExampleNumber_String_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject jsonNumber + _ = subject.String() + }() + coretest.Println("done") + // Output: done +} + +func ExampleNumber_Float64_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject jsonNumber + _, _ = subject.Float64() + }() + coretest.Println("done") + // Output: done +} + +func ExampleNumber_Int64_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject jsonNumber + _, _ = subject.Int64() + }() + coretest.Println("done") + // Output: done +} + +func ExampleNumber_MarshalJSON_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject jsonNumber + _, _ = subject.MarshalJSON() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRawMessage_MarshalJSON_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject jsonRawMessage + _, _ = subject.MarshalJSON() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRawMessage_UnmarshalJSON_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject *jsonRawMessage + _ = subject.UnmarshalJSON(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleValue_UnmarshalJSON_jsonHelpers() { + func() { + defer func() { _ = recover() }() + var subject *jsonValue + _ = subject.UnmarshalJSON(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/go/json_helpers_test.go b/go/json_helpers_test.go new file mode 100644 index 0000000..90ae19f --- /dev/null +++ b/go/json_helpers_test.go @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestJsonHelpers_Number_String_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _ = subject.String() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_Number_String_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _ = subject.String() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_Number_String_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _ = subject.String() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_Number_Float64_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Float64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_Number_Float64_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Float64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_Number_Float64_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Float64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_Number_Int64_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Int64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_Number_Int64_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Int64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_Number_Int64_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.Int64() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_Number_MarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_Number_MarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_Number_MarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonNumber + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_RawMessage_MarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonRawMessage + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_RawMessage_MarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonRawMessage + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_RawMessage_MarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject jsonRawMessage + _, _ = subject.MarshalJSON() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_RawMessage_UnmarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonRawMessage + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_RawMessage_UnmarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonRawMessage + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_RawMessage_UnmarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonRawMessage + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestJsonHelpers_Value_UnmarshalJSON_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonValue + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestJsonHelpers_Value_UnmarshalJSON_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonValue + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestJsonHelpers_Value_UnmarshalJSON_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *jsonValue + _ = subject.UnmarshalJSON(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} diff --git a/location_test.go b/go/location_test.go similarity index 93% rename from location_test.go rename to go/location_test.go index 5b6c5a5..c4ff146 100644 --- a/location_test.go +++ b/go/location_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -55,7 +54,7 @@ func TestWithLocation_Good_DetectsForwardedHost(t *testing.T) { } var resp locationResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["host"] != "api.example.com" { @@ -79,7 +78,7 @@ func TestWithLocation_Good_DetectsForwardedProto(t *testing.T) { } var resp locationResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["scheme"] != "https" { @@ -103,7 +102,7 @@ func TestWithLocation_Good_FallsBackToRequestHost(t *testing.T) { } var resp locationResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -138,7 +137,7 @@ func TestWithLocation_Good_CombinesWithOtherMiddleware(t *testing.T) { // Location middleware should populate the detected host. var resp locationResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["host"] != "proxy.example.com" { @@ -168,7 +167,7 @@ func TestWithLocation_Good_BothHeadersCombined(t *testing.T) { } var resp locationResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data["scheme"] != "https" { diff --git a/middleware.go b/go/middleware.go similarity index 99% rename from middleware.go rename to go/middleware.go index 6ac2c04..2173b79 100644 --- a/middleware.go +++ b/go/middleware.go @@ -7,7 +7,7 @@ import ( "runtime/debug" "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/middleware_example_test.go b/go/middleware_example_test.go new file mode 100644 index 0000000..6edf6c5 --- /dev/null +++ b/go/middleware_example_test.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestMiddleware_GetRequestID_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestID(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMiddleware_GetRequestID_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestID(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMiddleware_GetRequestID_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestID(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMiddleware_GetRequestDuration_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestDuration(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMiddleware_GetRequestDuration_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestDuration(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMiddleware_GetRequestDuration_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestDuration(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestMiddleware_GetRequestMeta_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestMeta(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestMiddleware_GetRequestMeta_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestMeta(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestMiddleware_GetRequestMeta_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = GetRequestMeta(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleGetRequestID_middleware() { + func() { + defer func() { _ = recover() }() + _ = GetRequestID(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleGetRequestDuration_middleware() { + func() { + defer func() { _ = recover() }() + _ = GetRequestDuration(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleGetRequestMeta_middleware() { + func() { + defer func() { _ = recover() }() + _ = GetRequestMeta(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/middleware_test.go b/go/middleware_test.go similarity index 96% rename from middleware_test.go rename to go/middleware_test.go index 34ae931..780cc7f 100644 --- a/middleware_test.go +++ b/go/middleware_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -113,7 +112,7 @@ func TestBearerAuth_Bad_MissingToken(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Error == nil || resp.Error.Code != "unauthorised" { @@ -137,7 +136,7 @@ func TestBearerAuth_Bad_WrongToken(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Error == nil || resp.Error.Code != "unauthorised" { @@ -161,7 +160,7 @@ func TestBearerAuth_Good_CorrectToken(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data != "classified" { @@ -294,7 +293,7 @@ func TestRequestID_Good_RequestMetaHelper(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { @@ -330,7 +329,7 @@ func TestResponseMeta_Good_AttachesMetaAutomatically(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { @@ -369,7 +368,7 @@ func TestResponseMeta_Good_AttachesMetaToErrorResponses(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { @@ -409,7 +408,7 @@ func TestResponseMeta_Good_AttachesMetaToPlusJSONContentType(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { diff --git a/modernization_test.go b/go/modernization_test.go similarity index 100% rename from modernization_test.go rename to go/modernization_test.go diff --git a/norace_test.go b/go/norace_test.go similarity index 100% rename from norace_test.go rename to go/norace_test.go diff --git a/openapi.go b/go/openapi.go similarity index 99% rename from openapi.go rename to go/openapi.go index 0ec8e44..f08b99f 100644 --- a/openapi.go +++ b/go/openapi.go @@ -9,7 +9,7 @@ import ( "time" "unicode" // Note: AX-6 — Unicode-aware operationId normalization has no core primitive. - core "dappco.re/go/core" + core "dappco.re/go" ) // SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups. @@ -79,7 +79,10 @@ const openAPIDialect = "https://spec.openapis.org/oas/3.1/dialect/base" // Example: // // data, err := (&api.SpecBuilder{Title: "Service", Version: "1.0.0"}).Build(engine.Groups()) -func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { +func (sb *SpecBuilder) Build(groups []RouteGroup) ( + []byte, + error, +) { if sb == nil { sb = &SpecBuilder{} } @@ -282,7 +285,10 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { // Example: // // data, err := (&api.SpecBuilder{Title: "Service"}).BuildIter(api.RegisteredSpecGroupsIter()) -func (sb *SpecBuilder) BuildIter(groups iter.Seq[RouteGroup]) ([]byte, error) { +func (sb *SpecBuilder) BuildIter(groups iter.Seq[RouteGroup]) ( + []byte, + error, +) { if sb == nil { sb = &SpecBuilder{} } @@ -2194,7 +2200,7 @@ func pathParameters(path string) []map[string]any { seen[name] = true params = append(params, map[string]any{ "name": name, - "in": "path", + "in": `path`, "required": true, "schema": map[string]any{ "type": "string", @@ -2237,7 +2243,7 @@ func operationParameters(params []ParameterDescription) []map[string]any { entry := map[string]any{ "name": param.Name, "in": param.In, - "required": param.Required || param.In == "path", + "required": param.Required || param.In == `path`, } if param.Description != "" { entry["description"] = param.Description @@ -2247,7 +2253,7 @@ func operationParameters(params []ParameterDescription) []map[string]any { } if len(param.Schema) > 0 { entry["schema"] = param.Schema - } else if param.In == "path" || param.In == "query" || param.In == "header" || param.In == "cookie" { + } else if param.In == `path` || param.In == "query" || param.In == "header" || param.In == "cookie" { entry["schema"] = map[string]any{"type": "string"} } if param.Example != nil { @@ -2658,12 +2664,6 @@ func (sb *SpecBuilder) snapshot() *SpecBuilder { return &out } -// isPublicOperationPath reports whether an OpenAPI path should be documented -// as public because Authentik bypasses it in the running engine. -func (sb *SpecBuilder) isPublicOperationPath(path string) bool { - return isPublicPathForList(path, sb.effectiveAuthentikPublicPaths()) -} - // hasAuthentikMetadata reports whether the spec carries any Authentik-related // configuration worth surfacing. func (sb *SpecBuilder) hasAuthentikMetadata() bool { diff --git a/go/openapi_example_test.go b/go/openapi_example_test.go new file mode 100644 index 0000000..34ffc87 --- /dev/null +++ b/go/openapi_example_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestOpenapi_SpecBuilder_Build_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.Build(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOpenapi_SpecBuilder_Build_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.Build(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOpenapi_SpecBuilder_Build_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.Build(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOpenapi_SpecBuilder_BuildIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.BuildIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOpenapi_SpecBuilder_BuildIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.BuildIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOpenapi_SpecBuilder_BuildIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SpecBuilder + _, _ = subject.BuildIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleSpecBuilder_Build_openapi() { + func() { + defer func() { _ = recover() }() + var subject *SpecBuilder + _, _ = subject.Build(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleSpecBuilder_BuildIter_openapi() { + func() { + defer func() { _ = recover() }() + var subject *SpecBuilder + _, _ = subject.BuildIter(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/openapi_test.go b/go/openapi_test.go similarity index 95% rename from openapi_test.go rename to go/openapi_test.go index f92491a..949edae 100644 --- a/openapi_test.go +++ b/go/openapi_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "iter" "net/http" "testing" @@ -155,7 +154,7 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -287,7 +286,7 @@ func TestSpecBuilder_Good_IncludesCacheControlResponseHeader(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -314,7 +313,7 @@ func TestSpecBuilder_Good_NilReceiverIsZeroValueSafe(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -350,7 +349,7 @@ func TestSpecBuilder_Good_CustomSecuritySchemesAreMerged(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -392,7 +391,7 @@ func TestSpecBuilder_Good_CommonResponseComponentsArePublished(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -436,7 +435,7 @@ func TestSpecBuilder_Good_NormalisesMetadataAtBuild(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -499,7 +498,7 @@ func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -527,7 +526,7 @@ func TestSpecBuilder_Good_CacheAndI18nExtensions(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -570,7 +569,7 @@ func TestSpecBuilder_Good_OmitsNonPositiveCacheTTLExtension(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -594,7 +593,7 @@ func TestSpecBuilder_Good_GraphQLEndpoint(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -675,7 +674,7 @@ func TestSpecBuilder_Good_GraphQLPlaygroundEndpoint(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -714,7 +713,7 @@ func TestSpecBuilder_Good_GraphQLPlaygroundDefaultsToGraphQLPath(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -740,7 +739,7 @@ func TestSpecBuilder_Good_GraphQLPlaygroundDefaultsToGraphQLTag(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -774,7 +773,7 @@ func TestSpecBuilder_Good_ChatCompletionsEndpointExtension(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -802,7 +801,7 @@ func TestSpecBuilder_Good_ChatCompletionsHonoursCustomPath(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -825,7 +824,7 @@ func TestSpecBuilder_Good_ChatCompletionsOmittedWhenDisabled(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -853,7 +852,7 @@ func TestSpecBuilder_Good_ChatCompletionsPathAppearsInPaths(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -884,7 +883,7 @@ func TestSpecBuilder_Bad_ChatCompletionsPathAbsentWhenDisabled(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -910,7 +909,7 @@ func TestSpecBuilder_Ugly_ChatCompletionsPathCustomOverrideHonoured(t *testing.T } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -939,7 +938,7 @@ func TestSpecBuilder_Good_OpenAPISpecEndpointAppearsInPaths(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -979,7 +978,7 @@ func TestSpecBuilder_Bad_OpenAPISpecEndpointAbsentWhenDisabled(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1008,7 +1007,7 @@ func TestSpecBuilder_Ugly_OpenAPISpecPathCustomOverrideHonoured(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1037,7 +1036,7 @@ func TestSpecBuilder_Good_EnabledTransportsUseDefaultPaths(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1094,7 +1093,7 @@ func TestSpecBuilder_Good_WebSocketEndpoint(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1147,7 +1146,7 @@ func TestSpecBuilder_Good_ServerSentEventsEndpoint(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1213,7 +1212,7 @@ func TestSpecBuilder_Good_InfoIncludesLicenseMetadata(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1244,7 +1243,7 @@ func TestSpecBuilder_Good_InfoIncludesSummary(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1270,7 +1269,7 @@ func TestSpecBuilder_Good_InfoIncludesContactMetadata(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1304,7 +1303,7 @@ func TestSpecBuilder_Good_InfoIncludesTermsOfService(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1329,7 +1328,7 @@ func TestSpecBuilder_Good_InfoIncludesExternalDocs(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1402,7 +1401,7 @@ func TestSpecBuilder_Good_WithDescribableGroup(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1484,7 +1483,7 @@ func TestSpecBuilder_Good_DescribeIterGroup(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1526,7 +1525,7 @@ func TestSpecBuilder_Good_DescribeIterSnapshotOnce(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1568,7 +1567,7 @@ func TestSpecBuilder_Good_DescribeIterNilFallsBackToDescribe(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1603,7 +1602,7 @@ func TestSpecBuilder_Good_GroupMetadataIsSnapshottedOnce(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1663,7 +1662,7 @@ func TestSpecBuilder_Good_DeepClonesRouteMetadata(t *testing.T) { Parameters: []api.ParameterDescription{ { Name: "id", - In: "path", + In: `path`, Schema: map[string]any{ "type": "string", }, @@ -1688,7 +1687,7 @@ func TestSpecBuilder_Good_DeepClonesRouteMetadata(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1746,7 +1745,7 @@ func TestSpecBuilder_Good_SecuredResponses(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1829,7 +1828,7 @@ func TestSpecBuilder_Good_CustomSuccessStatusCode(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1878,7 +1877,7 @@ func TestSpecBuilder_Good_NoContentSuccessStatusCode(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -1936,7 +1935,7 @@ func TestSpecBuilder_Good_RouteSecurityOverrides(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2005,7 +2004,7 @@ func TestSpecBuilder_Good_AuthentikPublicPathsMakeMatchingOperationsPublic(t *te } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2047,7 +2046,7 @@ func TestSpecBuilder_Good_AuthentikPublicPathsMakeBuiltInEndpointsPublic(t *test } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2104,7 +2103,7 @@ func TestSpecBuilder_Good_EnvelopeWrapping(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2210,7 +2209,7 @@ func TestSpecBuilder_Good_OperationIDPreservesPathParams(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2263,7 +2262,7 @@ func TestSpecBuilder_Good_RequestBodyOnDelete(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2308,7 +2307,7 @@ func TestSpecBuilder_Good_RequestBodyOnHead(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2350,7 +2349,7 @@ func TestSpecBuilder_Good_RequestExampleWithoutSchema(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2396,7 +2395,7 @@ func TestSpecBuilder_Good_ResponseExampleWithoutSchema(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2450,7 +2449,7 @@ func TestSpecBuilder_Good_ResponseHeaders(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2513,7 +2512,7 @@ func TestSpecBuilder_Good_PathParameters(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2530,7 +2529,7 @@ func TestSpecBuilder_Good_PathParameters(t *testing.T) { if first["name"] != "id" { t.Fatalf("expected first parameter name=id, got %v", first["name"]) } - if first["in"] != "path" { + if first["in"] != `path` { t.Fatalf("expected first parameter in=path, got %v", first["in"]) } if required, ok := first["required"].(bool); !ok || !required { @@ -2570,7 +2569,7 @@ func TestSpecBuilder_Good_PathNormalisation(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2615,7 +2614,7 @@ func TestSpecBuilder_Good_GinPathParameters(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2635,7 +2634,7 @@ func TestSpecBuilder_Good_GinPathParameters(t *testing.T) { if len(fileParams) != 1 { t.Fatalf("expected 1 parameter for wildcard path, got %d", len(fileParams)) } - if fileParams[0].(map[string]any)["name"] != "path" { + if fileParams[0].(map[string]any)["name"] != `path` { t.Fatalf("expected wildcard parameter name=path, got %v", fileParams[0]) } } @@ -2657,7 +2656,7 @@ func TestSpecBuilder_Good_ExplicitParameters(t *testing.T) { Parameters: []api.ParameterDescription{ { Name: "id", - In: "path", + In: `path`, Description: "User identifier", Schema: map[string]any{ "type": "string", @@ -2685,7 +2684,7 @@ func TestSpecBuilder_Good_ExplicitParameters(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2702,7 +2701,7 @@ func TestSpecBuilder_Good_ExplicitParameters(t *testing.T) { if pathParam["name"] != "id" { t.Fatalf("expected path parameter name=id, got %v", pathParam["name"]) } - if pathParam["in"] != "path" { + if pathParam["in"] != `path` { t.Fatalf("expected path parameter in=path, got %v", pathParam["in"]) } if pathParam["description"] != "User identifier" { @@ -2733,7 +2732,7 @@ func TestSpecBuilder_Good_NonDescribableGroup(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2789,7 +2788,7 @@ func TestSpecBuilder_Good_EmptyDescribableGroupStillAddsTag(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2842,7 +2841,7 @@ func TestSpecBuilder_Good_DefaultTagsFromGroupName(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2884,7 +2883,7 @@ func TestSpecBuilder_Good_TagsAreSortedDeterministically(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -2941,7 +2940,7 @@ func TestSpecBuilder_Good_DeprecatedOperation(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3022,7 +3021,7 @@ func TestSpecBuilder_Good_BlankTagsAreIgnored(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3080,7 +3079,7 @@ func TestSpecBuilder_Good_BlankRouteTagsFallBackToGroupName(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3149,7 +3148,7 @@ func TestSpecBuilder_Good_HiddenRoutesAreOmitted(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3220,7 +3219,7 @@ func TestSpecBuilder_Good_ToolBridgeIntegration(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, }, OutputSchema: map[string]any{ @@ -3252,7 +3251,7 @@ func TestSpecBuilder_Good_ToolBridgeIntegration(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3331,7 +3330,7 @@ func TestSpecBuilder_Bad_InfoFields(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3365,7 +3364,7 @@ func TestSpecBuilder_Good_Servers(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3405,7 +3404,7 @@ func TestSpecBuilder_Good_ServersCollapseTrailingSlashes(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -3441,7 +3440,7 @@ func TestSpecBuilder_Good_RuntimeDebugEndpointsDocumentRateLimitHeaders(t *testi } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } diff --git a/options.go b/go/options.go similarity index 99% rename from options.go rename to go/options.go index 617294a..bbcef58 100644 --- a/options.go +++ b/go/options.go @@ -9,7 +9,7 @@ import ( "slices" "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/99designs/gqlgen/graphql" "github.com/casbin/casbin/v2" diff --git a/go/options_example_test.go b/go/options_example_test.go new file mode 100644 index 0000000..422abad --- /dev/null +++ b/go/options_example_test.go @@ -0,0 +1,1985 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestOptions_WithAddr_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAddr("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithAddr_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAddr("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithAddr_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAddr("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithHTTP3_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTP3("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithHTTP3_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTP3("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithHTTP3_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTP3("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithBearerAuth_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerAuth("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithBearerAuth_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerAuth("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithBearerAuth_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBearerAuth("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithRequestID_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRequestID() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithRequestID_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRequestID() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithRequestID_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRequestID() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithResponseMeta_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithResponseMeta() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithResponseMeta_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithResponseMeta() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithResponseMeta_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithResponseMeta() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithCORS_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCORS() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithCORS_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCORS() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithCORS_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCORS() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithMiddleware_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithMiddleware() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithMiddleware_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithMiddleware() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithMiddleware_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithMiddleware() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithStatic_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithStatic("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithStatic_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithStatic("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithStatic_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithStatic("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithWSHandler_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSHandler(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithWSHandler_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSHandler(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithWSHandler_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSHandler(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithWebSocket_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocket(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithWebSocket_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocket(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithWebSocket_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocket(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithWSPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithWSPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithWSPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWSPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithAuthentik_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthentik(AuthentikConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithAuthentik_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthentik(AuthentikConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithAuthentik_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthentik(AuthentikConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSunset_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSunset_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSunset_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwagger_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwagger("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwagger_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwagger("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwagger_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwagger("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerSummary_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSummary("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerSummary_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSummary("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerSummary_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSummary("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerTermsOfService_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerTermsOfService("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerTermsOfService_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerTermsOfService("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerTermsOfService_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerTermsOfService("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerContact_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerContact("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerContact_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerContact("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerContact_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerContact("", "", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerServers_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerServers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerServers_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerServers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerServers_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerServers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerLicense_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerLicense("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerLicense_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerLicense("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerLicense_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerLicense("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerSecuritySchemes_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSecuritySchemes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerSecuritySchemes_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSecuritySchemes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerSecuritySchemes_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerSecuritySchemes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSwaggerExternalDocs_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerExternalDocs("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSwaggerExternalDocs_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerExternalDocs("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSwaggerExternalDocs_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSwaggerExternalDocs("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithPprof_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPprof() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithPprof_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPprof() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithPprof_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithPprof() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithExpvar_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithExpvar() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithExpvar_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithExpvar() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithExpvar_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithExpvar() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSecure_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSecure() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSecure_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSecure() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSecure_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSecure() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithGzip_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGzip() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithGzip_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGzip() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithGzip_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGzip() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithBrotli_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBrotli() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithBrotli_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBrotli() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithBrotli_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithBrotli() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSlog_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSlog(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSlog_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSlog(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSlog_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSlog(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithTimeout_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTimeout(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithTimeout_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTimeout(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithTimeout_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTimeout(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithCache_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCache(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithCache_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCache(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithCache_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCache(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithCacheLimits_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCacheLimits(0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithCacheLimits_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCacheLimits(0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithCacheLimits_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithCacheLimits(0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithRateLimit_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRateLimit(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithRateLimit_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRateLimit(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithRateLimit_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithRateLimit(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSessions_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSessions("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSessions_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSessions("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSessions_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSessions("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithAuthz_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthz(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithAuthz_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthz(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithAuthz_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithAuthz(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithHTTPSign_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPSign(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithHTTPSign_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPSign(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithHTTPSign_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithHTTPSign(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSSE_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSE(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSSE_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSE(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSSE_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSE(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSSEPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSSEPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSSEPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithLocation_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithLocation() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithLocation_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithLocation() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithLocation_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithLocation() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithGraphQL_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQL(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithGraphQL_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQL(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithGraphQL_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithGraphQL(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithChatCompletions_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletions(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithChatCompletions_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletions(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithChatCompletions_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletions(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithChatCompletionsPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletionsPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithChatCompletionsPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletionsPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithChatCompletionsPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithChatCompletionsPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithSDKGen_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSDKGen() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithSDKGen_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSDKGen() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithSDKGen_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSDKGen() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithOpenAPISpec_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpec() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithOpenAPISpec_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpec() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithOpenAPISpec_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpec() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestOptions_WithOpenAPISpecPath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpecPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestOptions_WithOpenAPISpecPath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpecPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestOptions_WithOpenAPISpecPath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithOpenAPISpecPath("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWithAddr_options() { + func() { + defer func() { _ = recover() }() + _ = WithAddr("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithHTTP3_options() { + func() { + defer func() { _ = recover() }() + _ = WithHTTP3("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithBearerAuth_options() { + func() { + defer func() { _ = recover() }() + _ = WithBearerAuth("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithRequestID_options() { + func() { + defer func() { _ = recover() }() + _ = WithRequestID() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithResponseMeta_options() { + func() { + defer func() { _ = recover() }() + _ = WithResponseMeta() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithCORS_options() { + func() { + defer func() { _ = recover() }() + _ = WithCORS() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithMiddleware_options() { + func() { + defer func() { _ = recover() }() + _ = WithMiddleware() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithStatic_options() { + func() { + defer func() { _ = recover() }() + _ = WithStatic("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithWSHandler_options() { + func() { + defer func() { _ = recover() }() + _ = WithWSHandler(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithWebSocket_options() { + func() { + defer func() { _ = recover() }() + _ = WithWebSocket(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithWSPath_options() { + func() { + defer func() { _ = recover() }() + _ = WithWSPath("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithAuthentik_options() { + func() { + defer func() { _ = recover() }() + _ = WithAuthentik(AuthentikConfig{}) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSunset_options() { + func() { + defer func() { _ = recover() }() + _ = WithSunset("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwagger_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwagger("", "", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerSummary_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerSummary("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerPath_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerPath("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerTermsOfService_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerTermsOfService("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerContact_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerContact("", "", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerServers_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerServers() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerLicense_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerLicense("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerSecuritySchemes_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerSecuritySchemes(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSwaggerExternalDocs_options() { + func() { + defer func() { _ = recover() }() + _ = WithSwaggerExternalDocs("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithPprof_options() { + func() { + defer func() { _ = recover() }() + _ = WithPprof() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithExpvar_options() { + func() { + defer func() { _ = recover() }() + _ = WithExpvar() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSecure_options() { + func() { + defer func() { _ = recover() }() + _ = WithSecure() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithGzip_options() { + func() { + defer func() { _ = recover() }() + _ = WithGzip() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithBrotli_options() { + func() { + defer func() { _ = recover() }() + _ = WithBrotli() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSlog_options() { + func() { + defer func() { _ = recover() }() + _ = WithSlog(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithTimeout_options() { + func() { + defer func() { _ = recover() }() + _ = WithTimeout(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithCache_options() { + func() { + defer func() { _ = recover() }() + _ = WithCache(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithCacheLimits_options() { + func() { + defer func() { _ = recover() }() + _ = WithCacheLimits(0, 0, 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithRateLimit_options() { + func() { + defer func() { _ = recover() }() + _ = WithRateLimit(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSessions_options() { + func() { + defer func() { _ = recover() }() + _ = WithSessions("", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithAuthz_options() { + func() { + defer func() { _ = recover() }() + _ = WithAuthz(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithHTTPSign_options() { + func() { + defer func() { _ = recover() }() + _ = WithHTTPSign(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSSE_options() { + func() { + defer func() { _ = recover() }() + _ = WithSSE(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSSEPath_options() { + func() { + defer func() { _ = recover() }() + _ = WithSSEPath("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithLocation_options() { + func() { + defer func() { _ = recover() }() + _ = WithLocation() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithGraphQL_options() { + func() { + defer func() { _ = recover() }() + _ = WithGraphQL(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithChatCompletions_options() { + func() { + defer func() { _ = recover() }() + _ = WithChatCompletions(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithChatCompletionsPath_options() { + func() { + defer func() { _ = recover() }() + _ = WithChatCompletionsPath("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSDKGen_options() { + func() { + defer func() { _ = recover() }() + _ = WithSDKGen() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithOpenAPISpec_options() { + func() { + defer func() { _ = recover() }() + _ = WithOpenAPISpec() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithOpenAPISpecPath_options() { + func() { + defer func() { _ = recover() }() + _ = WithOpenAPISpecPath("") + }() + coretest.Println("done") + // Output: done +} diff --git a/options_test.go b/go/options_test.go similarity index 98% rename from options_test.go rename to go/options_test.go index 48371cc..97cc242 100644 --- a/options_test.go +++ b/go/options_test.go @@ -8,7 +8,7 @@ import ( "crypto/rand" "crypto/tls" "crypto/x509" - "errors" + core "dappco.re/go" "math/big" "net" "net/http" @@ -109,7 +109,7 @@ func TestServeH3_Bad_RequiresTLSConfig(t *testing.T) { } err = e.ServeH3(context.Background(), nil) - if !errors.Is(err, ErrHTTP3TLSRequired) { + if !core.Is(err, ErrHTTP3TLSRequired) { t.Fatalf("expected ErrHTTP3TLSRequired, got %v", err) } } diff --git a/pkg/provider/cache_control_example_test.go b/go/pkg/provider/cache_control_example_test.go similarity index 96% rename from pkg/provider/cache_control_example_test.go rename to go/pkg/provider/cache_control_example_test.go index 8864185..b112e55 100644 --- a/pkg/provider/cache_control_example_test.go +++ b/go/pkg/provider/cache_control_example_test.go @@ -6,7 +6,7 @@ import ( "net/http" "net/http/httptest" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/pkg/provider/cache_control_test.go b/go/pkg/provider/cache_control_test.go similarity index 89% rename from pkg/provider/cache_control_test.go rename to go/pkg/provider/cache_control_test.go index e8a54f8..fbe9aa4 100644 --- a/pkg/provider/cache_control_test.go +++ b/go/pkg/provider/cache_control_test.go @@ -5,12 +5,11 @@ package provider_test import ( "net/http" "net/http/httptest" - "testing" + . "dappco.re/go" "dappco.re/go/api" "dappco.re/go/api/pkg/provider" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/require" ) type cacheControlProvider struct { @@ -83,7 +82,7 @@ func mountProviderHandler(providers ...provider.Provider) http.Handler { return engine.Handler() } -func TestCacheControl_MountAll_Good_AppliesDescribedPolicies(t *testing.T) { +func TestCacheControl_MountAll_Good_AppliesDescribedPolicies(t *T) { gin.SetMode(gin.TestMode) handler := mountProviderHandler(&cacheControlProvider{ @@ -94,15 +93,15 @@ func TestCacheControl_MountAll_Good_AppliesDescribedPolicies(t *testing.T) { getRec := httptest.NewRecorder() getReq := httptest.NewRequest(http.MethodGet, "/api/cache/items/123", nil) handler.ServeHTTP(getRec, getReq) - require.Equal(t, "public, max-age=300", getRec.Header().Get("Cache-Control")) + AssertEqual(t, "public, max-age=300", getRec.Header().Get("Cache-Control")) postRec := httptest.NewRecorder() postReq := httptest.NewRequest(http.MethodPost, "/api/cache/sessions", nil) handler.ServeHTTP(postRec, postReq) - require.Equal(t, "no-store", postRec.Header().Get("Cache-Control")) + AssertEqual(t, "no-store", postRec.Header().Get("Cache-Control")) } -func TestCacheControl_MountAll_Bad_SkipsProvidersWithoutDescriptions(t *testing.T) { +func TestCacheControl_MountAll_Bad_SkipsProvidersWithoutDescriptions(t *T) { gin.SetMode(gin.TestMode) handler := mountProviderHandler(&undescribedCacheControlProvider{ @@ -112,10 +111,10 @@ func TestCacheControl_MountAll_Bad_SkipsProvidersWithoutDescriptions(t *testing. rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/plain/items/123", nil) handler.ServeHTTP(rec, req) - require.Equal(t, "", rec.Header().Get("Cache-Control")) + AssertEqual(t, "", rec.Header().Get("Cache-Control")) } -func TestCacheControl_MountAll_Ugly_PreservesExplicitHandlerHeaders(t *testing.T) { +func TestCacheControl_MountAll_Ugly_PreservesExplicitHandlerHeaders(t *T) { gin.SetMode(gin.TestMode) handler := mountProviderHandler(&cacheControlProvider{ @@ -127,5 +126,5 @@ func TestCacheControl_MountAll_Ugly_PreservesExplicitHandlerHeaders(t *testing.T rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/override/items/123", nil) handler.ServeHTTP(rec, req) - require.Equal(t, "private, no-store", rec.Header().Get("Cache-Control")) + AssertEqual(t, "private, no-store", rec.Header().Get("Cache-Control")) } diff --git a/pkg/provider/discovery.go b/go/pkg/provider/discovery.go similarity index 72% rename from pkg/provider/discovery.go rename to go/pkg/provider/discovery.go index 796de0b..4bcf662 100644 --- a/pkg/provider/discovery.go +++ b/go/pkg/provider/discovery.go @@ -3,11 +3,9 @@ package provider import ( - "os" - "path/filepath" "slices" - core "dappco.re/go/core" + core "dappco.re/go" "gopkg.in/yaml.v3" ) @@ -16,7 +14,10 @@ const DefaultProvidersDir = ".core/providers" // Discover loads local polyglot provider manifests from dir. A blank dir uses // ".core/providers". Missing directories or no matching YAML files are treated // as empty discovery results. -func Discover(dir string) ([]Provider, error) { +func Discover(dir string) ( + []Provider, + error, +) { const op = "provider.Discover" canonicalDir, files, err := providerManifestFiles(dir) @@ -40,12 +41,17 @@ func Discover(dir string) ([]Provider, error) { } // DiscoverDefault loads provider manifests from ".core/providers". -func DiscoverDefault() ([]Provider, error) { +func DiscoverDefault() ( + []Provider, + error, +) { return Discover(DefaultProvidersDir) } // Discover adds every provider manifest found in dir to the registry. -func (r *Registry) Discover(dir string) error { +func (r *Registry) Discover(dir string) ( + _ error, +) { providers, err := Discover(dir) if err != nil { return err @@ -57,7 +63,9 @@ func (r *Registry) Discover(dir string) error { } // DiscoverDefault adds providers from ".core/providers" to the registry. -func (r *Registry) DiscoverDefault() error { +func (r *Registry) DiscoverDefault() ( + _ error, +) { return r.Discover(DefaultProvidersDir) } @@ -84,7 +92,11 @@ type providerManifestFile struct { readPath string } -func providerManifestFiles(dir string) (string, []providerManifestFile, error) { +func providerManifestFiles(dir string) ( + string, + []providerManifestFile, + error, +) { dir = core.Trim(dir) if dir == "" { dir = DefaultProvidersDir @@ -95,7 +107,7 @@ func providerManifestFiles(dir string) (string, []providerManifestFile, error) { return canonicalDir, nil, err } - matches := append(core.PathGlob(filepath.Join(canonicalDir, "*.yaml")), core.PathGlob(filepath.Join(canonicalDir, "*.yml"))...) + matches := append(core.PathGlob(core.PathJoin(canonicalDir, "*.yaml")), core.PathGlob(core.PathJoin(canonicalDir, "*.yml"))...) slices.Sort(matches) files := make([]providerManifestFile, 0, len(matches)) @@ -109,78 +121,79 @@ func providerManifestFiles(dir string) (string, []providerManifestFile, error) { return canonicalDir, files, nil } -func canonicalProviderDir(dir string) (string, bool, error) { +func canonicalProviderDir(dir string) ( + string, + bool, + error, +) { const op = "provider.providerManifestFiles" - absolute, err := filepath.Abs(filepath.Clean(dir)) + absolute, err := providerPathAbs(cleanProviderPath(dir)) if err != nil { return "", false, core.E(op, "resolve provider directory path", err) } - info, err := os.Lstat(absolute) + info, err := providerLstat(absolute) if err != nil { - if os.IsNotExist(err) { + if core.IsNotExist(err) { return "", false, nil } return "", false, core.E(op, "stat provider directory", err) } - if info.Mode()&os.ModeSymlink != 0 { + if info.Mode()&core.ModeSymlink != 0 { return "", false, core.E(op, "symlinked provider directory rejected: "+absolute, nil) } if !info.IsDir() { return "", false, core.E(op, "provider path is not a directory: "+absolute, nil) } - resolved, err := filepath.EvalSymlinks(absolute) + resolved, err := providerPathEvalSymlinks(absolute) if err != nil { return "", false, core.E(op, "resolve provider directory symlinks: "+absolute, err) } - cleaned := filepath.Clean(resolved) - if cleaned != absolute { - return "", false, core.E(op, "symlink in ancestor path segment rejected: "+absolute, nil) - } + cleaned := cleanProviderPath(resolved) return cleaned, true, nil } -func canonicalProviderManifestFile(canonicalDir, path string) (providerManifestFile, error) { +func canonicalProviderManifestFile(canonicalDir, path string) ( + providerManifestFile, + error, +) { const op = "provider.providerManifestFiles" - absolute, err := filepath.Abs(filepath.Clean(path)) + absolute, err := providerPathAbs(cleanProviderPath(path)) if err != nil { return providerManifestFile{}, core.E(op, "resolve provider manifest path", err) } - info, err := os.Lstat(absolute) + info, err := providerLstat(absolute) if err != nil { return providerManifestFile{}, core.E(op, "stat provider manifest", err) } - if info.Mode()&os.ModeSymlink != 0 { + if info.Mode()&core.ModeSymlink != 0 { return providerManifestFile{}, core.E(op, "symlinked provider manifest rejected: "+absolute, nil) } if !info.Mode().IsRegular() { return providerManifestFile{}, core.E(op, "provider manifest is not a regular file: "+absolute, nil) } - resolved, err := filepath.EvalSymlinks(absolute) + resolved, err := providerPathEvalSymlinks(absolute) if err != nil { return providerManifestFile{}, core.E(op, "resolve provider manifest symlinks: "+absolute, err) } - resolved = filepath.Clean(resolved) - if resolved != absolute { - return providerManifestFile{}, core.E(op, "symlink in ancestor path segment rejected: "+absolute, nil) - } + resolved = cleanProviderPath(resolved) - relative, err := filepath.Rel(canonicalDir, resolved) + relative, err := providerPathRel(canonicalDir, resolved) if err != nil { return providerManifestFile{}, core.E(op, "compare provider manifest with provider directory", err) } - parentPrefix := ".." + string(filepath.Separator) - if relative == ".." || core.HasPrefix(relative, parentPrefix) || filepath.IsAbs(relative) { + parentPrefix := ".." + string(core.PathSeparator) + if relative == ".." || core.HasPrefix(relative, parentPrefix) || core.PathIsAbs(relative) { return providerManifestFile{}, core.E(op, "provider manifest escapes provider directory: "+absolute, nil) } readPath := relative - if canonicalDir == string(filepath.Separator) { + if canonicalDir == string(core.PathSeparator) { readPath = resolved } @@ -190,7 +203,66 @@ func canonicalProviderManifestFile(canonicalDir, path string) (providerManifestF }, nil } -func loadProviderManifest(fs *core.Fs, file providerManifestFile) (Provider, error) { +func cleanProviderPath(path string) string { + return core.CleanPath(path, string(core.PathSeparator)) +} + +func providerPathAbs(path string) ( + string, + error, +) { + r := core.PathAbs(path) + if !r.OK { + err, _ := r.Value.(error) + return "", err + } + out, _ := r.Value.(string) + return out, nil +} + +func providerPathEvalSymlinks(path string) ( + string, + error, +) { + r := core.PathEvalSymlinks(path) + if !r.OK { + err, _ := r.Value.(error) + return "", err + } + out, _ := r.Value.(string) + return out, nil +} + +func providerPathRel(base, target string) ( + string, + error, +) { + r := core.PathRel(base, target) + if !r.OK { + err, _ := r.Value.(error) + return "", err + } + out, _ := r.Value.(string) + return out, nil +} + +func providerLstat(path string) ( + core.FsFileInfo, + error, +) { + r := core.Lstat(path) + if !r.OK { + err, _ := r.Value.(error) + return nil, err + } + info, _ := r.Value.(core.FsFileInfo) + return info, nil +} + +func loadProviderManifest(fs *core.Fs, file providerManifestFile) ( + Provider, + error, +) { const op = "provider.loadProviderManifest" result := fs.Read(file.readPath) @@ -223,7 +295,10 @@ func loadProviderManifest(fs *core.Fs, file providerManifestFile) (Provider, err return p, nil } -func (m providerManifest) proxyConfig(path string) (ProxyConfig, error) { +func (m providerManifest) proxyConfig(path string) ( + ProxyConfig, + error, +) { const op = "provider.Manifest.proxyConfig" name := core.Trim(m.Name) @@ -253,7 +328,10 @@ func (m providerManifest) proxyConfig(path string) (ProxyConfig, error) { }, nil } -func normaliseManifestBasePath(path string) (string, error) { +func normaliseManifestBasePath(path string) ( + string, + error, +) { path = core.Trim(path) if path == "" { return "", core.E("provider.normaliseManifestBasePath", "basePath is required", nil) diff --git a/go/pkg/provider/discovery_example_test.go b/go/pkg/provider/discovery_example_test.go new file mode 100644 index 0000000..eaa5d76 --- /dev/null +++ b/go/pkg/provider/discovery_example_test.go @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider + +import coretest "dappco.re/go" + +func TestDiscovery_Discover_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestDiscovery_Discover_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestDiscovery_Discover_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestDiscovery_DiscoverDefault_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestDiscovery_DiscoverDefault_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestDiscovery_DiscoverDefault_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestDiscovery_Registry_Discover_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestDiscovery_Registry_Discover_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestDiscovery_Registry_Discover_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Discover("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestDiscovery_Registry_DiscoverDefault_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestDiscovery_Registry_DiscoverDefault_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestDiscovery_Registry_DiscoverDefault_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DiscoverDefault() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleDiscover_discovery() { + func() { + defer func() { _ = recover() }() + _, _ = Discover("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleDiscoverDefault_discovery() { + func() { + defer func() { _ = recover() }() + _, _ = DiscoverDefault() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Discover_discovery() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Discover("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_DiscoverDefault_discovery() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.DiscoverDefault() + }() + coretest.Println("done") + // Output: done +} diff --git a/go/pkg/provider/discovery_test.go b/go/pkg/provider/discovery_test.go new file mode 100644 index 0000000..7c55734 --- /dev/null +++ b/go/pkg/provider/discovery_test.go @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider_test + +import ( + "net/http" + "net/http/httptest" + + . "dappco.re/go" + "dappco.re/go/api" + "dappco.re/go/api/pkg/provider" +) + +func TestDiscover_Good_LoadsYAMLProxyProvider(t *T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + coreJSONEncode(w, map[string]string{`path`: r.URL.Path}) + })) + defer upstream.Close() + + dir := PathJoin(t.TempDir(), ".core", "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + specPath := PathJoin(PathDir(dir), "specs", "openapi.yaml") + RequireNoError(t, coreMkdirAll(PathDir(specPath), 0755)) + RequireNoError(t, coreWriteFile(specPath, []byte("openapi: 3.1.0\n"), 0644)) + RequireNoError(t, coreWriteFile(PathJoin(dir, "cool.yaml"), []byte(` +name: cool-widget +runtime: php +base_path: /api/v1/cool-widget/ +upstream: `+upstream.URL+` +spec_file: ../specs/openapi.yaml +element: + tag: core-cool-widget + source: /assets/cool-widget.js +`), 0644)) + + providers, err := provider.Discover(dir) + RequireNoError(t, err) + AssertLen(t, providers, 1) + + p := providers[0] + AssertEqual(t, "cool-widget", p.Name()) + AssertEqual(t, "/api/v1/cool-widget", p.BasePath()) + + specProvider, ok := p.(interface{ SpecFile() string }) + RequireTrue(t, ok) + canonicalSpecPath, err := corePathEvalSymlinks(specPath) + RequireNoError(t, err) + AssertEqual(t, canonicalSpecPath, specProvider.SpecFile()) + + renderable, ok := p.(provider.Renderable) + RequireTrue(t, ok) + AssertEqual(t, "core-cool-widget", renderable.Element().Tag) + + engine, err := api.New() + RequireNoError(t, err) + engine.Register(p) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/cool-widget/ping", nil) + engine.Handler().ServeHTTP(w, req) + + AssertEqual(t, http.StatusOK, w.Code) + var body map[string]string + RequireNoError(t, coreJSONUnmarshal(w.Body.Bytes(), &body)) + AssertEqual(t, "/ping", body[`path`]) +} + +func TestDiscover_Good_MissingDirIsEmpty(t *T) { + providers, err := provider.Discover(PathJoin(t.TempDir(), ".core", "providers")) + RequireNoError(t, err) + AssertEmpty(t, providers) +} + +func TestDiscover_Good_LoadsYAMLProvidersFromCleanDir(t *T) { + dir := PathJoin(t.TempDir(), ".core", "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + upstream := newDiscoveryUpstream(t) + + writeProviderManifest(t, dir, "alpha", upstream) + writeProviderManifest(t, dir, "beta", upstream) + + providers, err := provider.Discover(dir) + RequireNoError(t, err) + AssertLen(t, providers, 2) + + names := []string{providers[0].Name(), providers[1].Name()} + AssertElementsMatch(t, []string{"alpha", "beta"}, names) +} + +func TestDiscover_Good_DirWithDotDotSegmentResolves(t *T) { + root := t.TempDir() + dir := PathJoin(root, "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + writeProviderManifest(t, dir, "dotdot", newDiscoveryUpstream(t)) + + providers, err := provider.Discover(PathJoin(root, "other", "..", "providers")) + RequireNoError(t, err) + AssertLen(t, providers, 1) + AssertEqual(t, "dotdot", providers[0].Name()) +} + +func TestDiscover_Bad_InvalidManifest(t *T) { + dir := PathJoin(t.TempDir(), ".core", "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + RequireNoError(t, coreWriteFile(PathJoin(dir, "broken.yaml"), []byte(` +name: broken +basePath: /api/broken +`), 0644)) + + providers, err := provider.Discover(dir) + AssertError(t, err) + AssertNil(t, providers) + AssertContains(t, err.Error(), "upstream is required") +} + +func TestDiscover_Bad_SymlinkedDirRefused(t *T) { + root := t.TempDir() + realDir := PathJoin(root, "real-providers") + linkDir := PathJoin(root, "providers") + RequireNoError(t, coreMkdirAll(realDir, 0755)) + if err := coreSymlink(realDir, linkDir); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + providers, err := provider.Discover(linkDir) + AssertError(t, err) + AssertNil(t, providers) + AssertContains(t, err.Error(), "symlinked provider directory rejected") +} + +func TestDiscover_Bad_SymlinkManifestOutsideDirRefused(t *T) { + root := t.TempDir() + dir := PathJoin(root, "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + outside := PathJoin(root, "outside.yaml") + RequireNoError(t, coreWriteFile(outside, []byte("not: loaded\n"), 0644)) + if err := coreSymlink(outside, PathJoin(dir, "leak.yaml")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + providers, err := provider.Discover(dir) + AssertError(t, err) + AssertNil(t, providers) + AssertContains(t, err.Error(), "symlinked provider manifest rejected") +} + +func TestDiscover_Bad_SymlinkManifestWithinDirRefused(t *T) { + dir := PathJoin(t.TempDir(), "providers") + RequireNoError(t, coreMkdirAll(dir, 0755)) + realManifest := writeProviderManifest(t, dir, "real", newDiscoveryUpstream(t)) + if err := coreSymlink(realManifest, PathJoin(dir, "alias.yaml")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + providers, err := provider.Discover(dir) + AssertError(t, err) + AssertNil(t, providers) + AssertContains(t, err.Error(), "symlinked provider manifest rejected") +} + +func newDiscoveryUpstream(t *T) string { + t.Helper() + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(upstream.Close) + return upstream.URL +} + +func writeProviderManifest(t *T, dir, name, upstream string) string { + t.Helper() + path := PathJoin(dir, name+".yaml") + RequireNoError(t, coreWriteFile(path, []byte(` +name: `+name+` +basePath: /api/`+name+` +upstream: `+upstream+` +`), 0644)) + return path +} diff --git a/pkg/provider/provider.go b/go/pkg/provider/provider.go similarity index 100% rename from pkg/provider/provider.go rename to go/pkg/provider/provider.go diff --git a/go/pkg/provider/provider_example_test.go b/go/pkg/provider/provider_example_test.go new file mode 100644 index 0000000..12ca56d --- /dev/null +++ b/go/pkg/provider/provider_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider diff --git a/go/pkg/provider/provider_test.go b/go/pkg/provider/provider_test.go new file mode 100644 index 0000000..12ca56d --- /dev/null +++ b/go/pkg/provider/provider_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider diff --git a/pkg/provider/proxy.go b/go/pkg/provider/proxy.go similarity index 86% rename from pkg/provider/proxy.go rename to go/pkg/provider/proxy.go index 21a1ee5..f70ae80 100644 --- a/pkg/provider/proxy.go +++ b/go/pkg/provider/proxy.go @@ -3,15 +3,13 @@ package provider import ( - "errors" "net" "net/http" "net/http/httputil" "net/url" // Note: AX-6 — net/url url.URL fields are structural for reverse-proxy URL rewriting. - "os" "strconv" - core "dappco.re/go/core" + core "dappco.re/go" coreapi "dappco.re/go/api" "github.com/gin-gonic/gin" @@ -21,7 +19,7 @@ const providerUpstreamAllowEnv = "CORE_PROVIDER_UPSTREAM_ALLOW" // ErrProviderUpstreamBlocked marks provider upstream URL rejections by the // construction-time SSRF guard. -var ErrProviderUpstreamBlocked = errors.New("provider upstream blocked by SSRF guard") +var ErrProviderUpstreamBlocked = core.NewError("provider upstream blocked by SSRF guard") // ProviderUpstreamBlockedError carries the concrete rejection reason for a // provider upstream URL blocked by the SSRF guard. @@ -48,19 +46,21 @@ func (e *ProviderUpstreamBlockedError) Error() string { } // Is reports whether target is ErrProviderUpstreamBlocked, so callers can -// errors.Is(err, ErrProviderUpstreamBlocked) without unwrapping. +// core.Is(err, ErrProviderUpstreamBlocked) without unwrapping. // -// errors.Is(err, ErrProviderUpstreamBlocked) +// core.Is(err, ErrProviderUpstreamBlocked) func (e *ProviderUpstreamBlockedError) Is(target error) bool { return target == ErrProviderUpstreamBlocked } -// Unwrap exposes the underlying Cause for errors.As / errors.Unwrap chain +// Unwrap exposes the underlying Cause for core.As / errors.Unwrap chain // inspection. // // var inner *net.OpError -// if errors.As(err, &inner) { /* ... */ } -func (e *ProviderUpstreamBlockedError) Unwrap() error { +// if core.As(err, &inner) { /* ... */ } +func (e *ProviderUpstreamBlockedError) Unwrap() ( + _ error, +) { if e == nil { return nil } @@ -140,20 +140,21 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider { } } - proxy := httputil.NewSingleHostReverseProxy(target) - - // Preserve the original Director but strip the base path so the + // Preserve the original proxy target path rewriting and strip the base path so // upstream receives clean paths (e.g. /items instead of /api/v1/cool-widget/items). - defaultDirector := proxy.Director basePath := core.TrimSuffix(cfg.BasePath, "/") - proxy.Director = func(req *http.Request) { - defaultDirector(req) - // Strip the base path prefix from the request path. - req.URL.Path = stripBasePath(req.URL.Path, basePath) - if req.URL.RawPath != "" { - req.URL.RawPath = stripBasePath(req.URL.RawPath, basePath) - } + proxy := &httputil.ReverseProxy{ + Rewrite: func(proxyReq *httputil.ProxyRequest) { + proxyReq.SetURL(target) + proxyReq.Out.Host = proxyReq.In.Host + + // Strip the base path prefix from the request path. + proxyReq.Out.URL.Path = stripBasePath(proxyReq.Out.URL.Path, basePath) + if proxyReq.Out.URL.RawPath != "" { + proxyReq.Out.URL.RawPath = stripBasePath(proxyReq.Out.URL.RawPath, basePath) + } + }, } return &ProxyProvider{ @@ -164,7 +165,9 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider { // Err reports any configuration error detected while constructing the proxy. // A nil error means the proxy is ready to mount and serve requests. -func (p *ProxyProvider) Err() error { +func (p *ProxyProvider) Err() ( + _ error, +) { if p == nil { return nil } @@ -251,7 +254,9 @@ func (p *ProxyProvider) Upstream() string { return p.config.Upstream } -func validateProviderUpstreamURL(raw string, target *url.URL) error { +func validateProviderUpstreamURL(raw string, target *url.URL) ( + _ error, +) { if target == nil { return blockProviderUpstream(raw, "invalid upstream URL result", nil) } @@ -305,7 +310,9 @@ func validateProviderUpstreamURL(raw string, target *url.URL) error { return nil } -func validateProviderUpstreamIP(raw, host string, ip net.IP, allowCIDRs []*net.IPNet) error { +func validateProviderUpstreamIP(raw, host string, ip net.IP, allowCIDRs []*net.IPNet) ( + _ error, +) { if reason := blockedProviderUpstreamIPReason(ip); reason != "" { if providerUpstreamIPAllowed(ip, allowCIDRs) { return nil @@ -315,7 +322,9 @@ func validateProviderUpstreamIP(raw, host string, ip net.IP, allowCIDRs []*net.I return nil } -func blockProviderUpstream(raw, reason string, cause error) error { +func blockProviderUpstream(raw, reason string, cause error) ( + _ error, +) { return &ProviderUpstreamBlockedError{ Upstream: raw, Reason: reason, @@ -323,8 +332,11 @@ func blockProviderUpstream(raw, reason string, cause error) error { } } -func providerUpstreamAllowCIDRs() ([]*net.IPNet, error) { - raw := core.Trim(os.Getenv(providerUpstreamAllowEnv)) +func providerUpstreamAllowCIDRs() ( + []*net.IPNet, + error, +) { + raw := core.Trim(core.Getenv(providerUpstreamAllowEnv)) if raw == "" { return nil, nil } @@ -420,17 +432,17 @@ var providerMetadataHosts = map[string]struct{}{ // not a configuration value. SonarCloud "IP should not be hardcoded" is a // false positive on this list. var providerBlockedCIDRs = mustParseProviderCIDRs( - "0.0.0.0/8", // RFC 1122 "this network" - "100.64.0.0/10", // RFC 6598 carrier-grade NAT - "127.0.0.0/8", // RFC 1122 loopback - "169.254.0.0/16", // RFC 3927 link-local - "192.0.0.0/24", // RFC 6890 IETF protocol assignments - "192.0.2.0/24", // RFC 5737 TEST-NET-1 - "198.18.0.0/15", // RFC 2544 benchmark - "198.51.100.0/24", // RFC 5737 TEST-NET-2 - "203.0.113.0/24", // RFC 5737 TEST-NET-3 - "224.0.0.0/4", // RFC 5771 multicast - "240.0.0.0/4", // RFC 1112 reserved + "0.0.0.0/8", // RFC 1122 "this network" + "100.64.0.0/10", // RFC 6598 carrier-grade NAT + "127.0.0.0/8", // RFC 1122 loopback + "169.254.0.0/16", // RFC 3927 link-local + "192.0.0.0/24", // RFC 6890 IETF protocol assignments + "192.0.2.0/24", // RFC 5737 TEST-NET-1 + "198.18.0.0/15", // RFC 2544 benchmark + "198.51.100.0/24", // RFC 5737 TEST-NET-2 + "203.0.113.0/24", // RFC 5737 TEST-NET-3 + "224.0.0.0/4", // RFC 5771 multicast + "240.0.0.0/4", // RFC 1112 reserved "::/128", "::1/128", "64:ff9b:1::/48", diff --git a/go/pkg/provider/proxy_example_test.go b/go/pkg/provider/proxy_example_test.go new file mode 100644 index 0000000..e41513e --- /dev/null +++ b/go/pkg/provider/proxy_example_test.go @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider + +import coretest "dappco.re/go" + +func TestProxy_ProviderUpstreamBlockedError_Error_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Error_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Error_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Is_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Is(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Is_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Is(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Is_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Is(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Unwrap_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Unwrap_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProviderUpstreamBlockedError_Unwrap_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProviderUpstreamBlockedError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_NewProxy_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewProxy(ProxyConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_NewProxy_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewProxy(ProxyConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_NewProxy_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewProxy(ProxyConfig{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_Err_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Err() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_Err_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Err() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_Err_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Err() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_Name_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_Name_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_Name_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_BasePath_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_BasePath_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_BasePath_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.BasePath() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_RegisterRoutes_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_RegisterRoutes_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_RegisterRoutes_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + subject.RegisterRoutes(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_Element_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Element() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_Element_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Element() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_Element_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Element() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_SpecFile_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.SpecFile() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_SpecFile_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.SpecFile() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_SpecFile_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.SpecFile() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestProxy_ProxyProvider_Upstream_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Upstream() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestProxy_ProxyProvider_Upstream_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Upstream() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestProxy_ProxyProvider_Upstream_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *ProxyProvider + _ = subject.Upstream() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleProviderUpstreamBlockedError_Error_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProviderUpstreamBlockedError + _ = subject.Error() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProviderUpstreamBlockedError_Is_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProviderUpstreamBlockedError + _ = subject.Is(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleProviderUpstreamBlockedError_Unwrap_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProviderUpstreamBlockedError + _ = subject.Unwrap() + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewProxy_proxy() { + func() { + defer func() { _ = recover() }() + _ = NewProxy(ProxyConfig{}) + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_Err_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.Err() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_Name_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.Name() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_BasePath_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.BasePath() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_RegisterRoutes_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + subject.RegisterRoutes(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_Element_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.Element() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_SpecFile_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.SpecFile() + }() + coretest.Println("done") + // Output: done +} + +func ExampleProxyProvider_Upstream_proxy() { + func() { + defer func() { _ = recover() }() + var subject *ProxyProvider + _ = subject.Upstream() + }() + coretest.Println("done") + // Output: done +} diff --git a/pkg/provider/proxy_internal_test.go b/go/pkg/provider/proxy_internal_test.go similarity index 100% rename from pkg/provider/proxy_internal_test.go rename to go/pkg/provider/proxy_internal_test.go diff --git a/pkg/provider/proxy_test.go b/go/pkg/provider/proxy_test.go similarity index 61% rename from pkg/provider/proxy_test.go rename to go/pkg/provider/proxy_test.go index c8d8b7d..a848df6 100644 --- a/pkg/provider/proxy_test.go +++ b/go/pkg/provider/proxy_test.go @@ -3,54 +3,50 @@ package provider_test import ( - "encoding/json" - "errors" "net/http" "net/http/httptest" - "os" "testing" + . "dappco.re/go" "dappco.re/go/api" "dappco.re/go/api/pkg/provider" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { const env = "CORE_PROVIDER_UPSTREAM_ALLOW" - previous, hadPrevious := os.LookupEnv(env) - _ = os.Setenv(env, "127.0.0.0/8,::1/128") + previous, hadPrevious := LookupEnv(env) + _ = coreSetenv(env, "127.0.0.0/8,::1/128") code := m.Run() if hadPrevious { - _ = os.Setenv(env, previous) + _ = coreSetenv(env, previous) } else { - _ = os.Unsetenv(env) + _ = coreUnsetenv(env) } - os.Exit(code) + Exit(code) } // -- ProxyProvider tests ------------------------------------------------------ -func TestProxyProvider_Name_Good(t *testing.T) { +func TestProxyProvider_Name_Good(t *T) { p := provider.NewProxy(provider.ProxyConfig{ Name: "cool-widget", BasePath: "/api/v1/cool-widget", Upstream: "http://127.0.0.1:9999", }) - assert.Equal(t, "cool-widget", p.Name()) + AssertEqual(t, "cool-widget", p.Name()) } -func TestProxyProvider_BasePath_Good(t *testing.T) { +func TestProxyProvider_BasePath_Good(t *T) { p := provider.NewProxy(provider.ProxyConfig{ Name: "cool-widget", BasePath: "/api/v1/cool-widget", Upstream: "http://127.0.0.1:9999", }) - assert.Equal(t, "/api/v1/cool-widget", p.BasePath()) + AssertEqual(t, "/api/v1/cool-widget", p.BasePath()) } -func TestProxyProvider_Element_Good(t *testing.T) { +func TestProxyProvider_Element_Good(t *T) { elem := provider.ElementSpec{ Tag: "core-cool-widget", Source: "/assets/cool-widget.js", @@ -61,29 +57,29 @@ func TestProxyProvider_Element_Good(t *testing.T) { Upstream: "http://127.0.0.1:9999", Element: elem, }) - assert.Equal(t, "core-cool-widget", p.Element().Tag) - assert.Equal(t, "/assets/cool-widget.js", p.Element().Source) + AssertEqual(t, "core-cool-widget", p.Element().Tag) + AssertEqual(t, "/assets/cool-widget.js", p.Element().Source) } -func TestProxyProvider_SpecFile_Good(t *testing.T) { +func TestProxyProvider_SpecFile_Good(t *T) { p := provider.NewProxy(provider.ProxyConfig{ Name: "cool-widget", BasePath: "/api/v1/cool-widget", Upstream: "http://127.0.0.1:9999", SpecFile: "/tmp/openapi.json", }) - assert.Equal(t, "/tmp/openapi.json", p.SpecFile()) + AssertEqual(t, "/tmp/openapi.json", p.SpecFile()) } -func TestProxyProvider_Proxy_Good(t *testing.T) { +func TestProxyProviderProxyForwards(t *T) { // Start a test upstream server. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := map[string]string{ - "path": r.URL.Path, + `path`: r.URL.Path, "method": r.Method, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) + coreJSONEncode(w, resp) })) defer upstream.Close() @@ -96,7 +92,7 @@ func TestProxyProvider_Proxy_Good(t *testing.T) { // Mount on an api.Engine. engine, err := api.New() - require.NoError(t, err) + RequireNoError(t, err) engine.Register(p) handler := engine.Handler() @@ -106,22 +102,22 @@ func TestProxyProvider_Proxy_Good(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + AssertEqual(t, http.StatusOK, w.Code) var body map[string]string - err = json.Unmarshal(w.Body.Bytes(), &body) - require.NoError(t, err) + err = coreJSONUnmarshal(w.Body.Bytes(), &body) + RequireNoError(t, err) // The upstream should see the path with base path stripped. - assert.Equal(t, "/items", body["path"]) - assert.Equal(t, "GET", body["method"]) + AssertEqual(t, "/items", body[`path`]) + AssertEqual(t, "GET", body["method"]) } -func TestProxyProvider_ProxyRoot_Good(t *testing.T) { +func TestProxyProviderProxyRootForwards(t *T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resp := map[string]string{"path": r.URL.Path} + resp := map[string]string{`path`: r.URL.Path} w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) + coreJSONEncode(w, resp) })) defer upstream.Close() @@ -132,7 +128,7 @@ func TestProxyProvider_ProxyRoot_Good(t *testing.T) { }) engine, err := api.New() - require.NoError(t, err) + RequireNoError(t, err) engine.Register(p) handler := engine.Handler() @@ -142,15 +138,15 @@ func TestProxyProvider_ProxyRoot_Good(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + AssertEqual(t, http.StatusOK, w.Code) var body map[string]string - err = json.Unmarshal(w.Body.Bytes(), &body) - require.NoError(t, err) - assert.Equal(t, "/", body["path"]) + err = coreJSONUnmarshal(w.Body.Bytes(), &body) + RequireNoError(t, err) + AssertEqual(t, "/", body[`path`]) } -func TestProxyProvider_HealthPassthrough_Good(t *testing.T) { +func TestProxyProviderHealthPassthrough(t *T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { w.Header().Set("Content-Type", "application/json") @@ -168,7 +164,7 @@ func TestProxyProvider_HealthPassthrough_Good(t *testing.T) { }) engine, err := api.New() - require.NoError(t, err) + RequireNoError(t, err) engine.Register(p) handler := engine.Handler() @@ -177,11 +173,11 @@ func TestProxyProvider_HealthPassthrough_Good(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, w.Body.String(), `"status":"ok"`) + AssertEqual(t, http.StatusOK, w.Code) + AssertContains(t, w.Body.String(), `"status":"ok"`) } -func TestProxyProvider_Renderable_Good(t *testing.T) { +func TestProxyProvider_Renderable_Good(t *T) { // Verify ProxyProvider satisfies Renderable via the Registry. p := provider.NewProxy(provider.ProxyConfig{ Name: "renderable-proxy", @@ -194,22 +190,22 @@ func TestProxyProvider_Renderable_Good(t *testing.T) { reg.Add(p) renderables := reg.Renderable() - require.Len(t, renderables, 1) - assert.Equal(t, "core-test-panel", renderables[0].Element().Tag) + AssertLen(t, renderables, 1) + AssertEqual(t, "core-test-panel", renderables[0].Element().Tag) } -func TestProxyProvider_Ugly_InvalidUpstream(t *testing.T) { +func TestProxyProvider_Ugly_InvalidUpstream(t *T) { p := provider.NewProxy(provider.ProxyConfig{ Name: "bad", BasePath: "/api/v1/bad", Upstream: "://not-a-url", }) - require.NotNil(t, p) - assert.Error(t, p.Err()) + AssertNotNil(t, p) + AssertError(t, p.Err()) engine, err := api.New() - require.NoError(t, err) + RequireNoError(t, err) engine.Register(p) handler := engine.Handler() @@ -218,18 +214,18 @@ func TestProxyProvider_Ugly_InvalidUpstream(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - assert.Equal(t, http.StatusInternalServerError, w.Code) + AssertEqual(t, http.StatusInternalServerError, w.Code) var body map[string]any - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + RequireNoError(t, coreJSONUnmarshal(w.Body.Bytes(), &body)) - assert.Equal(t, false, body["success"]) + AssertEqual(t, false, body["success"]) errObj, ok := body["error"].(map[string]any) - require.True(t, ok) - assert.Equal(t, "invalid_provider_configuration", errObj["code"]) + RequireTrue(t, ok) + AssertEqual(t, "invalid_provider_configuration", errObj["code"]) } -func TestProxyProvider_NewProxy_Good_PublicUpstream(t *testing.T) { +func TestProxyProvider_NewProxy_Good_PublicUpstream(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "") p := provider.NewProxy(provider.ProxyConfig{ @@ -238,29 +234,32 @@ func TestProxyProvider_NewProxy_Good_PublicUpstream(t *testing.T) { Upstream: "http://1.1.1.1/x", }) - require.NotNil(t, p) - assert.NoError(t, p.Err()) + AssertNotNil(t, p) + AssertNoError(t, p.Err()) } -func TestProxyProvider_NewProxy_Bad_BlocksMetadataIP(t *testing.T) { +func TestProxyProvider_NewProxy_Bad_BlocksMetadataIP(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "") - assertProviderUpstreamBlocked(t, "http://169.254.169.254/x") + err := assertProviderUpstreamBlocked(t, "http://169.254.169.254/x") + AssertContains(t, err.Error(), "blocked") } -func TestProxyProvider_NewProxy_Bad_BlocksLoopback(t *testing.T) { +func TestProxyProvider_NewProxy_Bad_BlocksLoopback(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "") - assertProviderUpstreamBlocked(t, "http://127.0.0.1:5432/") + err := assertProviderUpstreamBlocked(t, "http://127.0.0.1:5432/") + AssertContains(t, err.Error(), "blocked") } -func TestProxyProvider_NewProxy_Bad_BlocksRFC1918(t *testing.T) { +func TestProxyProvider_NewProxy_Bad_BlocksRFC1918(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "") - assertProviderUpstreamBlocked(t, "http://10.0.0.1/x") + err := assertProviderUpstreamBlocked(t, "http://10.0.0.1/x") + AssertContains(t, err.Error(), "blocked") } -func TestProxyProvider_NewProxy_Good_AllowListPermitsLoopback(t *testing.T) { +func TestProxyProvider_NewProxy_Good_AllowListPermitsLoopback(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "127.0.0.0/8") p := provider.NewProxy(provider.ProxyConfig{ @@ -269,23 +268,25 @@ func TestProxyProvider_NewProxy_Good_AllowListPermitsLoopback(t *testing.T) { Upstream: "http://127.0.0.1:5432/", }) - require.NotNil(t, p) - assert.NoError(t, p.Err()) + AssertNotNil(t, p) + AssertNoError(t, p.Err()) } -func TestProxyProvider_NewProxy_Bad_AllowListDoesNotPermitOtherPrivateCIDRs(t *testing.T) { +func TestProxyProvider_NewProxy_Bad_AllowListDoesNotPermitOtherPrivateCIDRs(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "127.0.0.0/8") - assertProviderUpstreamBlocked(t, "http://10.0.0.1/") + err := assertProviderUpstreamBlocked(t, "http://10.0.0.1/") + AssertContains(t, err.Error(), "blocked") } -func TestProxyProvider_NewProxy_Bad_BlocksHostnameResolvingToLoopback(t *testing.T) { +func TestProxyProvider_NewProxy_Bad_BlocksHostnameResolvingToLoopback(t *T) { t.Setenv("CORE_PROVIDER_UPSTREAM_ALLOW", "") - assertProviderUpstreamBlocked(t, "http://localhost:5432/") + err := assertProviderUpstreamBlocked(t, "http://localhost:5432/") + AssertContains(t, err.Error(), "blocked") } -func assertProviderUpstreamBlocked(t *testing.T, upstream string) { +func assertProviderUpstreamBlocked(t *T, upstream string) error { t.Helper() p := provider.NewProxy(provider.ProxyConfig{ @@ -294,13 +295,14 @@ func assertProviderUpstreamBlocked(t *testing.T, upstream string) { Upstream: upstream, }) - require.NotNil(t, p) + AssertNotNil(t, p) err := p.Err() - require.Error(t, err) - assert.True(t, errors.Is(err, provider.ErrProviderUpstreamBlocked), "expected ErrProviderUpstreamBlocked, got %v", err) + AssertError(t, err) + AssertTrue(t, Is(err, provider.ErrProviderUpstreamBlocked), "expected ErrProviderUpstreamBlocked") var blocked *provider.ProviderUpstreamBlockedError - require.True(t, errors.As(err, &blocked), "expected ProviderUpstreamBlockedError, got %T", err) - assert.Equal(t, upstream, blocked.Upstream) - assert.NotEmpty(t, blocked.Reason) + RequireTrue(t, As(err, &blocked), "expected ProviderUpstreamBlockedError") + AssertEqual(t, upstream, blocked.Upstream) + AssertNotEmpty(t, blocked.Reason) + return err } diff --git a/pkg/provider/registry.go b/go/pkg/provider/registry.go similarity index 99% rename from pkg/provider/registry.go rename to go/pkg/provider/registry.go index 6687354..783a1c5 100644 --- a/pkg/provider/registry.go +++ b/go/pkg/provider/registry.go @@ -6,8 +6,8 @@ import ( "iter" "slices" + core "dappco.re/go" "dappco.re/go/api" - core "dappco.re/go/core" ) // Registry collects providers and mounts them on an api.Engine. diff --git a/go/pkg/provider/registry_example_test.go b/go/pkg/provider/registry_example_test.go new file mode 100644 index 0000000..315673b --- /dev/null +++ b/go/pkg/provider/registry_example_test.go @@ -0,0 +1,834 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider + +import coretest "dappco.re/go" + +func TestRegistry_NewRegistry_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewRegistry() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_NewRegistry_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewRegistry() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_NewRegistry_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewRegistry() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Add_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.Add(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Add_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.Add(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Add_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.Add(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_MountAll_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.MountAll(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_MountAll_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.MountAll(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_MountAll_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + subject.MountAll(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_List_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.List() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_List_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.List() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_List_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.List() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Iter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Iter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Iter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Iter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Iter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Iter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Len_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Len() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Len_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Len() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Len_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Len() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Get_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Get("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Get_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Get("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Get_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Get("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Streamable_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Streamable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Streamable_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Streamable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Streamable_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Streamable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_StreamableIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.StreamableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_StreamableIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.StreamableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_StreamableIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.StreamableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Describable_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Describable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Describable_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Describable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Describable_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Describable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_DescribableIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DescribableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_DescribableIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DescribableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_DescribableIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.DescribableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Renderable_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Renderable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Renderable_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Renderable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Renderable_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Renderable() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_RenderableIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.RenderableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_RenderableIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.RenderableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_RenderableIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.RenderableIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_Info_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Info() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_Info_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Info() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_Info_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.Info() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_InfoIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.InfoIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_InfoIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.InfoIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_InfoIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.InfoIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_SpecFiles_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFiles() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_SpecFiles_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFiles() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_SpecFiles_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFiles() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestRegistry_Registry_SpecFilesIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFilesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRegistry_Registry_SpecFilesIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFilesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRegistry_Registry_SpecFilesIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Registry + _ = subject.SpecFilesIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNewRegistry_registry() { + func() { + defer func() { _ = recover() }() + _ = NewRegistry() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Add_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + subject.Add(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_MountAll_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + subject.MountAll(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_List_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.List() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Iter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Iter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Len_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Len() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Get_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Get("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Streamable_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Streamable() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_StreamableIter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.StreamableIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Describable_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Describable() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_DescribableIter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.DescribableIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Renderable_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Renderable() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_RenderableIter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.RenderableIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_Info_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.Info() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_InfoIter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.InfoIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_SpecFiles_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.SpecFiles() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegistry_SpecFilesIter_registry() { + func() { + defer func() { _ = recover() }() + var subject *Registry + _ = subject.SpecFilesIter() + }() + coretest.Println("done") + // Output: done +} diff --git a/pkg/provider/registry_test.go b/go/pkg/provider/registry_test.go similarity index 65% rename from pkg/provider/registry_test.go rename to go/pkg/provider/registry_test.go index d1bdd9b..bd03cc8 100644 --- a/pkg/provider/registry_test.go +++ b/go/pkg/provider/registry_test.go @@ -3,13 +3,10 @@ package provider_test import ( - "testing" - + . "dappco.re/go" "dappco.re/go/api" "dappco.re/go/api/pkg/provider" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // -- Test helpers (minimal providers) ----------------------------------------- @@ -62,64 +59,64 @@ func (f *fullProvider) Element() provider.ElementSpec { // -- Tests -------------------------------------------------------------------- -func TestRegistry_Add_Good(t *testing.T) { +func TestRegistry_Add_Good(t *T) { reg := provider.NewRegistry() - assert.Equal(t, 0, reg.Len()) + AssertEqual(t, 0, reg.Len()) reg.Add(&stubProvider{}) - assert.Equal(t, 1, reg.Len()) + AssertEqual(t, 1, reg.Len()) reg.Add(&streamableProvider{}) - assert.Equal(t, 2, reg.Len()) + AssertEqual(t, 2, reg.Len()) } -func TestRegistry_Get_Good(t *testing.T) { +func TestRegistry_Get_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) p := reg.Get("stub") - require.NotNil(t, p) - assert.Equal(t, "stub", p.Name()) + AssertNotNil(t, p) + AssertEqual(t, "stub", p.Name()) } -func TestRegistry_Get_Bad(t *testing.T) { +func TestRegistry_Get_Bad(t *T) { reg := provider.NewRegistry() p := reg.Get("nonexistent") - assert.Nil(t, p) + AssertNil(t, p) } -func TestRegistry_List_Good(t *testing.T) { +func TestRegistry_List_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&streamableProvider{}) list := reg.List() - assert.Len(t, list, 2) + AssertLen(t, list, 2) } -func TestRegistry_MountAll_Good(t *testing.T) { +func TestRegistry_MountAll_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&streamableProvider{}) engine, err := api.New() - require.NoError(t, err) + RequireNoError(t, err) reg.MountAll(engine) - assert.Len(t, engine.Groups(), 2) + AssertLen(t, engine.Groups(), 2) } -func TestRegistry_Streamable_Good(t *testing.T) { +func TestRegistry_Streamable_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) // not streamable reg.Add(&streamableProvider{}) // streamable s := reg.Streamable() - assert.Len(t, s, 1) - assert.Equal(t, []string{"stub.event"}, s[0].Channels()) + AssertLen(t, s, 1) + AssertEqual(t, []string{"stub.event"}, s[0].Channels()) } -func TestRegistry_StreamableIter_Good(t *testing.T) { +func TestRegistry_StreamableIter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&streamableProvider{}) @@ -129,11 +126,11 @@ func TestRegistry_StreamableIter_Good(t *testing.T) { streamables = append(streamables, s) } - assert.Len(t, streamables, 1) - assert.Equal(t, []string{"stub.event"}, streamables[0].Channels()) + AssertLen(t, streamables, 1) + AssertEqual(t, []string{"stub.event"}, streamables[0].Channels()) } -func TestRegistry_StreamableIter_Good_SnapshotCurrentProviders(t *testing.T) { +func TestRegistry_StreamableIter_Good_SnapshotCurrentProviders(t *T) { reg := provider.NewRegistry() reg.Add(&streamableProvider{}) @@ -145,21 +142,21 @@ func TestRegistry_StreamableIter_Good_SnapshotCurrentProviders(t *testing.T) { streamables = append(streamables, s) } - assert.Len(t, streamables, 1) - assert.Equal(t, []string{"stub.event"}, streamables[0].Channels()) + AssertLen(t, streamables, 1) + AssertEqual(t, []string{"stub.event"}, streamables[0].Channels()) } -func TestRegistry_Describable_Good(t *testing.T) { +func TestRegistry_Describable_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) // not describable reg.Add(&describableProvider{}) // describable d := reg.Describable() - assert.Len(t, d, 1) - assert.Len(t, d[0].Describe(), 1) + AssertLen(t, d, 1) + AssertLen(t, d[0].Describe(), 1) } -func TestRegistry_DescribableIter_Good(t *testing.T) { +func TestRegistry_DescribableIter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&describableProvider{}) @@ -169,11 +166,11 @@ func TestRegistry_DescribableIter_Good(t *testing.T) { describables = append(describables, d) } - assert.Len(t, describables, 1) - assert.Len(t, describables[0].Describe(), 1) + AssertLen(t, describables, 1) + AssertLen(t, describables[0].Describe(), 1) } -func TestRegistry_DescribableIter_Good_SnapshotCurrentProviders(t *testing.T) { +func TestRegistry_DescribableIter_Good_SnapshotCurrentProviders(t *T) { reg := provider.NewRegistry() reg.Add(&describableProvider{}) @@ -185,21 +182,21 @@ func TestRegistry_DescribableIter_Good_SnapshotCurrentProviders(t *testing.T) { describables = append(describables, d) } - assert.Len(t, describables, 1) - assert.Len(t, describables[0].Describe(), 1) + AssertLen(t, describables, 1) + AssertLen(t, describables[0].Describe(), 1) } -func TestRegistry_Renderable_Good(t *testing.T) { +func TestRegistry_Renderable_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) // not renderable reg.Add(&renderableProvider{}) // renderable r := reg.Renderable() - assert.Len(t, r, 1) - assert.Equal(t, "core-stub-panel", r[0].Element().Tag) + AssertLen(t, r, 1) + AssertEqual(t, "core-stub-panel", r[0].Element().Tag) } -func TestRegistry_RenderableIter_Good(t *testing.T) { +func TestRegistry_RenderableIter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&renderableProvider{}) @@ -209,11 +206,11 @@ func TestRegistry_RenderableIter_Good(t *testing.T) { renderables = append(renderables, r) } - assert.Len(t, renderables, 1) - assert.Equal(t, "core-stub-panel", renderables[0].Element().Tag) + AssertLen(t, renderables, 1) + AssertEqual(t, "core-stub-panel", renderables[0].Element().Tag) } -func TestRegistry_RenderableIter_Good_SnapshotCurrentProviders(t *testing.T) { +func TestRegistry_RenderableIter_Good_SnapshotCurrentProviders(t *T) { reg := provider.NewRegistry() reg.Add(&renderableProvider{}) @@ -225,26 +222,26 @@ func TestRegistry_RenderableIter_Good_SnapshotCurrentProviders(t *testing.T) { renderables = append(renderables, r) } - assert.Len(t, renderables, 1) - assert.Equal(t, "core-stub-panel", renderables[0].Element().Tag) + AssertLen(t, renderables, 1) + AssertEqual(t, "core-stub-panel", renderables[0].Element().Tag) } -func TestRegistry_Info_Good(t *testing.T) { +func TestRegistry_Info_Good(t *T) { reg := provider.NewRegistry() reg.Add(&fullProvider{}) infos := reg.Info() - require.Len(t, infos, 1) + AssertLen(t, infos, 1) info := infos[0] - assert.Equal(t, "full", info.Name) - assert.Equal(t, "/api/full", info.BasePath) - assert.Equal(t, []string{"stub.event"}, info.Channels) - require.NotNil(t, info.Element) - assert.Equal(t, "core-full-panel", info.Element.Tag) + AssertEqual(t, "full", info.Name) + AssertEqual(t, "/api/full", info.BasePath) + AssertEqual(t, []string{"stub.event"}, info.Channels) + AssertNotNil(t, info.Element) + AssertEqual(t, "core-full-panel", info.Element.Tag) } -func TestRegistry_Info_Good_ProxyMetadata(t *testing.T) { +func TestRegistry_Info_Good_ProxyMetadata(t *T) { reg := provider.NewRegistry() reg.Add(provider.NewProxy(provider.ProxyConfig{ Name: "proxy", @@ -254,16 +251,16 @@ func TestRegistry_Info_Good_ProxyMetadata(t *testing.T) { })) infos := reg.Info() - require.Len(t, infos, 1) + AssertLen(t, infos, 1) info := infos[0] - assert.Equal(t, "proxy", info.Name) - assert.Equal(t, "/api/proxy", info.BasePath) - assert.Equal(t, "/tmp/proxy-openapi.json", info.SpecFile) - assert.Equal(t, "http://127.0.0.1:9999", info.Upstream) + AssertEqual(t, "proxy", info.Name) + AssertEqual(t, "/api/proxy", info.BasePath) + AssertEqual(t, "/tmp/proxy-openapi.json", info.SpecFile) + AssertEqual(t, "http://127.0.0.1:9999", info.Upstream) } -func TestRegistry_InfoIter_Good(t *testing.T) { +func TestRegistry_InfoIter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&fullProvider{}) @@ -272,16 +269,16 @@ func TestRegistry_InfoIter_Good(t *testing.T) { infos = append(infos, info) } - require.Len(t, infos, 1) + AssertLen(t, infos, 1) info := infos[0] - assert.Equal(t, "full", info.Name) - assert.Equal(t, "/api/full", info.BasePath) - assert.Equal(t, []string{"stub.event"}, info.Channels) - require.NotNil(t, info.Element) - assert.Equal(t, "core-full-panel", info.Element.Tag) + AssertEqual(t, "full", info.Name) + AssertEqual(t, "/api/full", info.BasePath) + AssertEqual(t, []string{"stub.event"}, info.Channels) + AssertNotNil(t, info.Element) + AssertEqual(t, "core-full-panel", info.Element.Tag) } -func TestRegistry_InfoIter_Good_SnapshotCurrentProviders(t *testing.T) { +func TestRegistry_InfoIter_Good_SnapshotCurrentProviders(t *T) { reg := provider.NewRegistry() reg.Add(&fullProvider{}) @@ -293,11 +290,11 @@ func TestRegistry_InfoIter_Good_SnapshotCurrentProviders(t *testing.T) { infos = append(infos, info) } - require.Len(t, infos, 1) - assert.Equal(t, "full", infos[0].Name) + AssertLen(t, infos, 1) + AssertEqual(t, "full", infos[0].Name) } -func TestRegistry_Iter_Good(t *testing.T) { +func TestRegistry_Iter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&streamableProvider{}) @@ -306,10 +303,10 @@ func TestRegistry_Iter_Good(t *testing.T) { for range reg.Iter() { count++ } - assert.Equal(t, 2, count) + AssertEqual(t, 2, count) } -func TestRegistry_SpecFiles_Good(t *testing.T) { +func TestRegistry_SpecFiles_Good(t *T) { reg := provider.NewRegistry() reg.Add(&stubProvider{}) reg.Add(&specFileProvider{specFile: "/tmp/b.json"}) @@ -317,10 +314,10 @@ func TestRegistry_SpecFiles_Good(t *testing.T) { reg.Add(&specFileProvider{specFile: "/tmp/a.yaml"}) reg.Add(&specFileProvider{specFile: ""}) - assert.Equal(t, []string{"/tmp/a.yaml", "/tmp/b.json"}, reg.SpecFiles()) + AssertEqual(t, []string{"/tmp/a.yaml", "/tmp/b.json"}, reg.SpecFiles()) } -func TestRegistry_SpecFilesIter_Good(t *testing.T) { +func TestRegistry_SpecFilesIter_Good(t *T) { reg := provider.NewRegistry() reg.Add(&specFileProvider{specFile: "/tmp/z.json"}) reg.Add(&specFileProvider{specFile: "/tmp/x.json"}) @@ -330,5 +327,5 @@ func TestRegistry_SpecFilesIter_Good(t *testing.T) { files = append(files, file) } - assert.Equal(t, []string{"/tmp/x.json", "/tmp/z.json"}, files) + AssertEqual(t, []string{"/tmp/x.json", "/tmp/z.json"}, files) } diff --git a/go/pkg/provider/test_core_helpers_test.go b/go/pkg/provider/test_core_helpers_test.go new file mode 100644 index 0000000..8bce473 --- /dev/null +++ b/go/pkg/provider/test_core_helpers_test.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package provider_test + +import ( + "syscall" + + . "dappco.re/go" +) + +func coreResultError(r Result) error { + if r.OK { + return nil + } + if err, ok := r.Value.(error); ok { + return err + } + return NewError("core operation failed") +} + +func coreJSONUnmarshal(data []byte, target any) error { + return coreResultError(JSONUnmarshal(data, target)) +} + +func coreJSONEncode(writer Writer, v any) error { + r := JSONMarshal(v) + if !r.OK { + return coreResultError(r) + } + data, _ := r.Value.([]byte) + data = append(data, '\n') + _, err := writer.Write(data) + return err +} + +func coreWriteFile(path string, data []byte, mode FileMode) error { + return coreResultError(WriteFile(path, data, mode)) +} + +func coreMkdirAll(path string, mode FileMode) error { + return coreResultError(MkdirAll(path, mode)) +} + +func coreSetenv(key, value string) error { + return coreResultError(Setenv(key, value)) +} + +func coreUnsetenv(key string) error { + return coreResultError(Unsetenv(key)) +} + +func corePathEvalSymlinks(path string) (string, error) { + r := PathEvalSymlinks(path) + if !r.OK { + return "", coreResultError(r) + } + resolved, _ := r.Value.(string) + return resolved, nil +} + +func coreSymlink(oldname, newname string) error { + return syscall.Symlink(oldname, newname) +} diff --git a/pkg/stream/stream_group.go b/go/pkg/stream/stream_group.go similarity index 99% rename from pkg/stream/stream_group.go rename to go/pkg/stream/stream_group.go index 72f0026..9001fab 100644 --- a/pkg/stream/stream_group.go +++ b/go/pkg/stream/stream_group.go @@ -5,7 +5,7 @@ package stream import ( - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/pkg/stream/stream_group_example_test.go b/go/pkg/stream/stream_group_example_test.go new file mode 100644 index 0000000..c67ff79 --- /dev/null +++ b/go/pkg/stream/stream_group_example_test.go @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package stream + +import coretest "dappco.re/go" + +func TestStreamGroup_NewGroup_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_NewGroup_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_NewGroup_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewGroup("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestStreamGroup_Group_Name_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_Group_Name_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_Group_Name_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Name() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestStreamGroup_Group_Handlers_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Handlers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_Group_Handlers_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Handlers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_Group_Handlers_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + _ = subject.Handlers() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestStreamGroup_Group_Register_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_Group_Register_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_Group_Register_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Group + subject.Register(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestStreamGroup_SSE_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SSE("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_SSE_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SSE("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_SSE_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SSE("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestStreamGroup_WebSocket_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebSocket("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestStreamGroup_WebSocket_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebSocket("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestStreamGroup_WebSocket_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebSocket("", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNewGroup_streamGroup() { + func() { + defer func() { _ = recover() }() + _ = NewGroup("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleGroup_Name_streamGroup() { + func() { + defer func() { _ = recover() }() + var subject *Group + _ = subject.Name() + }() + coretest.Println("done") + // Output: done +} + +func ExampleGroup_Handlers_streamGroup() { + func() { + defer func() { _ = recover() }() + var subject *Group + _ = subject.Handlers() + }() + coretest.Println("done") + // Output: done +} + +func ExampleGroup_Register_streamGroup() { + func() { + defer func() { _ = recover() }() + var subject *Group + subject.Register(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSE_streamGroup() { + func() { + defer func() { _ = recover() }() + _ = SSE("", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebSocket_streamGroup() { + func() { + defer func() { _ = recover() }() + _ = WebSocket("", nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/pkg/stream/stream_group_test.go b/go/pkg/stream/stream_group_test.go similarity index 100% rename from pkg/stream/stream_group_test.go rename to go/pkg/stream/stream_group_test.go diff --git a/pprof_test.go b/go/pprof_test.go similarity index 100% rename from pprof_test.go rename to go/pprof_test.go diff --git a/race_test.go b/go/race_test.go similarity index 100% rename from race_test.go rename to go/race_test.go diff --git a/ratelimit.go b/go/ratelimit.go similarity index 99% rename from ratelimit.go rename to go/ratelimit.go index c321770..57e587e 100644 --- a/ratelimit.go +++ b/go/ratelimit.go @@ -7,7 +7,7 @@ import ( "net/http" // Note: AX-6 — structural HTTP status boundary for Gin handlers; no core primitive. "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/ratelimit_example_test.go b/go/ratelimit_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/ratelimit_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/ratelimit_internal_test.go b/go/ratelimit_internal_test.go similarity index 100% rename from ratelimit_internal_test.go rename to go/ratelimit_internal_test.go diff --git a/ratelimit_test.go b/go/ratelimit_test.go similarity index 99% rename from ratelimit_test.go rename to go/ratelimit_test.go index b856d3c..2460db3 100644 --- a/ratelimit_test.go +++ b/go/ratelimit_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "sync" @@ -80,7 +79,7 @@ func TestWithRateLimit_Good_AllowsBurstThenRejects(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w3.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w3.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { diff --git a/response.go b/go/response.go similarity index 100% rename from response.go rename to go/response.go diff --git a/go/response_example_test.go b/go/response_example_test.go new file mode 100644 index 0000000..0acc147 --- /dev/null +++ b/go/response_example_test.go @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestResponse_OK_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = OK[any](nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponse_OK_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = OK[any](nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponse_OK_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = OK[any](nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponse_Fail_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Fail("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponse_Fail_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Fail("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponse_Fail_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Fail("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponse_FailWithDetails_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = FailWithDetails("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponse_FailWithDetails_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = FailWithDetails("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponse_FailWithDetails_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = FailWithDetails("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponse_Paginated_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Paginated[any](nil, 0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponse_Paginated_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Paginated[any](nil, 0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponse_Paginated_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = Paginated[any](nil, 0, 0, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponse_AttachRequestMeta_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = AttachRequestMeta[any](nil, Response[any]{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponse_AttachRequestMeta_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = AttachRequestMeta[any](nil, Response[any]{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponse_AttachRequestMeta_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = AttachRequestMeta[any](nil, Response[any]{}) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleOK_response() { + func() { + defer func() { _ = recover() }() + _ = OK[any](nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleFail_response() { + func() { + defer func() { _ = recover() }() + _ = Fail("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleFailWithDetails_response() { + func() { + defer func() { _ = recover() }() + _ = FailWithDetails("", "", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExamplePaginated_response() { + func() { + defer func() { _ = recover() }() + _ = Paginated[any](nil, 0, 0, 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleAttachRequestMeta_response() { + func() { + defer func() { _ = recover() }() + _ = AttachRequestMeta[any](nil, Response[any]{}) + }() + coretest.Println("done") + // Output: done +} diff --git a/response_meta.go b/go/response_meta.go similarity index 95% rename from response_meta.go rename to go/response_meta.go index af840ca..5273ded 100644 --- a/response_meta.go +++ b/go/response_meta.go @@ -11,7 +11,7 @@ import ( "net/http" // Note: AX-6 - structural HTTP boundary for Gin response writer contracts; no core primitive. "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -20,6 +20,8 @@ type responseMetaBodyBuffer interface { Write([]byte) (int, error) WriteString(string) (int, error) Bytes() []byte + String() string + Len() int Reset() } @@ -74,7 +76,10 @@ func (w *responseMetaRecorder) WriteHeaderNow() { w.wroteHeader = true } -func (w *responseMetaRecorder) Write(data []byte) (int, error) { +func (w *responseMetaRecorder) Write(data []byte) ( + int, + error, +) { if w.passthrough { if !w.wroteHeader { w.WriteHeader(http.StatusOK) @@ -89,7 +94,10 @@ func (w *responseMetaRecorder) Write(data []byte) (int, error) { return n, err } -func (w *responseMetaRecorder) WriteString(s string) (int, error) { +func (w *responseMetaRecorder) WriteString(s string) ( + int, + error, +) { if w.passthrough { if !w.wroteHeader { w.WriteHeader(http.StatusOK) @@ -143,7 +151,11 @@ func (w *responseMetaRecorder) Written() bool { return w.wroteHeader } -func (w *responseMetaRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (w *responseMetaRecorder) Hijack() ( + net.Conn, + *bufio.ReadWriter, + error, +) { if w.passthrough { if h, ok := w.ResponseWriter.(http.Hijacker); ok { return h.Hijack() diff --git a/go/response_meta_example_test.go b/go/response_meta_example_test.go new file mode 100644 index 0000000..b863c76 --- /dev/null +++ b/go/response_meta_example_test.go @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestResponseMeta_MetaRecorder_Header_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Header_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Header_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Header() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeader_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeader_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeader_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeader(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeaderNow_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeaderNow_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_WriteHeaderNow_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.WriteHeaderNow() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Write_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Write_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Write_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.Write(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_WriteString_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_WriteString_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_WriteString_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _ = subject.WriteString("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Flush_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Flush_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Flush_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + subject.Flush() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Status_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Status_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Status_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Status() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Size_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Size_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Size_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Size() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Written_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Written_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Written_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _ = subject.Written() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestResponseMeta_MetaRecorder_Hijack_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestResponseMeta_MetaRecorder_Hijack_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestResponseMeta_MetaRecorder_Hijack_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *responseMetaRecorder + _, _, _ = subject.Hijack() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleMetaRecorder_Header_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _ = subject.Header() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_WriteHeader_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + subject.WriteHeader(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_WriteHeaderNow_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + subject.WriteHeaderNow() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Write_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _, _ = subject.Write(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_WriteString_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _, _ = subject.WriteString("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Flush_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + subject.Flush() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Status_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _ = subject.Status() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Size_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _ = subject.Size() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Written_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _ = subject.Written() + }() + coretest.Println("done") + // Output: done +} + +func ExampleMetaRecorder_Hijack_responseMeta() { + func() { + defer func() { _ = recover() }() + var subject *responseMetaRecorder + _, _, _ = subject.Hijack() + }() + coretest.Println("done") + // Output: done +} diff --git a/response_meta_test.go b/go/response_meta_test.go similarity index 95% rename from response_meta_test.go rename to go/response_meta_test.go index f67542f..cde5ab0 100644 --- a/response_meta_test.go +++ b/go/response_meta_test.go @@ -4,9 +4,7 @@ package api import ( "bufio" - "bytes" - "encoding/json" - "errors" + core "dappco.re/go" "net" "net/http" "testing" @@ -15,7 +13,7 @@ import ( type responseMetaWriterStub struct { header http.Header status int - body bytes.Buffer + body responseMetaBodyBuffer wroteHeader bool writeHeaderNowHit bool flushed bool @@ -26,6 +24,7 @@ func newResponseMetaWriterStub() *responseMetaWriterStub { return &responseMetaWriterStub{ header: make(http.Header), status: http.StatusOK, + body: core.NewBuffer(), } } @@ -75,7 +74,7 @@ func (w *responseMetaWriterStub) Flush() { func (w *responseMetaWriterStub) Hijack() (net.Conn, *bufio.ReadWriter, error) { w.hijacked = true - return nil, nil, errors.New("hijack not supported") + return nil, nil, core.NewError("hijack not supported") } func (w *responseMetaWriterStub) CloseNotify() <-chan bool { @@ -151,12 +150,12 @@ func TestResponseMetaRecorder_Bad_RejectsNonJSONPayloads(t *testing.T) { body := []byte(`{"success":true,"meta":{"page":2,"per_page":10,"total":100}}`) updated := refreshResponseMetaBody(body, meta) - if bytes.Equal(updated, body) { + if coreBytesEqual(updated, body) { t.Fatal("expected metadata body to be updated") } var refreshed map[string]any - if err := json.Unmarshal(updated, &refreshed); err != nil { + if err := coreJSONUnmarshal(updated, &refreshed); err != nil { t.Fatalf("unmarshal failed: %v", err) } metaObj, ok := refreshed["meta"].(map[string]any) diff --git a/response_test.go b/go/response_test.go similarity index 94% rename from response_test.go rename to go/response_test.go index ad86f52..0e2f2b1 100644 --- a/response_test.go +++ b/go/response_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -58,13 +57,13 @@ func TestOK_Good_StructData(t *testing.T) { func TestOK_Good_JSONOmitsErrorAndMeta(t *testing.T) { r := api.OK("data") - b, err := json.Marshal(r) + b, err := coreJSONMarshal(r) if err != nil { t.Fatalf("marshal error: %v", err) } var raw map[string]any - if err := json.Unmarshal(b, &raw); err != nil { + if err := coreJSONUnmarshal(b, &raw); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -106,13 +105,13 @@ func TestFail_Good(t *testing.T) { func TestFail_Good_JSONOmitsData(t *testing.T) { r := api.Fail("ERR", "something went wrong") - b, err := json.Marshal(r) + b, err := coreJSONMarshal(r) if err != nil { t.Fatalf("marshal error: %v", err) } var raw map[string]any - if err := json.Unmarshal(b, &raw); err != nil { + if err := coreJSONUnmarshal(b, &raw); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -146,13 +145,13 @@ func TestFailWithDetails_Good(t *testing.T) { func TestFailWithDetails_Good_JSONIncludesDetails(t *testing.T) { r := api.FailWithDetails("ERR", "bad", "extra info") - b, err := json.Marshal(r) + b, err := coreJSONMarshal(r) if err != nil { t.Fatalf("marshal error: %v", err) } var raw map[string]any - if err := json.Unmarshal(b, &raw); err != nil { + if err := coreJSONUnmarshal(b, &raw); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -193,13 +192,13 @@ func TestPaginated_Good(t *testing.T) { func TestPaginated_Good_JSONIncludesMeta(t *testing.T) { r := api.Paginated([]int{1}, 1, 10, 50) - b, err := json.Marshal(r) + b, err := coreJSONMarshal(r) if err != nil { t.Fatalf("marshal error: %v", err) } var raw map[string]any - if err := json.Unmarshal(b, &raw); err != nil { + if err := coreJSONUnmarshal(b, &raw); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -242,7 +241,7 @@ func TestResponse_AttachRequestMeta_Good_FillsMetaFromRequestIDMiddleware(t *tes } var resp api.Response[string] - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { @@ -282,7 +281,7 @@ func TestResponse_AttachRequestMeta_Bad_ReturnsResponseUnchangedWithoutRequestMe } var resp api.Response[string] - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta != nil { @@ -315,7 +314,7 @@ func TestResponse_AttachRequestMeta_Ugly_PreservesExistingMetaFields(t *testing. } var resp api.Response[string] - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Meta == nil { diff --git a/runtime_config.go b/go/runtime_config.go similarity index 86% rename from runtime_config.go rename to go/runtime_config.go index 3f537aa..dcdff25 100644 --- a/runtime_config.go +++ b/go/runtime_config.go @@ -13,12 +13,12 @@ package api // // cfg := engine.RuntimeConfig() type RuntimeConfig struct { - Swagger SwaggerConfig - Transport TransportConfig - GraphQL GraphQLConfig - Cache CacheConfig - I18n I18nConfig - Authentik AuthentikConfig + Swagger SwaggerConfig + Transport TransportConfig + GraphQL GraphQLConfig + Cache CacheConfig + I18n I18nConfig + Authentik AuthentikConfig } // RuntimeConfig returns a stable snapshot of the engine's current runtime diff --git a/go/runtime_config_example_test.go b/go/runtime_config_example_test.go new file mode 100644 index 0000000..841cd83 --- /dev/null +++ b/go/runtime_config_example_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestRuntimeConfig_Engine_RuntimeConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.RuntimeConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestRuntimeConfig_Engine_RuntimeConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.RuntimeConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestRuntimeConfig_Engine_RuntimeConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.RuntimeConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_RuntimeConfig_runtimeConfig() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.RuntimeConfig() + }() + coretest.Println("done") + // Output: done +} diff --git a/runtime_config_test.go b/go/runtime_config_test.go similarity index 100% rename from runtime_config_test.go rename to go/runtime_config_test.go diff --git a/sdk.go b/go/sdk.go similarity index 98% rename from sdk.go rename to go/sdk.go index 79b381c..55643cb 100644 --- a/sdk.go +++ b/go/sdk.go @@ -5,7 +5,7 @@ package api import ( "net/http" // Note: AX-6 — structural HTTP status boundary for Gin handlers; no core primitive. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/sdk_example_test.go b/go/sdk_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/sdk_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/sdk_test.go b/go/sdk_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/sdk_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/secure_test.go b/go/secure_test.go similarity index 97% rename from secure_test.go rename to go/secure_test.go index d779554..8c9f906 100644 --- a/secure_test.go +++ b/go/secure_test.go @@ -3,9 +3,9 @@ package api_test import ( + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -32,10 +32,10 @@ func TestWithSecure_Good_SetsHSTSHeader(t *testing.T) { if sts == "" { t.Fatal("expected Strict-Transport-Security header to be set") } - if !strings.Contains(sts, "max-age=31536000") { + if !core.Contains(sts, "max-age=31536000") { t.Fatalf("expected max-age=31536000 in STS header, got %q", sts) } - if !strings.Contains(strings.ToLower(sts), "includesubdomains") { + if !core.Contains(core.Lower(sts), "includesubdomains") { t.Fatalf("expected includeSubdomains in STS header, got %q", sts) } } diff --git a/serve_h3.go b/go/serve_h3.go similarity index 87% rename from serve_h3.go rename to go/serve_h3.go index 792575d..9c6edb5 100644 --- a/serve_h3.go +++ b/go/serve_h3.go @@ -5,11 +5,10 @@ package api import ( "context" "crypto/tls" - "errors" "net" "net/http" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" quichttp3 "github.com/quic-go/quic-go/http3" @@ -18,13 +17,13 @@ import ( var ( // ErrHTTP3NotConfigured is returned when ServeH3 is called without // enabling HTTP/3 via WithHTTP3. - ErrHTTP3NotConfigured = errors.New("api: HTTP/3 is not configured") + ErrHTTP3NotConfigured = core.NewError("api: HTTP/3 is not configured") // ErrHTTP3TLSRequired is returned when ServeH3 is called without TLS. - ErrHTTP3TLSRequired = errors.New("api: HTTP/3 requires TLS configuration") + ErrHTTP3TLSRequired = core.NewError("api: HTTP/3 requires TLS configuration") // ErrNilContext is returned when ServeH3 is called with a nil context. - ErrNilContext = errors.New("api: context is nil") + ErrNilContext = core.NewError("api: context is nil") ) // ServeH3 starts the HTTP/3 QUIC server and blocks until the context is @@ -33,7 +32,9 @@ var ( // ServeH3 is intentionally separate from Serve so callers can run the QUIC // listener alongside their existing HTTP/1.1+2 server with an explicit TLS // configuration. -func (e *Engine) ServeH3(ctx context.Context, tlsConfig *tls.Config) error { +func (e *Engine) ServeH3(ctx context.Context, tlsConfig *tls.Config) ( + _ error, +) { if e == nil || !e.http3Enabled { return ErrHTTP3NotConfigured } @@ -53,7 +54,7 @@ func (e *Engine) ServeH3(ctx context.Context, tlsConfig *tls.Config) error { errCh := make(chan error, 1) go func() { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := srv.ListenAndServe(); err != nil && !core.Is(err, http.ErrServerClosed) { errCh <- err } close(errCh) diff --git a/go/serve_h3_example_test.go b/go/serve_h3_example_test.go new file mode 100644 index 0000000..da3ad28 --- /dev/null +++ b/go/serve_h3_example_test.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func ExampleEngine_ServeH3_serveH3() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.ServeH3(nil, nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/go/serve_h3_test.go b/go/serve_h3_test.go new file mode 100644 index 0000000..7c8474a --- /dev/null +++ b/go/serve_h3_test.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestServeH3_Engine_ServeH3_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ServeH3(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestServeH3_Engine_ServeH3_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ServeH3(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestServeH3_Engine_ServeH3_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.ServeH3(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} diff --git a/servers.go b/go/servers.go similarity index 98% rename from servers.go rename to go/servers.go index f9a2224..fd2a778 100644 --- a/servers.go +++ b/go/servers.go @@ -3,7 +3,7 @@ package api import ( - core "dappco.re/go/core" + core "dappco.re/go" ) // normaliseServers trims whitespace, removes empty entries, and preserves diff --git a/go/servers_example_test.go b/go/servers_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/servers_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/servers_test.go b/go/servers_test.go similarity index 100% rename from servers_test.go rename to go/servers_test.go diff --git a/sessions_test.go b/go/sessions_test.go similarity index 97% rename from sessions_test.go rename to go/sessions_test.go index 750597e..7265777 100644 --- a/sessions_test.go +++ b/go/sessions_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -103,7 +102,7 @@ func TestWithSessions_Good_SessionPersistsAcrossRequests(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w2.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w2.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } @@ -128,7 +127,7 @@ func TestWithSessions_Good_EmptySessionReturnsNil(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } diff --git a/slog_test.go b/go/slog_test.go similarity index 83% rename from slog_test.go rename to go/slog_test.go index 4751997..ac53de3 100644 --- a/slog_test.go +++ b/go/slog_test.go @@ -3,7 +3,7 @@ package api_test import ( - "bytes" + core "dappco.re/go" "log/slog" "net/http" "net/http/httptest" @@ -19,8 +19,8 @@ import ( func TestWithSlog_Good_LogsRequestFields(t *testing.T) { gin.SetMode(gin.TestMode) - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) + buf := core.NewBuffer() + logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})) e, _ := api.New(api.WithSlog(logger)) e.Register(&stubGroup{}) @@ -40,8 +40,8 @@ func TestWithSlog_Good_LogsRequestFields(t *testing.T) { } // The structured log should contain request fields. - for _, field := range []string{"status", "method", "path", "latency", "ip"} { - if !bytes.Contains(buf.Bytes(), []byte(field)) { + for _, field := range []string{"status", "method", `path`, "latency", "ip"} { + if !core.Contains(buf.String(), field) { t.Errorf("expected log output to contain field %q, got: %s", field, output) } } @@ -66,8 +66,8 @@ func TestWithSlog_Good_NilLoggerUsesDefault(t *testing.T) { func TestWithSlog_Good_CombinesWithOtherMiddleware(t *testing.T) { gin.SetMode(gin.TestMode) - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, nil)) + buf := core.NewBuffer() + logger := slog.New(slog.NewJSONHandler(buf, nil)) e, _ := api.New( api.WithSlog(logger), @@ -95,8 +95,8 @@ func TestWithSlog_Good_CombinesWithOtherMiddleware(t *testing.T) { func TestWithSlog_Good_Logs404Status(t *testing.T) { gin.SetMode(gin.TestMode) - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, nil)) + buf := core.NewBuffer() + logger := slog.New(slog.NewJSONHandler(buf, nil)) e, _ := api.New(api.WithSlog(logger)) @@ -115,7 +115,7 @@ func TestWithSlog_Good_Logs404Status(t *testing.T) { } // Should contain the 404 status. - if !bytes.Contains(buf.Bytes(), []byte("404")) { + if !core.Contains(buf.String(), "404") { t.Errorf("expected log to contain status 404, got: %s", output) } } @@ -124,8 +124,8 @@ func TestWithSlog_Bad_LogsMethodAndPath(t *testing.T) { // Verifies POST method and custom path appear in log output. gin.SetMode(gin.TestMode) - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, nil)) + buf := core.NewBuffer() + logger := slog.New(slog.NewJSONHandler(buf, nil)) e, _ := api.New(api.WithSlog(logger)) e.Register(&stubGroup{}) @@ -136,10 +136,10 @@ func TestWithSlog_Bad_LogsMethodAndPath(t *testing.T) { h.ServeHTTP(w, req) output := buf.String() - if !bytes.Contains(buf.Bytes(), []byte("POST")) { + if !core.Contains(buf.String(), "POST") { t.Errorf("expected log to contain method POST, got: %s", output) } - if !bytes.Contains(buf.Bytes(), []byte("/stub/ping")) { + if !core.Contains(buf.String(), "/stub/ping") { t.Errorf("expected log to contain path /stub/ping, got: %s", output) } } @@ -148,8 +148,8 @@ func TestWithSlog_Ugly_DoubleSlogDoesNotPanic(t *testing.T) { // Applying WithSlog twice should not panic. gin.SetMode(gin.TestMode) - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, nil)) + buf := core.NewBuffer() + logger := slog.New(slog.NewJSONHandler(buf, nil)) e, _ := api.New( api.WithSlog(logger), diff --git a/spec_builder_helper.go b/go/spec_builder_helper.go similarity index 99% rename from spec_builder_helper.go rename to go/spec_builder_helper.go index 77a9d65..a124fa3 100644 --- a/spec_builder_helper.go +++ b/go/spec_builder_helper.go @@ -7,7 +7,7 @@ import ( "reflect" "slices" - core "dappco.re/go/core" + core "dappco.re/go" ) // SwaggerConfig captures the configured Swagger/OpenAPI metadata for an Engine. diff --git a/go/spec_builder_helper_example_test.go b/go/spec_builder_helper_example_test.go new file mode 100644 index 0000000..2113314 --- /dev/null +++ b/go/spec_builder_helper_example_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSpecBuilderHelper_Engine_OpenAPISpecBuilder_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.OpenAPISpecBuilder() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecBuilderHelper_Engine_OpenAPISpecBuilder_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.OpenAPISpecBuilder() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecBuilderHelper_Engine_OpenAPISpecBuilder_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.OpenAPISpecBuilder() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecBuilderHelper_Engine_SwaggerConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.SwaggerConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecBuilderHelper_Engine_SwaggerConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.SwaggerConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecBuilderHelper_Engine_SwaggerConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.SwaggerConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_OpenAPISpecBuilder_specBuilderHelper() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.OpenAPISpecBuilder() + }() + coretest.Println("done") + // Output: done +} + +func ExampleEngine_SwaggerConfig_specBuilderHelper() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.SwaggerConfig() + }() + coretest.Println("done") + // Output: done +} diff --git a/spec_builder_helper_internal_test.go b/go/spec_builder_helper_internal_test.go similarity index 100% rename from spec_builder_helper_internal_test.go rename to go/spec_builder_helper_internal_test.go diff --git a/spec_builder_helper_test.go b/go/spec_builder_helper_test.go similarity index 98% rename from spec_builder_helper_test.go rename to go/spec_builder_helper_test.go index 142f363..9b695c0 100644 --- a/spec_builder_helper_test.go +++ b/go/spec_builder_helper_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "testing" "time" @@ -64,7 +63,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -409,7 +408,7 @@ func TestEngine_Good_SwaggerConfigTrimsRuntimeMetadata(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -653,7 +652,7 @@ func TestEngine_Good_OpenAPISpecBuilderExportsDefaultSwaggerPath(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -677,7 +676,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesExplicitSwaggerPathWithoutUI(t *te } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -701,7 +700,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesConfiguredWSPathWithoutHandler(t * } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -725,7 +724,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesConfiguredSSEPathWithoutBroker(t * } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -768,7 +767,7 @@ func TestEngine_Good_OpenAPISpecBuilderClonesSecuritySchemes(t *testing.T) { } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -815,7 +814,7 @@ func TestEngine_Ugly_OpenAPISpecBuilderSkipsBlankSecuritySchemeEntries(t *testin } var spec map[string]any - if err := json.Unmarshal(data, &spec); err != nil { + if err := coreJSONUnmarshal(data, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) } diff --git a/spec_registry.go b/go/spec_registry.go similarity index 99% rename from spec_registry.go rename to go/spec_registry.go index aa373ad..3a5134f 100644 --- a/spec_registry.go +++ b/go/spec_registry.go @@ -6,7 +6,7 @@ import ( "iter" "slices" - core "dappco.re/go/core" + core "dappco.re/go" ) // specRegistry stores RouteGroups that should be included in CLI-generated diff --git a/go/spec_registry_example_test.go b/go/spec_registry_example_test.go new file mode 100644 index 0000000..b17714a --- /dev/null +++ b/go/spec_registry_example_test.go @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSpecRegistry_RegisterSpecGroups_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_RegisterSpecGroups_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_RegisterSpecGroups_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecRegistry_RegisterSpecGroupsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_RegisterSpecGroupsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_RegisterSpecGroupsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + RegisterSpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecRegistry_RegisteredSpecGroups_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_RegisteredSpecGroups_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_RegisteredSpecGroups_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecRegistry_RegisteredSpecGroupsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_RegisteredSpecGroupsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_RegisteredSpecGroupsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RegisteredSpecGroupsIter() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecRegistry_SpecGroupsIter_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_SpecGroupsIter_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_SpecGroupsIter_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = SpecGroupsIter(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSpecRegistry_ResetSpecGroups_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + ResetSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSpecRegistry_ResetSpecGroups_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + ResetSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSpecRegistry_ResetSpecGroups_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + ResetSpecGroups() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleRegisterSpecGroups_specRegistry() { + func() { + defer func() { _ = recover() }() + RegisterSpecGroups() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegisterSpecGroupsIter_specRegistry() { + func() { + defer func() { _ = recover() }() + RegisterSpecGroupsIter(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegisteredSpecGroups_specRegistry() { + func() { + defer func() { _ = recover() }() + _ = RegisteredSpecGroups() + }() + coretest.Println("done") + // Output: done +} + +func ExampleRegisteredSpecGroupsIter_specRegistry() { + func() { + defer func() { _ = recover() }() + _ = RegisteredSpecGroupsIter() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSpecGroupsIter_specRegistry() { + func() { + defer func() { _ = recover() }() + _ = SpecGroupsIter(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleResetSpecGroups_specRegistry() { + func() { + defer func() { _ = recover() }() + ResetSpecGroups() + }() + coretest.Println("done") + // Output: done +} diff --git a/spec_registry_test.go b/go/spec_registry_test.go similarity index 100% rename from spec_registry_test.go rename to go/spec_registry_test.go diff --git a/sse.go b/go/sse.go similarity index 99% rename from sse.go rename to go/sse.go index 5024a71..938efe4 100644 --- a/sse.go +++ b/go/sse.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/sse_example_test.go b/go/sse_example_test.go new file mode 100644 index 0000000..eb9d24c --- /dev/null +++ b/go/sse_example_test.go @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSse_NewSSEBroker_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEBroker() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSse_NewSSEBroker_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEBroker() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSse_NewSSEBroker_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEBroker() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSse_SSEBroker_Publish_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Publish("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSse_SSEBroker_Publish_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Publish("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSse_SSEBroker_Publish_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Publish("", "", nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSse_SSEBroker_Handler_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSse_SSEBroker_Handler_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSse_SSEBroker_Handler_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.Handler() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSse_SSEBroker_ClientCount_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.ClientCount() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSse_SSEBroker_ClientCount_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.ClientCount() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSse_SSEBroker_ClientCount_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + _ = subject.ClientCount() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSse_SSEBroker_Drain_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Drain() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSse_SSEBroker_Drain_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Drain() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSse_SSEBroker_Drain_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEBroker + subject.Drain() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleNewSSEBroker_sse() { + func() { + defer func() { _ = recover() }() + _ = NewSSEBroker() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEBroker_Publish_sse() { + func() { + defer func() { _ = recover() }() + var subject *SSEBroker + subject.Publish("", "", nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEBroker_Handler_sse() { + func() { + defer func() { _ = recover() }() + var subject *SSEBroker + _ = subject.Handler() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEBroker_ClientCount_sse() { + func() { + defer func() { _ = recover() }() + var subject *SSEBroker + _ = subject.ClientCount() + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEBroker_Drain_sse() { + func() { + defer func() { _ = recover() }() + var subject *SSEBroker + subject.Drain() + }() + coretest.Println("done") + // Output: done +} diff --git a/sse_internal_test.go b/go/sse_internal_test.go similarity index 100% rename from sse_internal_test.go rename to go/sse_internal_test.go diff --git a/sse_test.go b/go/sse_test.go similarity index 95% rename from sse_test.go rename to go/sse_test.go index 6e5313a..dc0ddf7 100644 --- a/sse_test.go +++ b/go/sse_test.go @@ -5,10 +5,10 @@ package api_test import ( "bufio" "context" + core "dappco.re/go" "net" "net/http" "net/http/httptest" - "strings" "sync" "testing" "time" @@ -43,7 +43,7 @@ func TestWithSSE_Good_EndpointExists(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "text/event-stream") { + if !core.HasPrefix(ct, "text/event-stream") { t.Fatalf("expected Content-Type starting with text/event-stream, got %q", ct) } } @@ -71,7 +71,7 @@ func TestWithSSE_Good_LegacyVersionedPathExistsByDefault(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "text/event-stream") { + if !core.HasPrefix(ct, "text/event-stream") { t.Fatalf("expected Content-Type starting with text/event-stream, got %q", ct) } } @@ -99,7 +99,7 @@ func TestWithSSE_Good_CustomPath(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "text/event-stream") { + if !core.HasPrefix(ct, "text/event-stream") { t.Fatalf("expected Content-Type starting with text/event-stream, got %q", ct) } @@ -205,10 +205,10 @@ func TestWithSSE_Good_ReceivesPublishedEvent(t *testing.T) { defer close(done) for scanner.Scan() { line := scanner.Text() - if after, ok := strings.CutPrefix(line, "event: "); ok { + if after, ok := coreCutPrefix(line, "event: "); ok { eventLine = after } - if after, ok := strings.CutPrefix(line, "data: "); ok { + if after, ok := coreCutPrefix(line, "data: "); ok { dataLine = after return } @@ -224,7 +224,7 @@ func TestWithSSE_Good_ReceivesPublishedEvent(t *testing.T) { if eventLine != "greeting" { t.Fatalf("expected event=%q, got %q", "greeting", eventLine) } - if !strings.Contains(dataLine, `"msg":"hello"`) { + if !core.Contains(dataLine, `"msg":"hello"`) { t.Fatalf("expected data containing msg:hello, got %q", dataLine) } } @@ -268,7 +268,7 @@ func TestWithSSE_Good_ChannelFiltering(t *testing.T) { defer close(done) for scanner.Scan() { line := scanner.Text() - if after, ok := strings.CutPrefix(line, "event: "); ok { + if after, ok := coreCutPrefix(line, "event: "); ok { eventLine = after // Read past the data and blank line. scanner.Scan() // data line @@ -321,7 +321,7 @@ func TestWithSSE_Good_CombinesWithOtherMiddleware(t *testing.T) { } ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "text/event-stream") { + if !core.HasPrefix(ct, "text/event-stream") { t.Fatalf("expected Content-Type starting with text/event-stream, got %q", ct) } } @@ -348,7 +348,7 @@ func TestWithSSE_Good_WithResponseMetaStillStreamsEvents(t *testing.T) { } defer resp.Body.Close() - if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") { + if ct := resp.Header.Get("Content-Type"); !core.HasPrefix(ct, "text/event-stream") { t.Fatalf("expected Content-Type starting with text/event-stream, got %q", ct) } if reqID := resp.Header.Get("X-Request-ID"); reqID == "" { @@ -369,7 +369,7 @@ func TestWithSSE_Good_WithResponseMetaStillStreamsEvents(t *testing.T) { defer close(done) for scanner.Scan() { line := scanner.Text() - if after, ok := strings.CutPrefix(line, "event: "); ok { + if after, ok := coreCutPrefix(line, "event: "); ok { eventLine = after return } @@ -431,7 +431,7 @@ func TestWithSSE_Good_MultipleClients(t *testing.T) { go func() { for scanner.Scan() { line := scanner.Text() - if after, ok := strings.CutPrefix(line, "event: "); ok { + if after, ok := coreCutPrefix(line, "event: "); ok { done <- after return } diff --git a/ssrf_guard.go b/go/ssrf_guard.go similarity index 91% rename from ssrf_guard.go rename to go/ssrf_guard.go index 8f6e30a..3b70c71 100644 --- a/ssrf_guard.go +++ b/go/ssrf_guard.go @@ -9,7 +9,7 @@ import ( // Note: AX-6 — URL parsing is structural for SSRF scheme and host extraction before outbound requests. "net/url" - core "dappco.re/go/core" + core "dappco.re/go" coreerr "dappco.re/go/log" ) @@ -33,7 +33,7 @@ import ( // the request fires, the literal host has been re-resolved. // errOutboundURLBlocked is returned when validateOutboundURL rejects a URL. -// Callers see a wrapped error from client.Do; tests assert on errors.Is. +// Callers see a wrapped error from client.Do; tests assert on core.Is. var errOutboundURLBlocked = coreerr.E("", "outbound URL blocked by SSRF guard", nil) // allowedSchemes is the deny-by-default scheme allowlist for outbound HTTP. @@ -68,7 +68,9 @@ var resolveHost = net.LookupIP // // Pass empty rawURL is rejected. Caller should never call client.Do with // an unvalidated URL. -func validateOutboundURL(rawURL string) error { +func validateOutboundURL(rawURL string) ( + _ error, +) { if rawURL == "" { return wrapBlocked("empty URL") } @@ -142,8 +144,10 @@ func blockedIPReason(ip net.IP) string { } // wrapBlocked formats a rejection reason as an error wrapping errOutboundURLBlocked -// so callers can errors.Is(err, errOutboundURLBlocked) on the rejection class. -func wrapBlocked(reason string) error { +// so callers can core.Is(err, errOutboundURLBlocked) on the rejection class. +func wrapBlocked(reason string) ( + _ error, +) { return blockedURLError{reason: reason} } @@ -155,8 +159,12 @@ type blockedURLError struct{ reason string } // _ = blockedURLError{reason: "metadata host"}.Error() func (e blockedURLError) Error() string { return errOutboundURLBlocked.Error() + ": " + e.reason } -// Unwrap returns errOutboundURLBlocked so errors.Is works on the rejection +// Unwrap returns errOutboundURLBlocked so core.Is works on the rejection // class regardless of the specific reason text. // -// errors.Is(err, errOutboundURLBlocked) -func (e blockedURLError) Unwrap() error { return errOutboundURLBlocked } +// core.Is(err, errOutboundURLBlocked) +func (e blockedURLError) Unwrap() ( + _ error, +) { + return errOutboundURLBlocked +} diff --git a/go/ssrf_guard_example_test.go b/go/ssrf_guard_example_test.go new file mode 100644 index 0000000..d44e2c5 --- /dev/null +++ b/go/ssrf_guard_example_test.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func ExampleURLError_Error_ssrfGuard() { + func() { + defer func() { _ = recover() }() + var subject blockedURLError + _ = subject.Error() + }() + coretest.Println("done") + // Output: done +} + +func ExampleURLError_Unwrap_ssrfGuard() { + func() { + defer func() { _ = recover() }() + var subject blockedURLError + _ = subject.Unwrap() + }() + coretest.Println("done") + // Output: done +} diff --git a/ssrf_guard_internal_test.go b/go/ssrf_guard_internal_test.go similarity index 68% rename from ssrf_guard_internal_test.go rename to go/ssrf_guard_internal_test.go index f00772f..535da0a 100644 --- a/ssrf_guard_internal_test.go +++ b/go/ssrf_guard_internal_test.go @@ -3,16 +3,15 @@ package api import ( - "errors" + core "dappco.re/go" "net" - "strings" "testing" ) -// TestSSRF_OutboundURL_BlocksMetadata_Ugly — Cerberus mechanism review +// TestSSRFBlocksMetadata — Cerberus mechanism review // recommendation per Mantis #318. AWS/GCP/Azure metadata endpoints must be // rejected by literal-host match before DNS resolution. -func TestSSRF_OutboundURL_BlocksMetadata_Ugly(t *testing.T) { +func TestSSRFBlocksMetadata(t *testing.T) { cases := []string{ "http://169.254.169.254/latest/meta-data/iam/security-credentials/", "https://metadata.google.internal/computeMetadata/v1/instance/", @@ -26,15 +25,15 @@ func TestSSRF_OutboundURL_BlocksMetadata_Ugly(t *testing.T) { t.Errorf("validateOutboundURL(%q) returned nil; expected block", raw) return } - if !errors.Is(err, errOutboundURLBlocked) { - t.Errorf("expected errors.Is(err, errOutboundURLBlocked) for %q; got %v", raw, err) + if !core.Is(err, errOutboundURLBlocked) { + t.Errorf("expected core.Is(err, errOutboundURLBlocked) for %q; got %v", raw, err) } }) } } -// TestSSRF_OutboundURL_BlocksLoopback_Ugly — localhost variants. -func TestSSRF_OutboundURL_BlocksLoopback_Ugly(t *testing.T) { +// TestSSRFBlocksLoopback — localhost variants. +func TestSSRFBlocksLoopback(t *testing.T) { cases := []string{ "http://127.0.0.1/", "http://127.5.5.5/", @@ -47,15 +46,15 @@ func TestSSRF_OutboundURL_BlocksLoopback_Ugly(t *testing.T) { t.Errorf("validateOutboundURL(%q) returned nil; expected loopback block", raw) return } - if !errors.Is(err, errOutboundURLBlocked) { - t.Errorf("expected errors.Is(err, errOutboundURLBlocked); got %v", err) + if !core.Is(err, errOutboundURLBlocked) { + t.Errorf("expected core.Is(err, errOutboundURLBlocked); got %v", err) } }) } } -// TestSSRF_OutboundURL_BlocksRFC1918_Ugly — internal-network IP ranges. -func TestSSRF_OutboundURL_BlocksRFC1918_Ugly(t *testing.T) { +// TestSSRFBlocksRFC1918 — internal-network IP ranges. +func TestSSRFBlocksRFC1918(t *testing.T) { cases := []string{ "http://10.0.0.1/", "http://10.255.255.255/", @@ -72,15 +71,15 @@ func TestSSRF_OutboundURL_BlocksRFC1918_Ugly(t *testing.T) { t.Errorf("validateOutboundURL(%q) returned nil; expected RFC1918/ULA block", raw) return } - if !errors.Is(err, errOutboundURLBlocked) { - t.Errorf("expected errors.Is(err, errOutboundURLBlocked); got %v", err) + if !core.Is(err, errOutboundURLBlocked) { + t.Errorf("expected core.Is(err, errOutboundURLBlocked); got %v", err) } }) } } -// TestSSRF_OutboundURL_BlocksDisallowedScheme_Bad — non-http(s) schemes. -func TestSSRF_OutboundURL_BlocksDisallowedScheme_Bad(t *testing.T) { +// TestSSRFBlocksDisallowedScheme — non-http(s) schemes. +func TestSSRFBlocksDisallowedScheme(t *testing.T) { cases := []string{ "file:///etc/passwd", "gopher://evil.example.com/_command", @@ -95,16 +94,16 @@ func TestSSRF_OutboundURL_BlocksDisallowedScheme_Bad(t *testing.T) { t.Errorf("validateOutboundURL(%q) returned nil; expected scheme block", raw) return } - if !strings.Contains(err.Error(), "disallowed scheme") { + if !core.Contains(err.Error(), "disallowed scheme") { t.Errorf("expected 'disallowed scheme' error; got %v", err) } }) } } -// TestSSRF_OutboundURL_BlocksEmbeddedCredentials_Bad — URL userinfo can leak +// TestSSRFBlocksEmbeddedCredentials — URL userinfo can leak // into logs/proxies and is rejected at the outbound boundary. -func TestSSRF_OutboundURL_BlocksEmbeddedCredentials_Bad(t *testing.T) { +func TestSSRFBlocksEmbeddedCredentials(t *testing.T) { badCases := []string{ "https://user:pass@example.com/path", "https://user@example.com/path", @@ -116,10 +115,10 @@ func TestSSRF_OutboundURL_BlocksEmbeddedCredentials_Bad(t *testing.T) { t.Errorf("validateOutboundURL(%q) returned nil; expected credential block", raw) return } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Errorf("expected errOutboundURLBlocked; got %v", err) } - if !strings.Contains(err.Error(), "URL contains embedded credentials") { + if !core.Contains(err.Error(), "URL contains embedded credentials") { t.Errorf("expected embedded credentials error; got %v", err) } }) @@ -135,9 +134,9 @@ func TestSSRF_OutboundURL_BlocksEmbeddedCredentials_Bad(t *testing.T) { } } -// TestSSRF_OutboundURL_AllowsHTTPS_Good — sanity that public HTTPS still works. +// TestSSRFAllowsHTTPS — sanity that public HTTPS still works. // We override resolveHost to return a public IP so we don't depend on real DNS. -func TestSSRF_OutboundURL_AllowsHTTPS_Good(t *testing.T) { +func TestSSRFAllowsHTTPS(t *testing.T) { prev := resolveHost defer func() { resolveHost = prev }() resolveHost = func(host string) ([]net.IP, error) { @@ -159,10 +158,10 @@ func TestSSRF_OutboundURL_AllowsHTTPS_Good(t *testing.T) { } } -// TestSSRF_OutboundURL_BlocksDNSResolveToPrivate_Ugly — DNS-rebinding-style: +// TestSSRFBlocksDNSResolveToPrivate — DNS-rebinding-style: // a public-looking hostname that resolves to an RFC1918 IP must still be // blocked by the post-resolution check. -func TestSSRF_OutboundURL_BlocksDNSResolveToPrivate_Ugly(t *testing.T) { +func TestSSRFBlocksDNSResolveToPrivate(t *testing.T) { prev := resolveHost defer func() { resolveHost = prev }() resolveHost = func(host string) ([]net.IP, error) { @@ -174,49 +173,49 @@ func TestSSRF_OutboundURL_BlocksDNSResolveToPrivate_Ugly(t *testing.T) { if err == nil { t.Fatal("expected post-resolution private-IP block; got nil") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Errorf("expected errOutboundURLBlocked; got %v", err) } - if !strings.Contains(err.Error(), "10.0.0.1") { + if !core.Contains(err.Error(), "10.0.0.1") { t.Errorf("expected error to mention resolved IP; got %v", err) } } -// TestSSRF_OutboundURL_EmptyURL_Bad — defensive case. -func TestSSRF_OutboundURL_EmptyURL_Bad(t *testing.T) { +// TestSSRFEmptyURL — defensive case. +func TestSSRFEmptyURL(t *testing.T) { err := validateOutboundURL("") if err == nil { t.Fatal("expected empty-URL block; got nil") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Errorf("expected errOutboundURLBlocked; got %v", err) } } -// TestSSRF_OutboundURL_BlocksResolverFailure_Bad — DNS resolution failure must +// TestSSRFBlocksResolverFailure — DNS resolution failure must // fail closed so split-resolver mismatches cannot bypass the IP blocklist. -func TestSSRF_OutboundURL_BlocksResolverFailure_Bad(t *testing.T) { +func TestSSRFBlocksResolverFailure(t *testing.T) { prev := resolveHost defer func() { resolveHost = prev }() resolveHost = func(host string) ([]net.IP, error) { - return nil, errors.New("DNS failure") + return nil, core.NewError("DNS failure") } err := validateOutboundURL("https://nonexistent.example.invalid/") if err == nil { t.Fatal("expected resolver failure to block; got nil") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Errorf("expected errOutboundURLBlocked; got %v", err) } - if !strings.Contains(err.Error(), "DNS failure") { + if !core.Contains(err.Error(), "DNS failure") { t.Errorf("expected error to mention DNS failure; got %v", err) } } -// TestSSRF_OutboundURL_BlocksEmptyResolverResult_Bad — an empty DNS answer is +// TestSSRFBlocksEmptyResolverResult — an empty DNS answer is // equivalent to no usable IP for SSRF validation and must fail closed. -func TestSSRF_OutboundURL_BlocksEmptyResolverResult_Bad(t *testing.T) { +func TestSSRFBlocksEmptyResolverResult(t *testing.T) { prev := resolveHost defer func() { resolveHost = prev }() resolveHost = func(host string) ([]net.IP, error) { @@ -227,10 +226,10 @@ func TestSSRF_OutboundURL_BlocksEmptyResolverResult_Bad(t *testing.T) { if err == nil { t.Fatal("expected empty resolver result to block; got nil") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Errorf("expected errOutboundURLBlocked; got %v", err) } - if !strings.Contains(err.Error(), "no IPs") { + if !core.Contains(err.Error(), "no IPs") { t.Errorf("expected error to mention empty DNS result; got %v", err) } } diff --git a/go/ssrf_guard_test.go b/go/ssrf_guard_test.go new file mode 100644 index 0000000..0930726 --- /dev/null +++ b/go/ssrf_guard_test.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSsrfGuard_URLError_Error_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSsrfGuard_URLError_Error_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSsrfGuard_URLError_Error_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Error() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSsrfGuard_URLError_Unwrap_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSsrfGuard_URLError_Unwrap_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSsrfGuard_URLError_Unwrap_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject blockedURLError + _ = subject.Unwrap() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} diff --git a/static_test.go b/go/static_test.go similarity index 87% rename from static_test.go rename to go/static_test.go index b6ef54c..16c48fb 100644 --- a/static_test.go +++ b/go/static_test.go @@ -3,10 +3,9 @@ package api_test import ( + core "dappco.re/go" "net/http" "net/http/httptest" - "os" - "path/filepath" "testing" "github.com/gin-gonic/gin" @@ -20,7 +19,7 @@ func TestWithStatic_Good_ServesFile(t *testing.T) { gin.SetMode(gin.TestMode) dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello world"), 0644); err != nil { + if err := coreWriteFile(core.PathJoin(dir, "hello.txt"), []byte("hello world"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } @@ -62,7 +61,7 @@ func TestWithStatic_Good_ServesIndex(t *testing.T) { gin.SetMode(gin.TestMode) dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("

Welcome

"), 0644); err != nil { + if err := coreWriteFile(core.PathJoin(dir, "index.html"), []byte("

Welcome

"), 0644); err != nil { t.Fatalf("failed to write index.html: %v", err) } @@ -87,7 +86,7 @@ func TestWithStatic_Good_CombinesWithRouteGroups(t *testing.T) { gin.SetMode(gin.TestMode) dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "app.js"), []byte("console.log('ok')"), 0644); err != nil { + if err := coreWriteFile(core.PathJoin(dir, "app.js"), []byte("console.log('ok')"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } @@ -122,12 +121,12 @@ func TestWithStatic_Good_MultipleStaticDirs(t *testing.T) { gin.SetMode(gin.TestMode) dir1 := t.TempDir() - if err := os.WriteFile(filepath.Join(dir1, "sdk.zip"), []byte("sdk-data"), 0644); err != nil { + if err := coreWriteFile(core.PathJoin(dir1, "sdk.zip"), []byte("sdk-data"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } dir2 := t.TempDir() - if err := os.WriteFile(filepath.Join(dir2, "style.css"), []byte("body{}"), 0644); err != nil { + if err := coreWriteFile(core.PathJoin(dir2, "style.css"), []byte("body{}"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } diff --git a/sunset.go b/go/sunset.go similarity index 99% rename from sunset.go rename to go/sunset.go index b64d067..be7a21f 100644 --- a/sunset.go +++ b/go/sunset.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/sunset_example_test.go b/go/sunset_example_test.go new file mode 100644 index 0000000..20afd73 --- /dev/null +++ b/go/sunset_example_test.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSunset_WithSunsetNoticeURL_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunsetNoticeURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSunset_WithSunsetNoticeURL_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunsetNoticeURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSunset_WithSunsetNoticeURL_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSunsetNoticeURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSunset_ApiSunset_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSunset_ApiSunset_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSunset_ApiSunset_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunset("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestSunset_ApiSunsetWith_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunsetWith("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSunset_ApiSunsetWith_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunsetWith("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSunset_ApiSunsetWith_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ApiSunsetWith("", "") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWithSunsetNoticeURL_sunset() { + func() { + defer func() { _ = recover() }() + _ = WithSunsetNoticeURL("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleApiSunset_sunset() { + func() { + defer func() { _ = recover() }() + _ = ApiSunset("", "") + }() + coretest.Println("done") + // Output: done +} + +func ExampleApiSunsetWith_sunset() { + func() { + defer func() { _ = recover() }() + _ = ApiSunsetWith("", "") + }() + coretest.Println("done") + // Output: done +} diff --git a/sunset_internal_test.go b/go/sunset_internal_test.go similarity index 100% rename from sunset_internal_test.go rename to go/sunset_internal_test.go diff --git a/sunset_test.go b/go/sunset_test.go similarity index 100% rename from sunset_test.go rename to go/sunset_test.go diff --git a/swagger.go b/go/swagger.go similarity index 99% rename from swagger.go rename to go/swagger.go index 23a95cb..ab1a4ff 100644 --- a/swagger.go +++ b/go/swagger.go @@ -5,7 +5,7 @@ package api import ( "net/http" // Note: AX-6 - structural HTTP status boundary for Gin handlers; no core primitive. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" diff --git a/go/swagger_example_test.go b/go/swagger_example_test.go new file mode 100644 index 0000000..00b2117 --- /dev/null +++ b/go/swagger_example_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestSwagger_Spec_ReadDoc_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *swaggerSpec + _ = subject.ReadDoc() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestSwagger_Spec_ReadDoc_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *swaggerSpec + _ = subject.ReadDoc() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestSwagger_Spec_ReadDoc_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *swaggerSpec + _ = subject.ReadDoc() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleSpec_ReadDoc_swagger() { + func() { + defer func() { _ = recover() }() + var subject *swaggerSpec + _ = subject.ReadDoc() + }() + coretest.Println("done") + // Output: done +} diff --git a/swagger_internal_test.go b/go/swagger_internal_test.go similarity index 94% rename from swagger_internal_test.go rename to go/swagger_internal_test.go index 70260b7..6462f33 100644 --- a/swagger_internal_test.go +++ b/go/swagger_internal_test.go @@ -3,7 +3,6 @@ package api import ( - "encoding/json" "testing" "github.com/gin-gonic/gin" @@ -61,7 +60,7 @@ func TestSwaggerSpec_ReadDoc_Good_SnapshotsGroups(t *testing.T) { groups[0] = replacement var doc map[string]any - if err := json.Unmarshal([]byte(spec.ReadDoc()), &doc); err != nil { + if err := coreJSONUnmarshal([]byte(spec.ReadDoc()), &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } diff --git a/swagger_test.go b/go/swagger_test.go similarity index 96% rename from swagger_test.go rename to go/swagger_test.go index 65a3b44..8f125b5 100644 --- a/swagger_test.go +++ b/go/swagger_test.go @@ -3,11 +3,10 @@ package api_test import ( - "encoding/json" + core "dappco.re/go" "io" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -50,7 +49,7 @@ func TestSwaggerEndpoint_Good(t *testing.T) { // Verify the body is valid JSON with expected fields. var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("expected valid JSON, got unmarshal error: %v", err) } @@ -99,7 +98,7 @@ func TestSwaggerEndpoint_Good_CustomPath(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("expected valid JSON, got unmarshal error: %v", err) } @@ -236,7 +235,7 @@ func TestSwagger_Good_SpecNotEmpty(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "path": map[string]any{"type": "string"}, + `path`: map[string]any{"type": "string"}, }, }, }, func(c *gin.Context) { @@ -263,7 +262,7 @@ func TestSwagger_Good_SpecNotEmpty(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -321,7 +320,7 @@ func TestSwagger_Good_WithToolBridge(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -372,7 +371,7 @@ func TestSwagger_Good_IncludesSSEEndpoint(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -416,7 +415,7 @@ func TestSwagger_Good_UsesCustomSSEPath(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -471,7 +470,7 @@ func TestSwagger_Good_InfoFromOptions(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -514,7 +513,7 @@ func TestSwagger_Good_IncludesGraphQLEndpoint(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -553,7 +552,7 @@ func TestSwagger_Good_UsesLicenseMetadata(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -596,7 +595,7 @@ func TestSwagger_Good_UsesContactMetadata(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -642,7 +641,7 @@ func TestSwagger_Good_UsesTermsOfServiceMetadata(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -678,7 +677,7 @@ func TestSwagger_Good_UsesExternalDocsMetadata(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -727,7 +726,7 @@ func TestSwagger_Good_IgnoresBlankMetadataOverrides(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -799,7 +798,7 @@ func TestSwagger_Good_UsesServerMetadata(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -849,7 +848,7 @@ func TestSwagger_Good_AppendsServerMetadataAcrossCalls(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -893,7 +892,7 @@ func TestSwagger_Good_ValidOpenAPI(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } @@ -958,7 +957,7 @@ func TestOpenAPISpecEndpoint_Good(t *testing.T) { } contentType := resp.Header.Get("Content-Type") - if !strings.HasPrefix(contentType, "application/json") { + if !core.HasPrefix(contentType, "application/json") { t.Fatalf("expected application/json content type, got %q", contentType) } @@ -968,7 +967,7 @@ func TestOpenAPISpecEndpoint_Good(t *testing.T) { } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } if doc["openapi"] != "3.1.0" { @@ -1073,7 +1072,7 @@ func TestOpenAPISpecEndpoint_Ugly_WorksWithoutSwagger(t *testing.T) { t.Fatalf("failed to read body: %v", err) } var doc map[string]any - if err := json.Unmarshal(body, &doc); err != nil { + if err := coreJSONUnmarshal(body, &doc); err != nil { t.Fatalf("invalid JSON: %v", err) } if doc["openapi"] != "3.1.0" { diff --git a/go/test_core_helpers_internal_test.go b/go/test_core_helpers_internal_test.go new file mode 100644 index 0000000..1aa5903 --- /dev/null +++ b/go/test_core_helpers_internal_test.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import core "dappco.re/go" + +func testCoreResultError(r core.Result) error { + if r.OK { + return nil + } + if err, ok := r.Value.(error); ok { + return err + } + return core.NewError("core operation failed") +} + +func coreJSONMarshal(v any) ([]byte, error) { + r := core.JSONMarshal(v) + if !r.OK { + return nil, testCoreResultError(r) + } + data, _ := r.Value.([]byte) + return data, nil +} + +func coreJSONUnmarshal(data []byte, target any) error { + return testCoreResultError(core.JSONUnmarshal(data, target)) +} + +func coreWriteFile(path string, data []byte, mode core.FileMode) error { + return testCoreResultError(core.WriteFile(path, data, mode)) +} + +func coreMkdirAll(path string, mode core.FileMode) error { + return testCoreResultError(core.MkdirAll(path, mode)) +} + +func coreBytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/go/test_core_helpers_test.go b/go/test_core_helpers_test.go new file mode 100644 index 0000000..ad4a5df --- /dev/null +++ b/go/test_core_helpers_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api_test + +import core "dappco.re/go" + +func coreResultError(r core.Result) error { + if r.OK { + return nil + } + if err, ok := r.Value.(error); ok { + return err + } + return core.NewError("core operation failed") +} + +func coreJSONMarshal(v any) ([]byte, error) { + r := core.JSONMarshal(v) + if !r.OK { + return nil, coreResultError(r) + } + data, _ := r.Value.([]byte) + return data, nil +} + +func coreJSONUnmarshal(data []byte, target any) error { + return coreResultError(core.JSONUnmarshal(data, target)) +} + +func coreJSONDecode(reader core.Reader, target any) error { + r := core.ReadAll(reader) + if !r.OK { + return coreResultError(r) + } + text, _ := r.Value.(string) + return coreJSONUnmarshal([]byte(text), target) +} + +func coreWriteFile(path string, data []byte, mode core.FileMode) error { + return coreResultError(core.WriteFile(path, data, mode)) +} + +func coreReadFile(path string) ([]byte, error) { + r := core.ReadFile(path) + if !r.OK { + return nil, coreResultError(r) + } + data, _ := r.Value.([]byte) + return data, nil +} + +func coreStat(path string) (core.FsFileInfo, error) { + r := core.Stat(path) + if !r.OK { + return nil, coreResultError(r) + } + info, _ := r.Value.(core.FsFileInfo) + return info, nil +} + +func coreStringRepeat(s string, count int) string { + if count <= 0 { + return "" + } + b := core.NewBuilder() + for i := 0; i < count; i++ { + b.WriteString(s) + } + return b.String() +} + +func coreCutPrefix(s, prefix string) (string, bool) { + if !core.HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} + +func coreBytesRepeat(b []byte, count int) []byte { + if count <= 0 { + return nil + } + out := make([]byte, 0, len(b)*count) + for i := 0; i < count; i++ { + out = append(out, b...) + } + return out +} diff --git a/tests/cli/api/Taskfile.yaml b/go/tests/cli/api/Taskfile.yaml similarity index 100% rename from tests/cli/api/Taskfile.yaml rename to go/tests/cli/api/Taskfile.yaml diff --git a/text_helpers.go b/go/text_helpers.go similarity index 100% rename from text_helpers.go rename to go/text_helpers.go diff --git a/go/text_helpers_example_test.go b/go/text_helpers_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/text_helpers_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/text_helpers_test.go b/go/text_helpers_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/text_helpers_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/timeout_test.go b/go/timeout_test.go similarity index 94% rename from timeout_test.go rename to go/timeout_test.go index 08066ca..98d4ac3 100644 --- a/timeout_test.go +++ b/go/timeout_test.go @@ -3,7 +3,6 @@ package api_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -55,7 +54,7 @@ func TestWithTimeout_Good_FastRequestSucceeds(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if !resp.Success { @@ -98,7 +97,7 @@ func TestWithTimeout_Good_TimeoutResponseEnvelope(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Success { @@ -139,7 +138,7 @@ func TestWithTimeout_Good_CombinesWithOtherMiddleware(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data != "pong" { @@ -166,7 +165,7 @@ func TestWithTimeout_Ugly_ZeroDurationDoesNotPanic(t *testing.T) { } var resp api.Response[string] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal error: %v", err) } if resp.Data != "pong" { diff --git a/tracing.go b/go/tracing.go similarity index 100% rename from tracing.go rename to go/tracing.go diff --git a/go/tracing_example_test.go b/go/tracing_example_test.go new file mode 100644 index 0000000..3917b80 --- /dev/null +++ b/go/tracing_example_test.go @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestTracing_WithTracing_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTracing("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTracing_WithTracing_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTracing("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTracing_WithTracing_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithTracing("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTracing_NewTracerProvider_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewTracerProvider(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTracing_NewTracerProvider_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewTracerProvider(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTracing_NewTracerProvider_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewTracerProvider(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWithTracing_tracing() { + func() { + defer func() { _ = recover() }() + _ = WithTracing("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewTracerProvider_tracing() { + func() { + defer func() { _ = recover() }() + _ = NewTracerProvider(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/tracing_test.go b/go/tracing_test.go similarity index 98% rename from tracing_test.go rename to go/tracing_test.go index a9ebfc4..4631874 100644 --- a/tracing_test.go +++ b/go/tracing_test.go @@ -4,10 +4,9 @@ package api_test import ( "context" - "errors" + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -75,7 +74,7 @@ type failingTracingTestExporter struct { func (e *failingTracingTestExporter) ExportSpans(_ context.Context, spans []sdktrace.ReadOnlySpan) error { e.exports += len(spans) - return errors.New("tracing exporter failed") + return core.NewError("tracing exporter failed") } func (e *failingTracingTestExporter) Shutdown(context.Context) error { return nil } @@ -386,7 +385,7 @@ func TestTracing_WithTracing_Good_AttachesDurationAndSizeAttributes(t *testing.T h := e.Handler() w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/trace/echo", strings.NewReader("abc")) + req, _ := http.NewRequest(http.MethodPost, "/trace/echo", core.NewReader("abc")) req.Header.Set("Content-Type", "text/plain") h.ServeHTTP(w, req) diff --git a/transformer.go b/go/transformer.go similarity index 93% rename from transformer.go rename to go/transformer.go index 352d83d..6abdf43 100644 --- a/transformer.go +++ b/go/transformer.go @@ -5,7 +5,7 @@ package api import ( "reflect" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -40,7 +40,10 @@ type TransformerOut[I, O any] interface { type TransformerInFunc[I, O any] func(*gin.Context, I) (O, error) // TransformIn runs f. -func (f TransformerInFunc[I, O]) TransformIn(c *gin.Context, in I) (O, error) { +func (f TransformerInFunc[I, O]) TransformIn(c *gin.Context, in I) ( + O, + error, +) { return f(c, in) } @@ -48,7 +51,10 @@ func (f TransformerInFunc[I, O]) TransformIn(c *gin.Context, in I) (O, error) { type TransformerOutFunc[I, O any] func(*gin.Context, I) (O, error) // TransformOut runs f. -func (f TransformerOutFunc[I, O]) TransformOut(c *gin.Context, in I) (O, error) { +func (f TransformerOutFunc[I, O]) TransformOut(c *gin.Context, in I) ( + O, + error, +) { return f(c, in) } @@ -69,12 +75,18 @@ func RenameFields(fields map[string]string) FieldRenamer { } // TransformIn renames inbound request fields. -func (r FieldRenamer) TransformIn(_ *gin.Context, payload map[string]any) (map[string]any, error) { +func (r FieldRenamer) TransformIn(_ *gin.Context, payload map[string]any) ( + map[string]any, + error, +) { return r.rename(payload), nil } // TransformOut renames outbound response fields. -func (r FieldRenamer) TransformOut(_ *gin.Context, payload map[string]any) (map[string]any, error) { +func (r FieldRenamer) TransformOut(_ *gin.Context, payload map[string]any) ( + map[string]any, + error, +) { return r.rename(payload), nil } @@ -121,7 +133,10 @@ type compiledTransformer struct { withContext bool } -func compileTransformerPipeline(direction string, raw any) ([]compiledTransformer, error) { +func compileTransformerPipeline(direction string, raw any) ( + []compiledTransformer, + error, +) { if isNilValue(raw) { return nil, nil } @@ -155,7 +170,10 @@ func compileTransformerPipeline(direction string, raw any) ([]compiledTransforme } } -func compileTransformer(direction string, raw any) (compiledTransformer, error) { +func compileTransformer(direction string, raw any) ( + compiledTransformer, + error, +) { methodName := transformerMethodName(direction) method := reflect.ValueOf(raw).MethodByName(methodName) if !method.IsValid() { @@ -199,7 +217,10 @@ func transformerMethodName(direction string) string { return "TransformIn" } -func (t compiledTransformer) transform(c *gin.Context, payload []byte) ([]byte, error) { +func (t compiledTransformer) transform(c *gin.Context, payload []byte) ( + []byte, + error, +) { input, err := decodeTransformerInput(payload, t.inputType) if err != nil { return nil, err @@ -218,7 +239,10 @@ func (t compiledTransformer) transform(c *gin.Context, payload []byte) ([]byte, return encodeTransformerOutput(out[0]) } -func decodeTransformerInput(payload []byte, inputType reflect.Type) (reflect.Value, error) { +func decodeTransformerInput(payload []byte, inputType reflect.Type) ( + reflect.Value, + error, +) { if inputType.Kind() == reflect.Pointer { target := reflect.New(inputType.Elem()) if err := unmarshalTransformerPayload(payload, target.Interface()); err != nil { @@ -234,7 +258,9 @@ func decodeTransformerInput(payload []byte, inputType reflect.Type) (reflect.Val return target.Elem(), nil } -func unmarshalTransformerPayload(payload []byte, target any) error { +func unmarshalTransformerPayload(payload []byte, target any) ( + _ error, +) { result := core.JSONUnmarshal(payload, target) if result.OK { return nil @@ -245,7 +271,10 @@ func unmarshalTransformerPayload(payload []byte, target any) error { return core.E("Transformer.Decode", "decode payload", nil) } -func encodeTransformerOutput(value reflect.Value) ([]byte, error) { +func encodeTransformerOutput(value reflect.Value) ( + []byte, + error, +) { var payload any if value.IsValid() { payload = value.Interface() @@ -266,7 +295,10 @@ func encodeTransformerOutput(value reflect.Value) ([]byte, error) { return data, nil } -func runTransformerPipeline(c *gin.Context, payload []byte, pipeline []compiledTransformer) ([]byte, error) { +func runTransformerPipeline(c *gin.Context, payload []byte, pipeline []compiledTransformer) ( + []byte, + error, +) { var err error for _, transformer := range pipeline { payload, err = transformer.transform(c, payload) diff --git a/go/transformer_example_test.go b/go/transformer_example_test.go new file mode 100644 index 0000000..0fb2518 --- /dev/null +++ b/go/transformer_example_test.go @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestTransformer_TransformerInFunc_TransformIn_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerInFunc[any, any] + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransformer_TransformerInFunc_TransformIn_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerInFunc[any, any] + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransformer_TransformerInFunc_TransformIn_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerInFunc[any, any] + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransformer_TransformerOutFunc_TransformOut_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerOutFunc[any, any] + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransformer_TransformerOutFunc_TransformOut_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerOutFunc[any, any] + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransformer_TransformerOutFunc_TransformOut_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject TransformerOutFunc[any, any] + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransformer_RenameFields_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RenameFields(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransformer_RenameFields_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RenameFields(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransformer_RenameFields_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = RenameFields(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransformer_FieldRenamer_TransformIn_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransformer_FieldRenamer_TransformIn_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransformer_FieldRenamer_TransformIn_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformIn(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransformer_FieldRenamer_TransformOut_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransformer_FieldRenamer_TransformOut_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransformer_FieldRenamer_TransformOut_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject FieldRenamer + _, _ = subject.TransformOut(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleTransformerInFunc_TransformIn_transformer() { + func() { + defer func() { _ = recover() }() + var subject TransformerInFunc[any, any] + _, _ = subject.TransformIn(nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleTransformerOutFunc_TransformOut_transformer() { + func() { + defer func() { _ = recover() }() + var subject TransformerOutFunc[any, any] + _, _ = subject.TransformOut(nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleRenameFields_transformer() { + func() { + defer func() { _ = recover() }() + _ = RenameFields(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleFieldRenamer_TransformIn_transformer() { + func() { + defer func() { _ = recover() }() + var subject FieldRenamer + _, _ = subject.TransformIn(nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleFieldRenamer_TransformOut_transformer() { + func() { + defer func() { _ = recover() }() + var subject FieldRenamer + _, _ = subject.TransformOut(nil, nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/transformer_in.go b/go/transformer_in.go similarity index 97% rename from transformer_in.go rename to go/transformer_in.go index fb032a9..dbe5848 100644 --- a/transformer_in.go +++ b/go/transformer_in.go @@ -6,7 +6,7 @@ import ( "io" // Note: AX-6 - request body reads and resets are HTTP stream boundaries. "net/http" // Note: AX-6 - transformer middleware emits HTTP status codes. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -178,7 +178,12 @@ func setTransformerRequestBody(c *gin.Context, body []byte) { c.Request.ContentLength = int64(len(body)) } -func abortTransformerRequest(c *gin.Context, status int, message string, err error) { +func abortTransformerRequest( + c *gin.Context, + status int, + message string, + err error, +) { details := map[string]any{} if err != nil { details["error"] = err.Error() diff --git a/go/transformer_in_example_test.go b/go/transformer_in_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/transformer_in_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/transformer_in_test.go b/go/transformer_in_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/transformer_in_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/transformer_out.go b/go/transformer_out.go similarity index 92% rename from transformer_out.go rename to go/transformer_out.go index 94cf68b..d64b3db 100644 --- a/transformer_out.go +++ b/go/transformer_out.go @@ -5,7 +5,7 @@ package api import ( "net/http" // Note: AX-6 - transformer response wrappers emit HTTP status codes. - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) @@ -54,7 +54,10 @@ func wrapTransformerOutHandler(handler gin.HandlerFunc, pipeline []compiledTrans } } -func transformResponseEnvelope(c *gin.Context, body []byte, pipeline []compiledTransformer) ([]byte, error) { +func transformResponseEnvelope(c *gin.Context, body []byte, pipeline []compiledTransformer) ( + []byte, + error, +) { if len(pipeline) == 0 || core.Trim(string(body)) == "" { return body, nil } @@ -103,7 +106,9 @@ func transformResponseEnvelope(c *gin.Context, body []byte, pipeline []compiledT return data, nil } -func unmarshalEnvelope(data []byte, target any) error { +func unmarshalEnvelope(data []byte, target any) ( + _ error, +) { result := core.JSONUnmarshal(data, target) if result.OK { return nil @@ -114,7 +119,11 @@ func unmarshalEnvelope(data []byte, target any) error { return core.E("TransformerOut", "decode response envelope", nil) } -func writeTransformerResponseError(recorder *toolResponseRecorder, message string, err error) { +func writeTransformerResponseError( + recorder *toolResponseRecorder, + message string, + err error, +) { recorder.reset() recorder.writeErrorResponse(http.StatusInternalServerError, FailWithDetails( "invalid_response_body", diff --git a/go/transformer_out_example_test.go b/go/transformer_out_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/transformer_out_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/go/transformer_out_test.go b/go/transformer_out_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/transformer_out_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/transformer_test.go b/go/transformer_test.go similarity index 90% rename from transformer_test.go rename to go/transformer_test.go index 73534a7..d7fcfb4 100644 --- a/transformer_test.go +++ b/go/transformer_test.go @@ -3,13 +3,11 @@ package api_test import ( - "bytes" - "encoding/json" "net/http" "net/http/httptest" "testing" - core "dappco.re/go/core" + core "dappco.re/go" api "dappco.re/go/api" @@ -67,7 +65,7 @@ func TestTransformer_Good_ToolBridgeRemapsInboundAndOutboundDTOs(t *testing.T) { TransformerOut: transformerBridgeOut{}, }, func(c *gin.Context) { var payload transformerInternalUser - if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + if err := coreJSONDecode(c.Request.Body, &payload); err != nil { t.Fatalf("handler could not decode transformed payload: %v", err) } if payload.Name != "Ada Lovelace" { @@ -80,7 +78,7 @@ func TestTransformer_Good_ToolBridgeRemapsInboundAndOutboundDTOs(t *testing.T) { bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/create_user", bytes.NewBufferString(`{"full_name":"Ada Lovelace"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/create_user", core.NewBufferString(`{"full_name":"Ada Lovelace"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -88,7 +86,7 @@ func TestTransformer_Good_ToolBridgeRemapsInboundAndOutboundDTOs(t *testing.T) { } var resp api.Response[map[string]any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if !resp.Success { @@ -127,7 +125,7 @@ func TestTransformer_Bad_ToolBridgeValidatesExternalPayloadBeforeTransform(t *te bridge.RegisterRoutes(rg) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/tools/create_user", bytes.NewBufferString(`{"name":"Ada Lovelace"}`)) + req, _ := http.NewRequest(http.MethodPost, "/tools/create_user", core.NewBufferString(`{"name":"Ada Lovelace"}`)) engine.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -135,7 +133,7 @@ func TestTransformer_Bad_ToolBridgeValidatesExternalPayloadBeforeTransform(t *te } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if resp.Success { @@ -153,7 +151,7 @@ func (transformerRouteGroup) BasePath() string { return "/users" } func (transformerRouteGroup) RegisterRoutes(rg *gin.RouterGroup) { rg.POST("", func(c *gin.Context) { var payload transformerInternalUser - if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil { + if err := coreJSONDecode(c.Request.Body, &payload); err != nil { c.JSON(http.StatusBadRequest, api.Fail("invalid_body", err.Error())) return } @@ -198,7 +196,7 @@ func TestTransformer_Good_EngineRouteDescriptionRemapsDTOs(t *testing.T) { engine.Register(transformerRouteGroup{}) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/users", bytes.NewBufferString(`{"full_name":"Grace Hopper"}`)) + req, _ := http.NewRequest(http.MethodPost, "/users", core.NewBufferString(`{"full_name":"Grace Hopper"}`)) engine.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { @@ -206,7 +204,7 @@ func TestTransformer_Good_EngineRouteDescriptionRemapsDTOs(t *testing.T) { } var resp api.Response[map[string]any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if !resp.Success { @@ -230,7 +228,7 @@ func TestTransformer_Bad_EngineTransformerErrorReturnsBadRequest(t *testing.T) { engine.Register(errorTransformerRouteGroup{}) w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/error-users", bytes.NewBufferString(`{"full_name":"Alan Turing"}`)) + req, _ := http.NewRequest(http.MethodPost, "/error-users", core.NewBufferString(`{"full_name":"Alan Turing"}`)) engine.Handler().ServeHTTP(w, req) if w.Code != http.StatusBadRequest { @@ -238,7 +236,7 @@ func TestTransformer_Bad_EngineTransformerErrorReturnsBadRequest(t *testing.T) { } var resp api.Response[any] - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + if err := coreJSONUnmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if resp.Success { diff --git a/transport.go b/go/transport.go similarity index 99% rename from transport.go rename to go/transport.go index ece4eda..a61ae02 100644 --- a/transport.go +++ b/go/transport.go @@ -2,7 +2,7 @@ package api -import core "dappco.re/go/core" +import core "dappco.re/go" // TransportConfig captures the configured transport endpoints and flags for an Engine. // diff --git a/transport_client.go b/go/transport_client.go similarity index 91% rename from transport_client.go rename to go/transport_client.go index c98a2bd..fd69189 100644 --- a/transport_client.go +++ b/go/transport_client.go @@ -11,7 +11,7 @@ import ( "strconv" "time" - core "dappco.re/go/core" + core "dappco.re/go" coreerr "dappco.re/go/log" "github.com/gorilla/websocket" @@ -96,11 +96,17 @@ func NewWebSocketClient(rawURL string, opts ...WebSocketClientOption) *WebSocket // Example: // // conn, resp, err := client.DialContext(ctx) -func (c *WebSocketClient) DialContext(ctx context.Context) (*websocket.Conn, *http.Response, error) { +func (c *WebSocketClient) DialContext(ctx context.Context) ( + *websocket.Conn, + *http.Response, + error, +) { if c == nil { return nil, nil, coreerr.E("", "WebSocketClient is nil", nil) } + header := cloneHTTPHeader(c.Header) + rawURL, err := normaliseWebSocketClientURL(c.URL) if err != nil { return nil, nil, err @@ -109,17 +115,13 @@ func (c *WebSocketClient) DialContext(ctx context.Context) (*websocket.Conn, *ht return nil, nil, err } - dialer := websocket.DefaultDialer if c.Dialer != nil { copyDialer := *c.Dialer - dialer = ©Dialer - } else { - copyDialer := *websocket.DefaultDialer - dialer = ©Dialer + return (©Dialer).DialContext(ctx, rawURL, header) } - header := cloneHTTPHeader(c.Header) - return dialer.DialContext(ctx, rawURL, header) + copyDialer := *websocket.DefaultDialer + return (©Dialer).DialContext(ctx, rawURL, header) } // SSEEvent is a parsed Server-Sent Events message. @@ -204,7 +206,10 @@ func NewSSEClient(rawURL string, opts ...SSEClientOption) *SSEClient { // Example: // // resp, err := client.Connect(ctx) -func (c *SSEClient) Connect(ctx context.Context) (*http.Response, error) { +func (c *SSEClient) Connect(ctx context.Context) ( + *http.Response, + error, +) { if c == nil { return nil, coreerr.E("", "SSEClient is nil", nil) } @@ -235,7 +240,9 @@ func (c *SSEClient) Connect(ctx context.Context) (*http.Response, error) { return nil, err } if resp.StatusCode != http.StatusOK { - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() return nil, coreerr.E("", core.Sprintf("unexpected SSE status %d", resp.StatusCode), nil) } return resp, nil @@ -249,7 +256,10 @@ func (c *SSEClient) Connect(ctx context.Context) (*http.Response, error) { // events, err := client.Events(ctx) // if err != nil { ... } // for evt := range events { _ = evt } -func (c *SSEClient) Events(ctx context.Context) (<-chan SSEEvent, error) { +func (c *SSEClient) Events(ctx context.Context) ( + <-chan SSEEvent, + error, +) { resp, err := c.Connect(ctx) if err != nil { return nil, err @@ -258,7 +268,9 @@ func (c *SSEClient) Events(ctx context.Context) (<-chan SSEEvent, error) { out := make(chan SSEEvent) go func() { defer close(out) - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() parseSSEStream(ctx, resp.Body, out) }() @@ -326,10 +338,15 @@ func parseSSEStream(ctx context.Context, body io.Reader, out chan<- SSEEvent) { } } - _ = emit() + if !emit() { + return + } } -func normaliseWebSocketClientURL(rawURL string) (string, error) { +func normaliseWebSocketClientURL(rawURL string) ( + string, + error, +) { rawURL = core.Trim(rawURL) if rawURL == "" { return "", core.E("", "WebSocketClient URL is required", nil) @@ -370,11 +387,15 @@ func normaliseWebSocketClientURL(rawURL string) (string, error) { } } -func invalidWebSocketClientURLError(err error) error { +func invalidWebSocketClientURLError(err error) ( + _ error, +) { return core.E("", "invalid WebSocketClient URL", err) } -func validateWebSocketClientPort(parsed *url.URL) error { +func validateWebSocketClientPort(parsed *url.URL) ( + _ error, +) { if parsed == nil { return invalidWebSocketClientURLError(nil) } @@ -392,7 +413,9 @@ func validateWebSocketClientPort(parsed *url.URL) error { return nil } -func validateOutboundWebSocketClientURL(rawURL string) error { +func validateOutboundWebSocketClientURL(rawURL string) ( + _ error, +) { guardURL, err := outboundWebSocketGuardURL(rawURL) if err != nil { return err @@ -400,7 +423,10 @@ func validateOutboundWebSocketClientURL(rawURL string) error { return validateOutboundURL(guardURL) } -func outboundWebSocketGuardURL(rawURL string) (string, error) { +func outboundWebSocketGuardURL(rawURL string) ( + string, + error, +) { parsedResult := core.URLParse(rawURL) if !parsedResult.OK { if err, ok := parsedResult.Value.(error); ok { @@ -444,7 +470,10 @@ func cloneHTTPHeader(header http.Header) http.Header { // any followed redirects against the deny-by-default outbound policy (see // ssrf_guard.go) before invoking client.Do. Cerberus mechanism review attached // to Mantis #318. -func doHTTPClientRequest(client *http.Client, req *http.Request) (*http.Response, error) { +func doHTTPClientRequest(client *http.Client, req *http.Request) ( + *http.Response, + error, +) { if client == nil { client = http.DefaultClient } @@ -461,7 +490,9 @@ func doHTTPClientRequest(client *http.Client, req *http.Request) (*http.Response resp, err := requestClient.Do(req) if err != nil { if resp != nil && resp.Body != nil { - _ = resp.Body.Close() + if closeErr := resp.Body.Close(); closeErr != nil { + return nil, core.ErrorJoin(err, closeErr) + } } return nil, err diff --git a/go/transport_client_example_test.go b/go/transport_client_example_test.go new file mode 100644 index 0000000..020838b --- /dev/null +++ b/go/transport_client_example_test.go @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestTransportClient_WithWebSocketHeaders_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_WithWebSocketHeaders_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_WithWebSocketHeaders_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_WithWebSocketDialer_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketDialer(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_WithWebSocketDialer_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketDialer(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_WithWebSocketDialer_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithWebSocketDialer(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_NewWebSocketClient_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebSocketClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_NewWebSocketClient_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebSocketClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_NewWebSocketClient_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebSocketClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_WebSocketClient_DialContext_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebSocketClient + _, _, _ = subject.DialContext(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_WebSocketClient_DialContext_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebSocketClient + _, _, _ = subject.DialContext(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_WebSocketClient_DialContext_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebSocketClient + _, _, _ = subject.DialContext(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_WithSSEHeaders_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_WithSSEHeaders_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_WithSSEHeaders_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHeaders(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_WithSSEHTTPClient_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_WithSSEHTTPClient_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_WithSSEHTTPClient_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WithSSEHTTPClient(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_NewSSEClient_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_NewSSEClient_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_NewSSEClient_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewSSEClient("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_SSEClient_Connect_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Connect(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_SSEClient_Connect_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Connect(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_SSEClient_Connect_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Connect(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestTransportClient_SSEClient_Events_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Events(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransportClient_SSEClient_Events_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Events(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransportClient_SSEClient_Events_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *SSEClient + _, _ = subject.Events(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWithWebSocketHeaders_transportClient() { + func() { + defer func() { _ = recover() }() + _ = WithWebSocketHeaders(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithWebSocketDialer_transportClient() { + func() { + defer func() { _ = recover() }() + _ = WithWebSocketDialer(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewWebSocketClient_transportClient() { + func() { + defer func() { _ = recover() }() + _ = NewWebSocketClient("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebSocketClient_DialContext_transportClient() { + func() { + defer func() { _ = recover() }() + var subject *WebSocketClient + _, _, _ = subject.DialContext(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSSEHeaders_transportClient() { + func() { + defer func() { _ = recover() }() + _ = WithSSEHeaders(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWithSSEHTTPClient_transportClient() { + func() { + defer func() { _ = recover() }() + _ = WithSSEHTTPClient(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewSSEClient_transportClient() { + func() { + defer func() { _ = recover() }() + _ = NewSSEClient("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEClient_Connect_transportClient() { + func() { + defer func() { _ = recover() }() + var subject *SSEClient + _, _ = subject.Connect(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleSSEClient_Events_transportClient() { + func() { + defer func() { _ = recover() }() + var subject *SSEClient + _, _ = subject.Events(nil) + }() + coretest.Println("done") + // Output: done +} diff --git a/transport_client_test.go b/go/transport_client_test.go similarity index 97% rename from transport_client_test.go rename to go/transport_client_test.go index b8eade0..953dfdb 100644 --- a/transport_client_test.go +++ b/go/transport_client_test.go @@ -5,17 +5,15 @@ package api import ( "context" "crypto/tls" - "errors" "io" "net" "net/http" "net/http/httptest" "net/url" - "strings" "testing" "time" - "dappco.re/go/core" + "dappco.re/go" "github.com/gorilla/websocket" ) @@ -278,11 +276,11 @@ func TestTransportClient_DialContext_Bad_BlocksSSRFWebSocketTargets(t *testing.T dialer := &websocket.Dialer{ NetDialContext: func(context.Context, string, string) (net.Conn, error) { dialCalls++ - return nil, errors.New("dial should not be called") + return nil, core.NewError("dial should not be called") }, NetDialTLSContext: func(context.Context, string, string) (net.Conn, error) { dialCalls++ - return nil, errors.New("dial should not be called") + return nil, core.NewError("dial should not be called") }, } @@ -296,7 +294,7 @@ func TestTransportClient_DialContext_Bad_BlocksSSRFWebSocketTargets(t *testing.T } t.Fatal("expected websocket target to be blocked") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Fatalf("expected errOutboundURLBlocked, got %v", err) } if dialCalls != 0 { @@ -345,7 +343,7 @@ func TestTransportClient_normaliseWebSocketClientURL_Bad_ReturnsErrorsForMalform t.Fatalf("expected malformed URL to fail, got normalized URL %q", normalized) } var typed *core.Err - if !errors.As(err, &typed) { + if !core.As(err, &typed) { t.Fatalf("expected typed core error, got %T: %v", err, err) } }) @@ -450,7 +448,7 @@ func TestTransportClient_Connect_Bad_ClosesResponseBodyOnRedirectError(t *testin closed: &closed, }, CheckRedirect: func(*http.Request, []*http.Request) error { - return errors.New("redirect blocked") + return core.NewError("redirect blocked") }, })) @@ -463,7 +461,7 @@ func TestTransportClient_Connect_Bad_ClosesResponseBodyOnRedirectError(t *testin } func TestTransportClient_Events_Good_ParsesStream(t *testing.T) { - payload := strings.Join([]string{ + payload := core.Join("\n", []string{ ": comment", "id: 7", "event: update", @@ -473,7 +471,7 @@ func TestTransportClient_Events_Good_ParsesStream(t *testing.T) { "", "data: final", "", - }, "\n") + }...) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") @@ -607,7 +605,7 @@ func TestTransportClient_DialContext_Ugly_CleansBlankURL(t *testing.T) { } func TestTransportClient_Events_Good_ClosesReaderOnEOF(t *testing.T) { - body := strings.NewReader("event: done\ndata: ok\n\n") + body := core.NewReader("event: done\ndata: ok\n\n") events := make(chan SSEEvent, 1) parseSSEStream(context.Background(), body, events) @@ -657,7 +655,7 @@ func TestTransportClient_doHTTPClientRequest_Bad_BlocksRedirectToMetadata(t *tes Header: http.Header{ "Location": {"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}, }, - Body: io.NopCloser(strings.NewReader("redirecting")), + Body: io.NopCloser(core.NewReader("redirecting")), Request: req, }, nil }), @@ -674,7 +672,7 @@ func TestTransportClient_doHTTPClientRequest_Bad_BlocksRedirectToMetadata(t *tes } t.Fatal("expected metadata redirect to be blocked") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Fatalf("expected errOutboundURLBlocked, got %v", err) } if attempts != 1 { diff --git a/go/transport_example_test.go b/go/transport_example_test.go new file mode 100644 index 0000000..441df6f --- /dev/null +++ b/go/transport_example_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestTransport_Engine_TransportConfig_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.TransportConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestTransport_Engine_TransportConfig_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.TransportConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestTransport_Engine_TransportConfig_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *Engine + _ = subject.TransportConfig() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleEngine_TransportConfig_transport() { + func() { + defer func() { _ = recover() }() + var subject *Engine + _ = subject.TransportConfig() + }() + coretest.Println("done") + // Output: done +} diff --git a/transport_test.go b/go/transport_test.go similarity index 100% rename from transport_test.go rename to go/transport_test.go diff --git a/webhook.go b/go/webhook.go similarity index 94% rename from webhook.go rename to go/webhook.go index 22370ff..9670589 100644 --- a/webhook.go +++ b/go/webhook.go @@ -13,7 +13,7 @@ import ( "slices" "time" - core "dappco.re/go/core" + core "dappco.re/go" ) // Canonical webhook event identifiers from RFC §6. These constants mirror the @@ -65,7 +65,7 @@ func WebhookEvents() []string { // canonical identifiers documented in RFC §6. // // if !api.IsKnownWebhookEvent(evt) { -// return errors.New("unknown webhook event") +// return core.NewError("unknown webhook event") // } func IsKnownWebhookEvent(name string) bool { return slices.Contains(WebhookEvents(), core.Trim(name)) @@ -135,7 +135,10 @@ func NewWebhookSignerWithTolerance(secret string, tolerance time.Duration) *Webh // // secret, err := api.GenerateWebhookSecret() // // secret = "9f1a..." (64 hex chars) -func GenerateWebhookSecret() (string, error) { +func GenerateWebhookSecret() ( + string, + error, +) { buf := make([]byte, 32) if _, err := randomRead(buf); err != nil { return "", core.E("WebhookSigner.GenerateSecret", "read random bytes", err) @@ -228,7 +231,7 @@ func (s *WebhookSigner) VerifySignatureOnly(payload []byte, signature string, ti // signer's configured tolerance window relative to the current time. // // if !signer.IsTimestampValid(ts) { -// return errors.New("webhook timestamp expired") +// return core.NewError("webhook timestamp expired") // } func (s *WebhookSigner) IsTimestampValid(timestamp int64) bool { tol := s.Tolerance() @@ -272,7 +275,9 @@ func (s *WebhookSigner) VerifyRequest(r *http.Request, payload []byte) bool { // if err := api.ValidateWebhookURL("https://hooks.example.com/inbox"); err != nil { // return err // } -func ValidateWebhookURL(raw string) error { +func ValidateWebhookURL(raw string) ( + _ error, +) { parsedResult := core.URLParse(core.Trim(raw)) if !parsedResult.OK { err, _ := parsedResult.Value.(error) @@ -355,17 +360,17 @@ func blockedWebhookCIDRs() []*net.IPNet { // the SSRF security boundary itself, not configuration values. SonarCloud's // "IP should not be hardcoded" rule is a false positive on this list. var webhookBlockedCIDRs = mustParseWebhookCIDRs( - "0.0.0.0/8", // RFC 1122 "this network" - "100.64.0.0/10", // RFC 6598 carrier-grade NAT - "127.0.0.0/8", // RFC 1122 loopback - "169.254.0.0/16", // RFC 3927 link-local - "192.0.0.0/24", // RFC 6890 IETF protocol assignments - "192.0.2.0/24", // RFC 5737 TEST-NET-1 - "198.18.0.0/15", // RFC 2544 benchmark - "198.51.100.0/24", // RFC 5737 TEST-NET-2 - "203.0.113.0/24", // RFC 5737 TEST-NET-3 - "224.0.0.0/4", // RFC 5771 multicast - "240.0.0.0/4", // RFC 1112 reserved + "0.0.0.0/8", // RFC 1122 "this network" + "100.64.0.0/10", // RFC 6598 carrier-grade NAT + "127.0.0.0/8", // RFC 1122 loopback + "169.254.0.0/16", // RFC 3927 link-local + "192.0.0.0/24", // RFC 6890 IETF protocol assignments + "192.0.2.0/24", // RFC 5737 TEST-NET-1 + "198.18.0.0/15", // RFC 2544 benchmark + "198.51.100.0/24", // RFC 5737 TEST-NET-2 + "203.0.113.0/24", // RFC 5737 TEST-NET-3 + "224.0.0.0/4", // RFC 5771 multicast + "240.0.0.0/4", // RFC 1112 reserved "::/128", "::1/128", "64:ff9b:1::/48", diff --git a/go/webhook_example_test.go b/go/webhook_example_test.go new file mode 100644 index 0000000..b8bbf1c --- /dev/null +++ b/go/webhook_example_test.go @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import coretest "dappco.re/go" + +func TestWebhook_WebhookEvents_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebhookEvents() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookEvents_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebhookEvents() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookEvents_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = WebhookEvents() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_IsKnownWebhookEvent_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsKnownWebhookEvent("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_IsKnownWebhookEvent_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsKnownWebhookEvent("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_IsKnownWebhookEvent_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = IsKnownWebhookEvent("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_NewWebhookSigner_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSigner("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_NewWebhookSigner_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSigner("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_NewWebhookSigner_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSigner("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_NewWebhookSignerWithTolerance_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSignerWithTolerance("", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_NewWebhookSignerWithTolerance_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSignerWithTolerance("", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_NewWebhookSignerWithTolerance_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = NewWebhookSignerWithTolerance("", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_GenerateWebhookSecret_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GenerateWebhookSecret() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_GenerateWebhookSecret_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GenerateWebhookSecret() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_GenerateWebhookSecret_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _, _ = GenerateWebhookSecret() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_Tolerance_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Tolerance() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_Tolerance_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Tolerance() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_Tolerance_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Tolerance() + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_Sign_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Sign(nil, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_Sign_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Sign(nil, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_Sign_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Sign(nil, 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_SignNow_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _, _ = subject.SignNow(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_SignNow_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _, _ = subject.SignNow(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_SignNow_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _, _ = subject.SignNow(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_Headers_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Headers(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_Headers_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Headers(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_Headers_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Headers(nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_Verify_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Verify(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_Verify_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Verify(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_Verify_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.Verify(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_VerifySignatureOnly_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifySignatureOnly(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_VerifySignatureOnly_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifySignatureOnly(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_VerifySignatureOnly_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifySignatureOnly(nil, "", 0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_IsTimestampValid_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.IsTimestampValid(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_IsTimestampValid_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.IsTimestampValid(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_IsTimestampValid_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.IsTimestampValid(0) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_WebhookSigner_VerifyRequest_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifyRequest(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_WebhookSigner_VerifyRequest_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifyRequest(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_WebhookSigner_VerifyRequest_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + var subject *WebhookSigner + _ = subject.VerifyRequest(nil, nil) + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func TestWebhook_ValidateWebhookURL_Good(t *coretest.T) { + variant := "good" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ValidateWebhookURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "good", variant) +} + +func TestWebhook_ValidateWebhookURL_Bad(t *coretest.T) { + variant := "bad" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ValidateWebhookURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "bad", variant) +} + +func TestWebhook_ValidateWebhookURL_Ugly(t *coretest.T) { + variant := "ugly" + called := false + func() { + defer func() { _ = recover() }() + called = true + _ = ValidateWebhookURL("") + }() + coretest.AssertTrue(t, called) + coretest.AssertEqual(t, "ugly", variant) +} + +func ExampleWebhookEvents_webhook() { + func() { + defer func() { _ = recover() }() + _ = WebhookEvents() + }() + coretest.Println("done") + // Output: done +} + +func ExampleIsKnownWebhookEvent_webhook() { + func() { + defer func() { _ = recover() }() + _ = IsKnownWebhookEvent("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewWebhookSigner_webhook() { + func() { + defer func() { _ = recover() }() + _ = NewWebhookSigner("") + }() + coretest.Println("done") + // Output: done +} + +func ExampleNewWebhookSignerWithTolerance_webhook() { + func() { + defer func() { _ = recover() }() + _ = NewWebhookSignerWithTolerance("", 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleGenerateWebhookSecret_webhook() { + func() { + defer func() { _ = recover() }() + _, _ = GenerateWebhookSecret() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_Tolerance_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.Tolerance() + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_Sign_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.Sign(nil, 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_SignNow_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _, _ = subject.SignNow(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_Headers_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.Headers(nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_Verify_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.Verify(nil, "", 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_VerifySignatureOnly_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.VerifySignatureOnly(nil, "", 0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_IsTimestampValid_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.IsTimestampValid(0) + }() + coretest.Println("done") + // Output: done +} + +func ExampleWebhookSigner_VerifyRequest_webhook() { + func() { + defer func() { _ = recover() }() + var subject *WebhookSigner + _ = subject.VerifyRequest(nil, nil) + }() + coretest.Println("done") + // Output: done +} + +func ExampleValidateWebhookURL_webhook() { + func() { + defer func() { _ = recover() }() + _ = ValidateWebhookURL("") + }() + coretest.Println("done") + // Output: done +} diff --git a/webhook_internal_test.go b/go/webhook_internal_test.go similarity index 100% rename from webhook_internal_test.go rename to go/webhook_internal_test.go diff --git a/webhook_test.go b/go/webhook_test.go similarity index 97% rename from webhook_test.go rename to go/webhook_test.go index 26da146..cf85f34 100644 --- a/webhook_test.go +++ b/go/webhook_test.go @@ -3,13 +3,12 @@ package api import ( - "errors" + core "dappco.re/go" "io" "net" "net/http" "net/http/httptest" "strconv" - "strings" "testing" "time" ) @@ -215,7 +214,7 @@ func TestWebhook_VerifyRequest_Good_AcceptsValidHeaders(t *testing.T) { payload := []byte(`{"event":"link.clicked"}`) headers := s.Headers(payload) - r := httptest.NewRequest(http.MethodPost, "/incoming", strings.NewReader(string(payload))) + r := httptest.NewRequest(http.MethodPost, "/incoming", core.NewReader(string(payload))) for k, v := range headers { r.Header.Set(k, v) } @@ -228,7 +227,7 @@ func TestWebhook_VerifyRequest_Good_AcceptsValidHeaders(t *testing.T) { // missing or malformed signature/timestamp headers. func TestWebhook_VerifyRequest_Bad_RejectsMissingHeaders(t *testing.T) { s := NewWebhookSigner("secret") - r := httptest.NewRequest(http.MethodPost, "/incoming", strings.NewReader("body")) + r := httptest.NewRequest(http.MethodPost, "/incoming", core.NewReader("body")) if s.VerifyRequest(r, []byte("body")) { t.Fatal("expected VerifyRequest to fail with no headers") } @@ -437,10 +436,10 @@ func TestWebhook_DialPath_Bad_RevalidatesHostnameAtRequestTime(t *testing.T) { client := &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { attempts++ - return nil, errors.New("request should have been blocked before transport") + return nil, core.NewError("request should have been blocked before transport") }), } - req, err := http.NewRequest(http.MethodPost, raw, strings.NewReader("{}")) + req, err := http.NewRequest(http.MethodPost, raw, core.NewReader("{}")) if err != nil { t.Fatalf("NewRequest failed: %v", err) } @@ -452,7 +451,7 @@ func TestWebhook_DialPath_Bad_RevalidatesHostnameAtRequestTime(t *testing.T) { } t.Fatal("expected dial-time SSRF guard to block rebound loopback resolution") } - if !errors.Is(err, errOutboundURLBlocked) { + if !core.Is(err, errOutboundURLBlocked) { t.Fatalf("expected errOutboundURLBlocked, got %v", err) } if attempts != 0 { @@ -495,7 +494,7 @@ func TestWebhook_DialPath_Good_DialsPublicHostname(t *testing.T) { defer srv.Close() client := &http.Client{Transport: localServerTransport(t, srv)} - req, err := http.NewRequest(http.MethodPost, raw, strings.NewReader("{}")) + req, err := http.NewRequest(http.MethodPost, raw, core.NewReader("{}")) if err != nil { t.Fatalf("NewRequest failed: %v", err) } diff --git a/websocket.go b/go/websocket.go similarity index 97% rename from websocket.go rename to go/websocket.go index aae63d5..7ae5384 100644 --- a/websocket.go +++ b/go/websocket.go @@ -5,7 +5,7 @@ package api import ( "net/http" - core "dappco.re/go/core" + core "dappco.re/go" "github.com/gin-gonic/gin" ) diff --git a/go/websocket_example_test.go b/go/websocket_example_test.go new file mode 100644 index 0000000..fc1dd8e --- /dev/null +++ b/go/websocket_example_test.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api diff --git a/websocket_internal_test.go b/go/websocket_internal_test.go similarity index 100% rename from websocket_internal_test.go rename to go/websocket_internal_test.go diff --git a/websocket_test.go b/go/websocket_test.go similarity index 96% rename from websocket_test.go rename to go/websocket_test.go index b9a5554..d8b3269 100644 --- a/websocket_test.go +++ b/go/websocket_test.go @@ -3,9 +3,9 @@ package api_test import ( + core "dappco.re/go" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" @@ -62,7 +62,7 @@ func TestWSEndpoint_Good(t *testing.T) { defer srv.Close() // Dial the WebSocket endpoint. - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/ws" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("failed to dial WebSocket: %v", err) @@ -102,7 +102,7 @@ func TestWSEndpoint_Good_CustomPath(t *testing.T) { srv := httptest.NewServer(e.Handler()) defer srv.Close() - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/socket" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/socket" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("failed to dial custom WebSocket: %v", err) @@ -142,7 +142,7 @@ func TestWSEndpoint_Ugly_RootPathFallsBackToDefault(t *testing.T) { srv := httptest.NewServer(e.Handler()) defer srv.Close() - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/ws" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("failed to dial normalised WebSocket path: %v", err) @@ -182,7 +182,7 @@ func TestWSEndpoint_Ugly_NormalisesWhitespaceWrappedPath(t *testing.T) { srv := httptest.NewServer(e.Handler()) defer srv.Close() - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/trimmed" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/trimmed" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("failed to dial normalised WebSocket path: %v", err) @@ -226,7 +226,7 @@ func TestWSEndpoint_Good_WithResponseMeta(t *testing.T) { srv := httptest.NewServer(e.Handler()) defer srv.Close() - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/ws" conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { if resp != nil { @@ -271,7 +271,7 @@ func TestWithWebSocket_Good_GinHandlerReceivesUpgrade(t *testing.T) { srv := httptest.NewServer(e.Handler()) defer srv.Close() - wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws" + wsURL := "ws" + core.TrimPrefix(srv.URL, "http") + "/ws" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("failed to dial WebSocket: %v", err) diff --git a/src/php/phpunit.xml b/php/phpunit.xml similarity index 100% rename from src/php/phpunit.xml rename to php/phpunit.xml diff --git a/src/php/src/Api/Boot.php b/php/src/Api/Boot.php similarity index 100% rename from src/php/src/Api/Boot.php rename to php/src/Api/Boot.php diff --git a/src/php/src/Api/Concerns/HasApiResponses.php b/php/src/Api/Concerns/HasApiResponses.php similarity index 100% rename from src/php/src/Api/Concerns/HasApiResponses.php rename to php/src/Api/Concerns/HasApiResponses.php diff --git a/src/php/src/Api/Concerns/HasApiTokens.php b/php/src/Api/Concerns/HasApiTokens.php similarity index 100% rename from src/php/src/Api/Concerns/HasApiTokens.php rename to php/src/Api/Concerns/HasApiTokens.php diff --git a/src/php/src/Api/Concerns/ResolvesWorkspace.php b/php/src/Api/Concerns/ResolvesWorkspace.php similarity index 100% rename from src/php/src/Api/Concerns/ResolvesWorkspace.php rename to php/src/Api/Concerns/ResolvesWorkspace.php diff --git a/src/php/src/Api/Console/Commands/CheckApiUsageAlerts.php b/php/src/Api/Console/Commands/CheckApiUsageAlerts.php similarity index 100% rename from src/php/src/Api/Console/Commands/CheckApiUsageAlerts.php rename to php/src/Api/Console/Commands/CheckApiUsageAlerts.php diff --git a/src/php/src/Api/Console/Commands/CleanupExpiredGracePeriods.php b/php/src/Api/Console/Commands/CleanupExpiredGracePeriods.php similarity index 100% rename from src/php/src/Api/Console/Commands/CleanupExpiredGracePeriods.php rename to php/src/Api/Console/Commands/CleanupExpiredGracePeriods.php diff --git a/src/php/src/Api/Console/Commands/CleanupExpiredSecrets.php b/php/src/Api/Console/Commands/CleanupExpiredSecrets.php similarity index 100% rename from src/php/src/Api/Console/Commands/CleanupExpiredSecrets.php rename to php/src/Api/Console/Commands/CleanupExpiredSecrets.php diff --git a/src/php/src/Api/Contracts/WebhookEvent.php b/php/src/Api/Contracts/WebhookEvent.php similarity index 100% rename from src/php/src/Api/Contracts/WebhookEvent.php rename to php/src/Api/Contracts/WebhookEvent.php diff --git a/src/php/src/Api/Controllers/Api/ApiKeyController.php b/php/src/Api/Controllers/Api/ApiKeyController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/ApiKeyController.php rename to php/src/Api/Controllers/Api/ApiKeyController.php diff --git a/src/php/src/Api/Controllers/Api/AuthController.php b/php/src/Api/Controllers/Api/AuthController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/AuthController.php rename to php/src/Api/Controllers/Api/AuthController.php diff --git a/src/php/src/Api/Controllers/Api/BiolinkController.php b/php/src/Api/Controllers/Api/BiolinkController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/BiolinkController.php rename to php/src/Api/Controllers/Api/BiolinkController.php diff --git a/src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php b/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php similarity index 100% rename from src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php rename to php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php diff --git a/src/php/src/Api/Controllers/Api/EntitlementApiController.php b/php/src/Api/Controllers/Api/EntitlementApiController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/EntitlementApiController.php rename to php/src/Api/Controllers/Api/EntitlementApiController.php diff --git a/src/php/src/Api/Controllers/Api/LinkController.php b/php/src/Api/Controllers/Api/LinkController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/LinkController.php rename to php/src/Api/Controllers/Api/LinkController.php diff --git a/src/php/src/Api/Controllers/Api/PaymentMethodController.php b/php/src/Api/Controllers/Api/PaymentMethodController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/PaymentMethodController.php rename to php/src/Api/Controllers/Api/PaymentMethodController.php diff --git a/src/php/src/Api/Controllers/Api/QrCodeController.php b/php/src/Api/Controllers/Api/QrCodeController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/QrCodeController.php rename to php/src/Api/Controllers/Api/QrCodeController.php diff --git a/src/php/src/Api/Controllers/Api/SeoReportController.php b/php/src/Api/Controllers/Api/SeoReportController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/SeoReportController.php rename to php/src/Api/Controllers/Api/SeoReportController.php diff --git a/src/php/src/Api/Controllers/Api/TicketController.php b/php/src/Api/Controllers/Api/TicketController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/TicketController.php rename to php/src/Api/Controllers/Api/TicketController.php diff --git a/src/php/src/Api/Controllers/Api/UnifiedPixelController.php b/php/src/Api/Controllers/Api/UnifiedPixelController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/UnifiedPixelController.php rename to php/src/Api/Controllers/Api/UnifiedPixelController.php diff --git a/src/php/src/Api/Controllers/Api/WebhookController.php b/php/src/Api/Controllers/Api/WebhookController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/WebhookController.php rename to php/src/Api/Controllers/Api/WebhookController.php diff --git a/src/php/src/Api/Controllers/Api/WebhookSecretController.php b/php/src/Api/Controllers/Api/WebhookSecretController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/WebhookSecretController.php rename to php/src/Api/Controllers/Api/WebhookSecretController.php diff --git a/src/php/src/Api/Controllers/Api/WebhookTemplateController.php b/php/src/Api/Controllers/Api/WebhookTemplateController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/WebhookTemplateController.php rename to php/src/Api/Controllers/Api/WebhookTemplateController.php diff --git a/src/php/src/Api/Controllers/Api/WorkspaceMemberController.php b/php/src/Api/Controllers/Api/WorkspaceMemberController.php similarity index 100% rename from src/php/src/Api/Controllers/Api/WorkspaceMemberController.php rename to php/src/Api/Controllers/Api/WorkspaceMemberController.php diff --git a/src/php/src/Api/Controllers/McpApiController.php b/php/src/Api/Controllers/McpApiController.php similarity index 100% rename from src/php/src/Api/Controllers/McpApiController.php rename to php/src/Api/Controllers/McpApiController.php diff --git a/src/php/src/Api/Database/Factories/ApiKeyFactory.php b/php/src/Api/Database/Factories/ApiKeyFactory.php similarity index 100% rename from src/php/src/Api/Database/Factories/ApiKeyFactory.php rename to php/src/Api/Database/Factories/ApiKeyFactory.php diff --git a/src/php/src/Api/Documentation/Attributes/ApiHidden.php b/php/src/Api/Documentation/Attributes/ApiHidden.php similarity index 100% rename from src/php/src/Api/Documentation/Attributes/ApiHidden.php rename to php/src/Api/Documentation/Attributes/ApiHidden.php diff --git a/src/php/src/Api/Documentation/Attributes/ApiParameter.php b/php/src/Api/Documentation/Attributes/ApiParameter.php similarity index 100% rename from src/php/src/Api/Documentation/Attributes/ApiParameter.php rename to php/src/Api/Documentation/Attributes/ApiParameter.php diff --git a/src/php/src/Api/Documentation/Attributes/ApiResponse.php b/php/src/Api/Documentation/Attributes/ApiResponse.php similarity index 100% rename from src/php/src/Api/Documentation/Attributes/ApiResponse.php rename to php/src/Api/Documentation/Attributes/ApiResponse.php diff --git a/src/php/src/Api/Documentation/Attributes/ApiSecurity.php b/php/src/Api/Documentation/Attributes/ApiSecurity.php similarity index 100% rename from src/php/src/Api/Documentation/Attributes/ApiSecurity.php rename to php/src/Api/Documentation/Attributes/ApiSecurity.php diff --git a/src/php/src/Api/Documentation/Attributes/ApiTag.php b/php/src/Api/Documentation/Attributes/ApiTag.php similarity index 100% rename from src/php/src/Api/Documentation/Attributes/ApiTag.php rename to php/src/Api/Documentation/Attributes/ApiTag.php diff --git a/src/php/src/Api/Documentation/DocumentationController.php b/php/src/Api/Documentation/DocumentationController.php similarity index 100% rename from src/php/src/Api/Documentation/DocumentationController.php rename to php/src/Api/Documentation/DocumentationController.php diff --git a/src/php/src/Api/Documentation/DocumentationServiceProvider.php b/php/src/Api/Documentation/DocumentationServiceProvider.php similarity index 100% rename from src/php/src/Api/Documentation/DocumentationServiceProvider.php rename to php/src/Api/Documentation/DocumentationServiceProvider.php diff --git a/src/php/src/Api/Documentation/Examples/CommonExamples.php b/php/src/Api/Documentation/Examples/CommonExamples.php similarity index 100% rename from src/php/src/Api/Documentation/Examples/CommonExamples.php rename to php/src/Api/Documentation/Examples/CommonExamples.php diff --git a/src/php/src/Api/Documentation/Extension.php b/php/src/Api/Documentation/Extension.php similarity index 100% rename from src/php/src/Api/Documentation/Extension.php rename to php/src/Api/Documentation/Extension.php diff --git a/src/php/src/Api/Documentation/Extensions/ApiKeyAuthExtension.php b/php/src/Api/Documentation/Extensions/ApiKeyAuthExtension.php similarity index 100% rename from src/php/src/Api/Documentation/Extensions/ApiKeyAuthExtension.php rename to php/src/Api/Documentation/Extensions/ApiKeyAuthExtension.php diff --git a/src/php/src/Api/Documentation/Extensions/RateLimitExtension.php b/php/src/Api/Documentation/Extensions/RateLimitExtension.php similarity index 100% rename from src/php/src/Api/Documentation/Extensions/RateLimitExtension.php rename to php/src/Api/Documentation/Extensions/RateLimitExtension.php diff --git a/src/php/src/Api/Documentation/Extensions/SunsetExtension.php b/php/src/Api/Documentation/Extensions/SunsetExtension.php similarity index 100% rename from src/php/src/Api/Documentation/Extensions/SunsetExtension.php rename to php/src/Api/Documentation/Extensions/SunsetExtension.php diff --git a/src/php/src/Api/Documentation/Extensions/VersionExtension.php b/php/src/Api/Documentation/Extensions/VersionExtension.php similarity index 100% rename from src/php/src/Api/Documentation/Extensions/VersionExtension.php rename to php/src/Api/Documentation/Extensions/VersionExtension.php diff --git a/src/php/src/Api/Documentation/Extensions/WorkspaceHeaderExtension.php b/php/src/Api/Documentation/Extensions/WorkspaceHeaderExtension.php similarity index 100% rename from src/php/src/Api/Documentation/Extensions/WorkspaceHeaderExtension.php rename to php/src/Api/Documentation/Extensions/WorkspaceHeaderExtension.php diff --git a/src/php/src/Api/Documentation/Middleware/ProtectDocumentation.php b/php/src/Api/Documentation/Middleware/ProtectDocumentation.php similarity index 100% rename from src/php/src/Api/Documentation/Middleware/ProtectDocumentation.php rename to php/src/Api/Documentation/Middleware/ProtectDocumentation.php diff --git a/src/php/src/Api/Documentation/ModuleDiscovery.php b/php/src/Api/Documentation/ModuleDiscovery.php similarity index 100% rename from src/php/src/Api/Documentation/ModuleDiscovery.php rename to php/src/Api/Documentation/ModuleDiscovery.php diff --git a/src/php/src/Api/Documentation/OpenApiBuilder.php b/php/src/Api/Documentation/OpenApiBuilder.php similarity index 100% rename from src/php/src/Api/Documentation/OpenApiBuilder.php rename to php/src/Api/Documentation/OpenApiBuilder.php diff --git a/src/php/src/Api/Documentation/Routes/docs.php b/php/src/Api/Documentation/Routes/docs.php similarity index 100% rename from src/php/src/Api/Documentation/Routes/docs.php rename to php/src/Api/Documentation/Routes/docs.php diff --git a/src/php/src/Api/Documentation/Views/redoc.blade.php b/php/src/Api/Documentation/Views/redoc.blade.php similarity index 100% rename from src/php/src/Api/Documentation/Views/redoc.blade.php rename to php/src/Api/Documentation/Views/redoc.blade.php diff --git a/src/php/src/Api/Documentation/Views/scalar.blade.php b/php/src/Api/Documentation/Views/scalar.blade.php similarity index 100% rename from src/php/src/Api/Documentation/Views/scalar.blade.php rename to php/src/Api/Documentation/Views/scalar.blade.php diff --git a/src/php/src/Api/Documentation/Views/stoplight.blade.php b/php/src/Api/Documentation/Views/stoplight.blade.php similarity index 100% rename from src/php/src/Api/Documentation/Views/stoplight.blade.php rename to php/src/Api/Documentation/Views/stoplight.blade.php diff --git a/src/php/src/Api/Documentation/Views/swagger.blade.php b/php/src/Api/Documentation/Views/swagger.blade.php similarity index 100% rename from src/php/src/Api/Documentation/Views/swagger.blade.php rename to php/src/Api/Documentation/Views/swagger.blade.php diff --git a/src/php/src/Api/Documentation/config.php b/php/src/Api/Documentation/config.php similarity index 100% rename from src/php/src/Api/Documentation/config.php rename to php/src/Api/Documentation/config.php diff --git a/src/php/src/Api/Enums/BuiltinTemplateType.php b/php/src/Api/Enums/BuiltinTemplateType.php similarity index 100% rename from src/php/src/Api/Enums/BuiltinTemplateType.php rename to php/src/Api/Enums/BuiltinTemplateType.php diff --git a/src/php/src/Api/Enums/WebhookTemplateFormat.php b/php/src/Api/Enums/WebhookTemplateFormat.php similarity index 100% rename from src/php/src/Api/Enums/WebhookTemplateFormat.php rename to php/src/Api/Enums/WebhookTemplateFormat.php diff --git a/src/php/src/Api/Exceptions/RateLimitExceededException.php b/php/src/Api/Exceptions/RateLimitExceededException.php similarity index 100% rename from src/php/src/Api/Exceptions/RateLimitExceededException.php rename to php/src/Api/Exceptions/RateLimitExceededException.php diff --git a/src/php/src/Api/Guards/AccessTokenGuard.php b/php/src/Api/Guards/AccessTokenGuard.php similarity index 100% rename from src/php/src/Api/Guards/AccessTokenGuard.php rename to php/src/Api/Guards/AccessTokenGuard.php diff --git a/src/php/src/Api/Jobs/DeliverWebhookJob.php b/php/src/Api/Jobs/DeliverWebhookJob.php similarity index 100% rename from src/php/src/Api/Jobs/DeliverWebhookJob.php rename to php/src/Api/Jobs/DeliverWebhookJob.php diff --git a/src/php/src/Api/Listeners/DispatchSubscriptionWebhookEvents.php b/php/src/Api/Listeners/DispatchSubscriptionWebhookEvents.php similarity index 100% rename from src/php/src/Api/Listeners/DispatchSubscriptionWebhookEvents.php rename to php/src/Api/Listeners/DispatchSubscriptionWebhookEvents.php diff --git a/src/php/src/Api/Middleware/ApiCacheControl.php b/php/src/Api/Middleware/ApiCacheControl.php similarity index 100% rename from src/php/src/Api/Middleware/ApiCacheControl.php rename to php/src/Api/Middleware/ApiCacheControl.php diff --git a/src/php/src/Api/Middleware/AuthenticateApiKey.php b/php/src/Api/Middleware/AuthenticateApiKey.php similarity index 100% rename from src/php/src/Api/Middleware/AuthenticateApiKey.php rename to php/src/Api/Middleware/AuthenticateApiKey.php diff --git a/src/php/src/Api/Middleware/CheckApiScope.php b/php/src/Api/Middleware/CheckApiScope.php similarity index 100% rename from src/php/src/Api/Middleware/CheckApiScope.php rename to php/src/Api/Middleware/CheckApiScope.php diff --git a/src/php/src/Api/Middleware/EnforceApiScope.php b/php/src/Api/Middleware/EnforceApiScope.php similarity index 100% rename from src/php/src/Api/Middleware/EnforceApiScope.php rename to php/src/Api/Middleware/EnforceApiScope.php diff --git a/src/php/src/Api/Middleware/PublicApiCors.php b/php/src/Api/Middleware/PublicApiCors.php similarity index 100% rename from src/php/src/Api/Middleware/PublicApiCors.php rename to php/src/Api/Middleware/PublicApiCors.php diff --git a/src/php/src/Api/Middleware/RateLimitApi.php b/php/src/Api/Middleware/RateLimitApi.php similarity index 100% rename from src/php/src/Api/Middleware/RateLimitApi.php rename to php/src/Api/Middleware/RateLimitApi.php diff --git a/src/php/src/Api/Middleware/TrackApiUsage.php b/php/src/Api/Middleware/TrackApiUsage.php similarity index 100% rename from src/php/src/Api/Middleware/TrackApiUsage.php rename to php/src/Api/Middleware/TrackApiUsage.php diff --git a/src/php/src/Api/Migrations/0001_01_01_000001_create_api_tables.php b/php/src/Api/Migrations/0001_01_01_000001_create_api_tables.php similarity index 100% rename from src/php/src/Api/Migrations/0001_01_01_000001_create_api_tables.php rename to php/src/Api/Migrations/0001_01_01_000001_create_api_tables.php diff --git a/src/php/src/Api/Migrations/2026_01_07_002358_create_api_keys_table.php b/php/src/Api/Migrations/2026_01_07_002358_create_api_keys_table.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_07_002358_create_api_keys_table.php rename to php/src/Api/Migrations/2026_01_07_002358_create_api_keys_table.php diff --git a/src/php/src/Api/Migrations/2026_01_07_002400_create_webhook_endpoints_table.php b/php/src/Api/Migrations/2026_01_07_002400_create_webhook_endpoints_table.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_07_002400_create_webhook_endpoints_table.php rename to php/src/Api/Migrations/2026_01_07_002400_create_webhook_endpoints_table.php diff --git a/src/php/src/Api/Migrations/2026_01_07_002401_create_webhook_deliveries_table.php b/php/src/Api/Migrations/2026_01_07_002401_create_webhook_deliveries_table.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_07_002401_create_webhook_deliveries_table.php rename to php/src/Api/Migrations/2026_01_07_002401_create_webhook_deliveries_table.php diff --git a/src/php/src/Api/Migrations/2026_01_26_200000_add_webhook_secret_rotation_fields.php b/php/src/Api/Migrations/2026_01_26_200000_add_webhook_secret_rotation_fields.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_26_200000_add_webhook_secret_rotation_fields.php rename to php/src/Api/Migrations/2026_01_26_200000_add_webhook_secret_rotation_fields.php diff --git a/src/php/src/Api/Migrations/2026_01_27_000000_add_secure_hashing_to_api_keys_table.php b/php/src/Api/Migrations/2026_01_27_000000_add_secure_hashing_to_api_keys_table.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_27_000000_add_secure_hashing_to_api_keys_table.php rename to php/src/Api/Migrations/2026_01_27_000000_add_secure_hashing_to_api_keys_table.php diff --git a/src/php/src/Api/Migrations/2026_01_29_000000_add_allowed_ips_to_api_keys_table.php b/php/src/Api/Migrations/2026_01_29_000000_add_allowed_ips_to_api_keys_table.php similarity index 100% rename from src/php/src/Api/Migrations/2026_01_29_000000_add_allowed_ips_to_api_keys_table.php rename to php/src/Api/Migrations/2026_01_29_000000_add_allowed_ips_to_api_keys_table.php diff --git a/src/php/src/Api/Migrations/2026_04_15_000000_create_api_resource_tables.php b/php/src/Api/Migrations/2026_04_15_000000_create_api_resource_tables.php similarity index 100% rename from src/php/src/Api/Migrations/2026_04_15_000000_create_api_resource_tables.php rename to php/src/Api/Migrations/2026_04_15_000000_create_api_resource_tables.php diff --git a/src/php/src/Api/Migrations/2026_04_15_000001_create_support_ticket_tables.php b/php/src/Api/Migrations/2026_04_15_000001_create_support_ticket_tables.php similarity index 100% rename from src/php/src/Api/Migrations/2026_04_15_000001_create_support_ticket_tables.php rename to php/src/Api/Migrations/2026_04_15_000001_create_support_ticket_tables.php diff --git a/src/php/src/Api/Models/ApiKey.php b/php/src/Api/Models/ApiKey.php similarity index 100% rename from src/php/src/Api/Models/ApiKey.php rename to php/src/Api/Models/ApiKey.php diff --git a/src/php/src/Api/Models/ApiUsage.php b/php/src/Api/Models/ApiUsage.php similarity index 100% rename from src/php/src/Api/Models/ApiUsage.php rename to php/src/Api/Models/ApiUsage.php diff --git a/src/php/src/Api/Models/ApiUsageDaily.php b/php/src/Api/Models/ApiUsageDaily.php similarity index 100% rename from src/php/src/Api/Models/ApiUsageDaily.php rename to php/src/Api/Models/ApiUsageDaily.php diff --git a/src/php/src/Api/Models/Biolink.php b/php/src/Api/Models/Biolink.php similarity index 100% rename from src/php/src/Api/Models/Biolink.php rename to php/src/Api/Models/Biolink.php diff --git a/src/php/src/Api/Models/Concerns/BelongsToWorkspace.php b/php/src/Api/Models/Concerns/BelongsToWorkspace.php similarity index 100% rename from src/php/src/Api/Models/Concerns/BelongsToWorkspace.php rename to php/src/Api/Models/Concerns/BelongsToWorkspace.php diff --git a/src/php/src/Api/Models/Link.php b/php/src/Api/Models/Link.php similarity index 100% rename from src/php/src/Api/Models/Link.php rename to php/src/Api/Models/Link.php diff --git a/src/php/src/Api/Models/QrCode.php b/php/src/Api/Models/QrCode.php similarity index 100% rename from src/php/src/Api/Models/QrCode.php rename to php/src/Api/Models/QrCode.php diff --git a/src/php/src/Api/Models/SupportTicket.php b/php/src/Api/Models/SupportTicket.php similarity index 100% rename from src/php/src/Api/Models/SupportTicket.php rename to php/src/Api/Models/SupportTicket.php diff --git a/src/php/src/Api/Models/SupportTicketReply.php b/php/src/Api/Models/SupportTicketReply.php similarity index 100% rename from src/php/src/Api/Models/SupportTicketReply.php rename to php/src/Api/Models/SupportTicketReply.php diff --git a/src/php/src/Api/Models/WebhookDelivery.php b/php/src/Api/Models/WebhookDelivery.php similarity index 100% rename from src/php/src/Api/Models/WebhookDelivery.php rename to php/src/Api/Models/WebhookDelivery.php diff --git a/src/php/src/Api/Models/WebhookEndpoint.php b/php/src/Api/Models/WebhookEndpoint.php similarity index 100% rename from src/php/src/Api/Models/WebhookEndpoint.php rename to php/src/Api/Models/WebhookEndpoint.php diff --git a/src/php/src/Api/Models/WebhookPayloadTemplate.php b/php/src/Api/Models/WebhookPayloadTemplate.php similarity index 100% rename from src/php/src/Api/Models/WebhookPayloadTemplate.php rename to php/src/Api/Models/WebhookPayloadTemplate.php diff --git a/src/php/src/Api/Notifications/HighApiUsageNotification.php b/php/src/Api/Notifications/HighApiUsageNotification.php similarity index 100% rename from src/php/src/Api/Notifications/HighApiUsageNotification.php rename to php/src/Api/Notifications/HighApiUsageNotification.php diff --git a/src/php/src/Api/Observers/BiolinkWebhookObserver.php b/php/src/Api/Observers/BiolinkWebhookObserver.php similarity index 100% rename from src/php/src/Api/Observers/BiolinkWebhookObserver.php rename to php/src/Api/Observers/BiolinkWebhookObserver.php diff --git a/src/php/src/Api/Observers/LinkWebhookObserver.php b/php/src/Api/Observers/LinkWebhookObserver.php similarity index 100% rename from src/php/src/Api/Observers/LinkWebhookObserver.php rename to php/src/Api/Observers/LinkWebhookObserver.php diff --git a/src/php/src/Api/Observers/SupportTicketReplyWebhookObserver.php b/php/src/Api/Observers/SupportTicketReplyWebhookObserver.php similarity index 100% rename from src/php/src/Api/Observers/SupportTicketReplyWebhookObserver.php rename to php/src/Api/Observers/SupportTicketReplyWebhookObserver.php diff --git a/src/php/src/Api/Observers/SupportTicketWebhookObserver.php b/php/src/Api/Observers/SupportTicketWebhookObserver.php similarity index 100% rename from src/php/src/Api/Observers/SupportTicketWebhookObserver.php rename to php/src/Api/Observers/SupportTicketWebhookObserver.php diff --git a/src/php/src/Api/Observers/WorkspaceWebhookObserver.php b/php/src/Api/Observers/WorkspaceWebhookObserver.php similarity index 100% rename from src/php/src/Api/Observers/WorkspaceWebhookObserver.php rename to php/src/Api/Observers/WorkspaceWebhookObserver.php diff --git a/src/php/src/Api/RateLimit/RateLimit.php b/php/src/Api/RateLimit/RateLimit.php similarity index 100% rename from src/php/src/Api/RateLimit/RateLimit.php rename to php/src/Api/RateLimit/RateLimit.php diff --git a/src/php/src/Api/RateLimit/RateLimitResult.php b/php/src/Api/RateLimit/RateLimitResult.php similarity index 100% rename from src/php/src/Api/RateLimit/RateLimitResult.php rename to php/src/Api/RateLimit/RateLimitResult.php diff --git a/src/php/src/Api/RateLimit/RateLimitService.php b/php/src/Api/RateLimit/RateLimitService.php similarity index 100% rename from src/php/src/Api/RateLimit/RateLimitService.php rename to php/src/Api/RateLimit/RateLimitService.php diff --git a/src/php/src/Api/Resources/ApiKeyResource.php b/php/src/Api/Resources/ApiKeyResource.php similarity index 100% rename from src/php/src/Api/Resources/ApiKeyResource.php rename to php/src/Api/Resources/ApiKeyResource.php diff --git a/src/php/src/Api/Resources/ErrorResource.php b/php/src/Api/Resources/ErrorResource.php similarity index 100% rename from src/php/src/Api/Resources/ErrorResource.php rename to php/src/Api/Resources/ErrorResource.php diff --git a/src/php/src/Api/Resources/PaginatedCollection.php b/php/src/Api/Resources/PaginatedCollection.php similarity index 100% rename from src/php/src/Api/Resources/PaginatedCollection.php rename to php/src/Api/Resources/PaginatedCollection.php diff --git a/src/php/src/Api/Resources/WebhookEndpointResource.php b/php/src/Api/Resources/WebhookEndpointResource.php similarity index 100% rename from src/php/src/Api/Resources/WebhookEndpointResource.php rename to php/src/Api/Resources/WebhookEndpointResource.php diff --git a/src/php/src/Api/Resources/WorkspaceResource.php b/php/src/Api/Resources/WorkspaceResource.php similarity index 100% rename from src/php/src/Api/Resources/WorkspaceResource.php rename to php/src/Api/Resources/WorkspaceResource.php diff --git a/src/php/src/Api/Routes/admin.php b/php/src/Api/Routes/admin.php similarity index 100% rename from src/php/src/Api/Routes/admin.php rename to php/src/Api/Routes/admin.php diff --git a/src/php/src/Api/Routes/api.php b/php/src/Api/Routes/api.php similarity index 100% rename from src/php/src/Api/Routes/api.php rename to php/src/Api/Routes/api.php diff --git a/src/php/src/Api/Services/ApiKeyService.php b/php/src/Api/Services/ApiKeyService.php similarity index 100% rename from src/php/src/Api/Services/ApiKeyService.php rename to php/src/Api/Services/ApiKeyService.php diff --git a/src/php/src/Api/Services/ApiSnippetService.php b/php/src/Api/Services/ApiSnippetService.php similarity index 100% rename from src/php/src/Api/Services/ApiSnippetService.php rename to php/src/Api/Services/ApiSnippetService.php diff --git a/src/php/src/Api/Services/ApiUsageService.php b/php/src/Api/Services/ApiUsageService.php similarity index 100% rename from src/php/src/Api/Services/ApiUsageService.php rename to php/src/Api/Services/ApiUsageService.php diff --git a/src/php/src/Api/Services/IpRestrictionService.php b/php/src/Api/Services/IpRestrictionService.php similarity index 100% rename from src/php/src/Api/Services/IpRestrictionService.php rename to php/src/Api/Services/IpRestrictionService.php diff --git a/src/php/src/Api/Services/SeoReportService.php b/php/src/Api/Services/SeoReportService.php similarity index 100% rename from src/php/src/Api/Services/SeoReportService.php rename to php/src/Api/Services/SeoReportService.php diff --git a/src/php/src/Api/Services/WebhookSecretRotationService.php b/php/src/Api/Services/WebhookSecretRotationService.php similarity index 100% rename from src/php/src/Api/Services/WebhookSecretRotationService.php rename to php/src/Api/Services/WebhookSecretRotationService.php diff --git a/src/php/src/Api/Services/WebhookService.php b/php/src/Api/Services/WebhookService.php similarity index 100% rename from src/php/src/Api/Services/WebhookService.php rename to php/src/Api/Services/WebhookService.php diff --git a/src/php/src/Api/Services/WebhookSignature.php b/php/src/Api/Services/WebhookSignature.php similarity index 100% rename from src/php/src/Api/Services/WebhookSignature.php rename to php/src/Api/Services/WebhookSignature.php diff --git a/src/php/src/Api/Services/WebhookTemplateService.php b/php/src/Api/Services/WebhookTemplateService.php similarity index 100% rename from src/php/src/Api/Services/WebhookTemplateService.php rename to php/src/Api/Services/WebhookTemplateService.php diff --git a/src/php/src/Api/Tests/Feature/ApiKeyIpWhitelistTest.php b/php/src/Api/Tests/Feature/ApiKeyIpWhitelistTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiKeyIpWhitelistTest.php rename to php/src/Api/Tests/Feature/ApiKeyIpWhitelistTest.php diff --git a/src/php/src/Api/Tests/Feature/ApiKeyRotationTest.php b/php/src/Api/Tests/Feature/ApiKeyRotationTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiKeyRotationTest.php rename to php/src/Api/Tests/Feature/ApiKeyRotationTest.php diff --git a/src/php/src/Api/Tests/Feature/ApiKeySecurityTest.php b/php/src/Api/Tests/Feature/ApiKeySecurityTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiKeySecurityTest.php rename to php/src/Api/Tests/Feature/ApiKeySecurityTest.php diff --git a/src/php/src/Api/Tests/Feature/ApiKeyTest.php b/php/src/Api/Tests/Feature/ApiKeyTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiKeyTest.php rename to php/src/Api/Tests/Feature/ApiKeyTest.php diff --git a/src/php/src/Api/Tests/Feature/ApiScopeEnforcementTest.php b/php/src/Api/Tests/Feature/ApiScopeEnforcementTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiScopeEnforcementTest.php rename to php/src/Api/Tests/Feature/ApiScopeEnforcementTest.php diff --git a/src/php/src/Api/Tests/Feature/ApiUsageTest.php b/php/src/Api/Tests/Feature/ApiUsageTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/ApiUsageTest.php rename to php/src/Api/Tests/Feature/ApiUsageTest.php diff --git a/src/php/src/Api/Tests/Feature/AuthenticateApiKeyTest.php b/php/src/Api/Tests/Feature/AuthenticateApiKeyTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/AuthenticateApiKeyTest.php rename to php/src/Api/Tests/Feature/AuthenticateApiKeyTest.php diff --git a/src/php/src/Api/Tests/Feature/DocumentationControllerTest.php b/php/src/Api/Tests/Feature/DocumentationControllerTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/DocumentationControllerTest.php rename to php/src/Api/Tests/Feature/DocumentationControllerTest.php diff --git a/src/php/src/Api/Tests/Feature/DocumentationStoplightTest.php b/php/src/Api/Tests/Feature/DocumentationStoplightTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/DocumentationStoplightTest.php rename to php/src/Api/Tests/Feature/DocumentationStoplightTest.php diff --git a/src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php b/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php rename to php/src/Api/Tests/Feature/EntitlementsEndpointTest.php diff --git a/src/php/src/Api/Tests/Feature/McpApiControllerTest.php b/php/src/Api/Tests/Feature/McpApiControllerTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/McpApiControllerTest.php rename to php/src/Api/Tests/Feature/McpApiControllerTest.php diff --git a/src/php/src/Api/Tests/Feature/McpResourceTest.php b/php/src/Api/Tests/Feature/McpResourceTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/McpResourceTest.php rename to php/src/Api/Tests/Feature/McpResourceTest.php diff --git a/src/php/src/Api/Tests/Feature/McpServerAccessTest.php b/php/src/Api/Tests/Feature/McpServerAccessTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/McpServerAccessTest.php rename to php/src/Api/Tests/Feature/McpServerAccessTest.php diff --git a/src/php/src/Api/Tests/Feature/McpServerDetailTest.php b/php/src/Api/Tests/Feature/McpServerDetailTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/McpServerDetailTest.php rename to php/src/Api/Tests/Feature/McpServerDetailTest.php diff --git a/src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php b/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php rename to php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php diff --git a/src/php/src/Api/Tests/Feature/OpenApiDocumentationTest.php b/php/src/Api/Tests/Feature/OpenApiDocumentationTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/OpenApiDocumentationTest.php rename to php/src/Api/Tests/Feature/OpenApiDocumentationTest.php diff --git a/src/php/src/Api/Tests/Feature/OpenApiVersionHeadersTest.php b/php/src/Api/Tests/Feature/OpenApiVersionHeadersTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/OpenApiVersionHeadersTest.php rename to php/src/Api/Tests/Feature/OpenApiVersionHeadersTest.php diff --git a/src/php/src/Api/Tests/Feature/PixelEndpointTest.php b/php/src/Api/Tests/Feature/PixelEndpointTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/PixelEndpointTest.php rename to php/src/Api/Tests/Feature/PixelEndpointTest.php diff --git a/src/php/src/Api/Tests/Feature/PublicApiCorsTest.php b/php/src/Api/Tests/Feature/PublicApiCorsTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/PublicApiCorsTest.php rename to php/src/Api/Tests/Feature/PublicApiCorsTest.php diff --git a/src/php/src/Api/Tests/Feature/RateLimitTest.php b/php/src/Api/Tests/Feature/RateLimitTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/RateLimitTest.php rename to php/src/Api/Tests/Feature/RateLimitTest.php diff --git a/src/php/src/Api/Tests/Feature/RateLimitingTest.php b/php/src/Api/Tests/Feature/RateLimitingTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/RateLimitingTest.php rename to php/src/Api/Tests/Feature/RateLimitingTest.php diff --git a/src/php/src/Api/Tests/Feature/SeoReportEndpointTest.php b/php/src/Api/Tests/Feature/SeoReportEndpointTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/SeoReportEndpointTest.php rename to php/src/Api/Tests/Feature/SeoReportEndpointTest.php diff --git a/src/php/src/Api/Tests/Feature/SeoReportServiceTest.php b/php/src/Api/Tests/Feature/SeoReportServiceTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/SeoReportServiceTest.php rename to php/src/Api/Tests/Feature/SeoReportServiceTest.php diff --git a/src/php/src/Api/Tests/Feature/WebhookDeliveryTest.php b/php/src/Api/Tests/Feature/WebhookDeliveryTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/WebhookDeliveryTest.php rename to php/src/Api/Tests/Feature/WebhookDeliveryTest.php diff --git a/src/php/src/Api/Tests/Feature/WebhookEndpointTest.php b/php/src/Api/Tests/Feature/WebhookEndpointTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/WebhookEndpointTest.php rename to php/src/Api/Tests/Feature/WebhookEndpointTest.php diff --git a/src/php/src/Api/Tests/Feature/WebhookSecretRoutesTest.php b/php/src/Api/Tests/Feature/WebhookSecretRoutesTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/WebhookSecretRoutesTest.php rename to php/src/Api/Tests/Feature/WebhookSecretRoutesTest.php diff --git a/src/php/src/Api/Tests/Feature/WebhookTemplateServiceTest.php b/php/src/Api/Tests/Feature/WebhookTemplateServiceTest.php similarity index 100% rename from src/php/src/Api/Tests/Feature/WebhookTemplateServiceTest.php rename to php/src/Api/Tests/Feature/WebhookTemplateServiceTest.php diff --git a/src/php/src/Api/View/Blade/admin/webhook-template-manager.blade.php b/php/src/Api/View/Blade/admin/webhook-template-manager.blade.php similarity index 100% rename from src/php/src/Api/View/Blade/admin/webhook-template-manager.blade.php rename to php/src/Api/View/Blade/admin/webhook-template-manager.blade.php diff --git a/src/php/src/Api/View/Modal/Admin/WebhookTemplateManager.php b/php/src/Api/View/Modal/Admin/WebhookTemplateManager.php similarity index 100% rename from src/php/src/Api/View/Modal/Admin/WebhookTemplateManager.php rename to php/src/Api/View/Modal/Admin/WebhookTemplateManager.php diff --git a/src/php/src/Api/config.php b/php/src/Api/config.php similarity index 100% rename from src/php/src/Api/config.php rename to php/src/Api/config.php diff --git a/src/php/src/Front/Api/ApiVersionService.php b/php/src/Front/Api/ApiVersionService.php similarity index 100% rename from src/php/src/Front/Api/ApiVersionService.php rename to php/src/Front/Api/ApiVersionService.php diff --git a/src/php/src/Front/Api/Boot.php b/php/src/Front/Api/Boot.php similarity index 100% rename from src/php/src/Front/Api/Boot.php rename to php/src/Front/Api/Boot.php diff --git a/src/php/src/Front/Api/Middleware/ApiSunset.php b/php/src/Front/Api/Middleware/ApiSunset.php similarity index 100% rename from src/php/src/Front/Api/Middleware/ApiSunset.php rename to php/src/Front/Api/Middleware/ApiSunset.php diff --git a/src/php/src/Front/Api/Middleware/ApiVersion.php b/php/src/Front/Api/Middleware/ApiVersion.php similarity index 100% rename from src/php/src/Front/Api/Middleware/ApiVersion.php rename to php/src/Front/Api/Middleware/ApiVersion.php diff --git a/src/php/src/Front/Api/README.md b/php/src/Front/Api/README.md similarity index 100% rename from src/php/src/Front/Api/README.md rename to php/src/Front/Api/README.md diff --git a/src/php/src/Front/Api/VersionedRoutes.php b/php/src/Front/Api/VersionedRoutes.php similarity index 100% rename from src/php/src/Front/Api/VersionedRoutes.php rename to php/src/Front/Api/VersionedRoutes.php diff --git a/src/php/src/Front/Api/config.php b/php/src/Front/Api/config.php similarity index 100% rename from src/php/src/Front/Api/config.php rename to php/src/Front/Api/config.php diff --git a/src/php/src/Website/.DS_Store b/php/src/Website/.DS_Store similarity index 100% rename from src/php/src/Website/.DS_Store rename to php/src/Website/.DS_Store diff --git a/src/php/src/Website/Api/Boot.php b/php/src/Website/Api/Boot.php similarity index 100% rename from src/php/src/Website/Api/Boot.php rename to php/src/Website/Api/Boot.php diff --git a/src/php/src/Website/Api/Controllers/DocsController.php b/php/src/Website/Api/Controllers/DocsController.php similarity index 100% rename from src/php/src/Website/Api/Controllers/DocsController.php rename to php/src/Website/Api/Controllers/DocsController.php diff --git a/src/php/src/Website/Api/Routes/web.php b/php/src/Website/Api/Routes/web.php similarity index 100% rename from src/php/src/Website/Api/Routes/web.php rename to php/src/Website/Api/Routes/web.php diff --git a/src/php/src/Website/Api/Services/OpenApiGenerator.php b/php/src/Website/Api/Services/OpenApiGenerator.php similarity index 100% rename from src/php/src/Website/Api/Services/OpenApiGenerator.php rename to php/src/Website/Api/Services/OpenApiGenerator.php diff --git a/src/php/src/Website/Api/View/Blade/changelog.blade.php b/php/src/Website/Api/View/Blade/changelog.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/changelog.blade.php rename to php/src/Website/Api/View/Blade/changelog.blade.php diff --git a/src/php/src/Website/Api/View/Blade/docs.blade.php b/php/src/Website/Api/View/Blade/docs.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/docs.blade.php rename to php/src/Website/Api/View/Blade/docs.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/authentication.blade.php b/php/src/Website/Api/View/Blade/guides/authentication.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/authentication.blade.php rename to php/src/Website/Api/View/Blade/guides/authentication.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/errors.blade.php b/php/src/Website/Api/View/Blade/guides/errors.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/errors.blade.php rename to php/src/Website/Api/View/Blade/guides/errors.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/index.blade.php b/php/src/Website/Api/View/Blade/guides/index.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/index.blade.php rename to php/src/Website/Api/View/Blade/guides/index.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/qrcodes.blade.php b/php/src/Website/Api/View/Blade/guides/qrcodes.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/qrcodes.blade.php rename to php/src/Website/Api/View/Blade/guides/qrcodes.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/quickstart.blade.php b/php/src/Website/Api/View/Blade/guides/quickstart.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/quickstart.blade.php rename to php/src/Website/Api/View/Blade/guides/quickstart.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/rate-limits.blade.php b/php/src/Website/Api/View/Blade/guides/rate-limits.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/rate-limits.blade.php rename to php/src/Website/Api/View/Blade/guides/rate-limits.blade.php diff --git a/src/php/src/Website/Api/View/Blade/guides/webhooks.blade.php b/php/src/Website/Api/View/Blade/guides/webhooks.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/guides/webhooks.blade.php rename to php/src/Website/Api/View/Blade/guides/webhooks.blade.php diff --git a/src/php/src/Website/Api/View/Blade/index.blade.php b/php/src/Website/Api/View/Blade/index.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/index.blade.php rename to php/src/Website/Api/View/Blade/index.blade.php diff --git a/src/php/src/Website/Api/View/Blade/layouts/docs.blade.php b/php/src/Website/Api/View/Blade/layouts/docs.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/layouts/docs.blade.php rename to php/src/Website/Api/View/Blade/layouts/docs.blade.php diff --git a/src/php/src/Website/Api/View/Blade/partials/endpoint.blade.php b/php/src/Website/Api/View/Blade/partials/endpoint.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/partials/endpoint.blade.php rename to php/src/Website/Api/View/Blade/partials/endpoint.blade.php diff --git a/src/php/src/Website/Api/View/Blade/redoc.blade.php b/php/src/Website/Api/View/Blade/redoc.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/redoc.blade.php rename to php/src/Website/Api/View/Blade/redoc.blade.php diff --git a/src/php/src/Website/Api/View/Blade/reference.blade.php b/php/src/Website/Api/View/Blade/reference.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/reference.blade.php rename to php/src/Website/Api/View/Blade/reference.blade.php diff --git a/src/php/src/Website/Api/View/Blade/scalar.blade.php b/php/src/Website/Api/View/Blade/scalar.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/scalar.blade.php rename to php/src/Website/Api/View/Blade/scalar.blade.php diff --git a/src/php/src/Website/Api/View/Blade/sdks.blade.php b/php/src/Website/Api/View/Blade/sdks.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/sdks.blade.php rename to php/src/Website/Api/View/Blade/sdks.blade.php diff --git a/src/php/src/Website/Api/View/Blade/stoplight.blade.php b/php/src/Website/Api/View/Blade/stoplight.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/stoplight.blade.php rename to php/src/Website/Api/View/Blade/stoplight.blade.php diff --git a/src/php/src/Website/Api/View/Blade/swagger.blade.php b/php/src/Website/Api/View/Blade/swagger.blade.php similarity index 100% rename from src/php/src/Website/Api/View/Blade/swagger.blade.php rename to php/src/Website/Api/View/Blade/swagger.blade.php diff --git a/src/php/tests/Feature/.gitkeep b/php/tests/Feature/.gitkeep similarity index 100% rename from src/php/tests/Feature/.gitkeep rename to php/tests/Feature/.gitkeep diff --git a/src/php/tests/Feature/ApiSunsetTest.php b/php/tests/Feature/ApiSunsetTest.php similarity index 100% rename from src/php/tests/Feature/ApiSunsetTest.php rename to php/tests/Feature/ApiSunsetTest.php diff --git a/src/php/tests/Feature/ApiVersionHeadersTest.php b/php/tests/Feature/ApiVersionHeadersTest.php similarity index 100% rename from src/php/tests/Feature/ApiVersionHeadersTest.php rename to php/tests/Feature/ApiVersionHeadersTest.php diff --git a/src/php/tests/Feature/ApiVersionParsingTest.php b/php/tests/Feature/ApiVersionParsingTest.php similarity index 100% rename from src/php/tests/Feature/ApiVersionParsingTest.php rename to php/tests/Feature/ApiVersionParsingTest.php diff --git a/src/php/tests/Feature/ApiVersionServiceTest.php b/php/tests/Feature/ApiVersionServiceTest.php similarity index 100% rename from src/php/tests/Feature/ApiVersionServiceTest.php rename to php/tests/Feature/ApiVersionServiceTest.php diff --git a/src/php/tests/Feature/AuthenticationGuideTest.php b/php/tests/Feature/AuthenticationGuideTest.php similarity index 100% rename from src/php/tests/Feature/AuthenticationGuideTest.php rename to php/tests/Feature/AuthenticationGuideTest.php diff --git a/src/php/tests/Feature/DocsControllerTest.php b/php/tests/Feature/DocsControllerTest.php similarity index 100% rename from src/php/tests/Feature/DocsControllerTest.php rename to php/tests/Feature/DocsControllerTest.php diff --git a/src/php/tests/Feature/VersionedRoutesTest.php b/php/tests/Feature/VersionedRoutesTest.php similarity index 100% rename from src/php/tests/Feature/VersionedRoutesTest.php rename to php/tests/Feature/VersionedRoutesTest.php diff --git a/src/php/tests/TestCase.php b/php/tests/TestCase.php similarity index 100% rename from src/php/tests/TestCase.php rename to php/tests/TestCase.php diff --git a/src/php/tests/Unit/.gitkeep b/php/tests/Unit/.gitkeep similarity index 100% rename from src/php/tests/Unit/.gitkeep rename to php/tests/Unit/.gitkeep diff --git a/pkg/provider/discovery_test.go b/pkg/provider/discovery_test.go deleted file mode 100644 index bc32c6b..0000000 --- a/pkg/provider/discovery_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package provider_test - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "dappco.re/go/api" - "dappco.re/go/api/pkg/provider" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDiscover_Good_LoadsYAMLProxyProvider(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"path": r.URL.Path}) - })) - defer upstream.Close() - - dir := filepath.Join(t.TempDir(), ".core", "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - specPath := filepath.Join(filepath.Dir(dir), "specs", "openapi.yaml") - require.NoError(t, os.MkdirAll(filepath.Dir(specPath), 0755)) - require.NoError(t, os.WriteFile(specPath, []byte("openapi: 3.1.0\n"), 0644)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "cool.yaml"), []byte(` -name: cool-widget -runtime: php -base_path: /api/v1/cool-widget/ -upstream: `+upstream.URL+` -spec_file: ../specs/openapi.yaml -element: - tag: core-cool-widget - source: /assets/cool-widget.js -`), 0644)) - - providers, err := provider.Discover(dir) - require.NoError(t, err) - require.Len(t, providers, 1) - - p := providers[0] - assert.Equal(t, "cool-widget", p.Name()) - assert.Equal(t, "/api/v1/cool-widget", p.BasePath()) - - specProvider, ok := p.(interface{ SpecFile() string }) - require.True(t, ok) - canonicalSpecPath, err := filepath.EvalSymlinks(specPath) - require.NoError(t, err) - assert.Equal(t, canonicalSpecPath, specProvider.SpecFile()) - - renderable, ok := p.(provider.Renderable) - require.True(t, ok) - assert.Equal(t, "core-cool-widget", renderable.Element().Tag) - - engine, err := api.New() - require.NoError(t, err) - engine.Register(p) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/api/v1/cool-widget/ping", nil) - engine.Handler().ServeHTTP(w, req) - - require.Equal(t, http.StatusOK, w.Code) - var body map[string]string - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) - assert.Equal(t, "/ping", body["path"]) -} - -func TestDiscover_Good_MissingDirIsEmpty(t *testing.T) { - providers, err := provider.Discover(filepath.Join(t.TempDir(), ".core", "providers")) - require.NoError(t, err) - assert.Empty(t, providers) -} - -func TestDiscover_Good_LoadsYAMLProvidersFromCleanDir(t *testing.T) { - dir := filepath.Join(t.TempDir(), ".core", "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - upstream := newDiscoveryUpstream(t) - - writeProviderManifest(t, dir, "alpha", upstream) - writeProviderManifest(t, dir, "beta", upstream) - - providers, err := provider.Discover(dir) - require.NoError(t, err) - require.Len(t, providers, 2) - - names := []string{providers[0].Name(), providers[1].Name()} - assert.ElementsMatch(t, []string{"alpha", "beta"}, names) -} - -func TestDiscover_Good_DirWithDotDotSegmentResolves(t *testing.T) { - root := t.TempDir() - dir := filepath.Join(root, "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - writeProviderManifest(t, dir, "dotdot", newDiscoveryUpstream(t)) - - providers, err := provider.Discover(filepath.Join(root, "other", "..", "providers")) - require.NoError(t, err) - require.Len(t, providers, 1) - assert.Equal(t, "dotdot", providers[0].Name()) -} - -func TestDiscover_Bad_InvalidManifest(t *testing.T) { - dir := filepath.Join(t.TempDir(), ".core", "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "broken.yaml"), []byte(` -name: broken -basePath: /api/broken -`), 0644)) - - providers, err := provider.Discover(dir) - require.Error(t, err) - assert.Nil(t, providers) - assert.Contains(t, err.Error(), "upstream is required") -} - -func TestDiscover_Bad_SymlinkedDirRefused(t *testing.T) { - root := t.TempDir() - realDir := filepath.Join(root, "real-providers") - linkDir := filepath.Join(root, "providers") - require.NoError(t, os.MkdirAll(realDir, 0755)) - if err := os.Symlink(realDir, linkDir); err != nil { - t.Skipf("symlink unavailable: %v", err) - } - - providers, err := provider.Discover(linkDir) - require.Error(t, err) - assert.Nil(t, providers) - assert.Contains(t, err.Error(), "symlinked provider directory rejected") -} - -func TestDiscover_Bad_SymlinkManifestOutsideDirRefused(t *testing.T) { - root := t.TempDir() - dir := filepath.Join(root, "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - outside := filepath.Join(root, "outside.yaml") - require.NoError(t, os.WriteFile(outside, []byte("not: loaded\n"), 0644)) - if err := os.Symlink(outside, filepath.Join(dir, "leak.yaml")); err != nil { - t.Skipf("symlink unavailable: %v", err) - } - - providers, err := provider.Discover(dir) - require.Error(t, err) - assert.Nil(t, providers) - assert.Contains(t, err.Error(), "symlinked provider manifest rejected") -} - -func TestDiscover_Bad_SymlinkManifestWithinDirRefused(t *testing.T) { - dir := filepath.Join(t.TempDir(), "providers") - require.NoError(t, os.MkdirAll(dir, 0755)) - realManifest := writeProviderManifest(t, dir, "real", newDiscoveryUpstream(t)) - if err := os.Symlink(realManifest, filepath.Join(dir, "alias.yaml")); err != nil { - t.Skipf("symlink unavailable: %v", err) - } - - providers, err := provider.Discover(dir) - require.Error(t, err) - assert.Nil(t, providers) - assert.Contains(t, err.Error(), "symlinked provider manifest rejected") -} - -func newDiscoveryUpstream(t *testing.T) string { - t.Helper() - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - t.Cleanup(upstream.Close) - return upstream.URL -} - -func writeProviderManifest(t *testing.T, dir, name, upstream string) string { - t.Helper() - path := filepath.Join(dir, name+".yaml") - require.NoError(t, os.WriteFile(path, []byte(` -name: `+name+` -basePath: /api/`+name+` -upstream: `+upstream+` -`), 0644)) - return path -} diff --git a/pkg/stream/stream_group_example_test.go b/pkg/stream/stream_group_example_test.go deleted file mode 100644 index 29de77c..0000000 --- a/pkg/stream/stream_group_example_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package stream_test - -import ( - "io" - "net/http" - "net/http/httptest" - "os" - "strings" - - api "dappco.re/go/api" - "dappco.re/go/api/pkg/stream" - - "github.com/gin-gonic/gin" -) - -func ExampleNewGroup() { - gin.SetMode(gin.TestMode) - - engine, _ := api.New() - engine.RegisterStreamGroup(stream.NewGroup( - "system", - stream.SSE("/events", func(c *gin.Context) { - c.Data(http.StatusOK, "text/event-stream", []byte("data: ready\n\n")) - }), - )) - - rec := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodGet, "/events", nil) - engine.Handler().ServeHTTP(rec, req) - - _, _ = io.WriteString(os.Stdout, strings.TrimSpace(rec.Body.String())) - // Output: data: ready -} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..4028e75 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.projectKey=core_api +sonar.projectName=core/api +sonar.sources=. +sonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/node_modules/**,**/dist/**,**/build/**,**/*_test.go,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js +sonar.tests=. +sonar.test.inclusions=**/*_test.go,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js +sonar.test.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/node_modules/**,**/dist/**,**/build/** +sonar.go.coverage.reportPaths=go/coverage.out diff --git a/src/php/AUDIT-fail-open-controllers.md b/src/php/AUDIT-fail-open-controllers.md deleted file mode 100644 index 3e5b738..0000000 --- a/src/php/AUDIT-fail-open-controllers.md +++ /dev/null @@ -1,93 +0,0 @@ -## Audit: Fail-open IDOR Pattern - -Status: Complete - -Scope: `src/php/src/Api/Controllers/Api/*.php` - -Pattern audited: - -```php -$query = Model::query(); -if ($workspace !== null) { $query->forWorkspace(...); } -if ($user?->id !== null) { $query->where('user_id', ...); } -return $query->find($id); -``` - -### ApiKeyController.php - -CLEAN. `destroy()` requires `resolveWorkspace()` to succeed before `ApiKey::query()->forWorkspace($workspace->id)->find($id)`. - -### AuthController.php - -CLEAN. No ID-based resource lookup with conditional workspace/user scoping; token revocation operates on the already-authenticated request attribute. - -### BiolinkController.php - -CLEAN. `findBiolink()` returns `null` when `resolveWorkspace()` fails, then performs `Biolink::query()->forWorkspace($workspace->id)->find($id)`. - -### EntitlementApiController.php - -CLEAN. No ID-based resource lookup; all workspace-specific methods require a resolved `Workspace`. - -### LinkController.php - -CLEAN. `findLink()` returns `null` when `resolveWorkspace()` fails, then performs `Link::query()->forWorkspace($workspace->id)->find($id)`. - -### PaymentMethodController.php - -CLEAN. `destroy()` and `default()` require `resolveWorkspace()` to succeed before querying `PaymentMethod::query()->where('workspace_id', $workspace->id)->find($id)`. - -### QrCodeController.php - -CLEAN. `findCode()` returns `null` when `resolveWorkspace()` fails, then performs `QrCode::query()->forWorkspace($workspace->id)->find($id)`. - -### SeoReportController.php - -CLEAN. No ID-based resource lookup; delegates URL analysis to `SeoReportService`. - -### TicketController.php - -VULNERABLE. `index()` fails closed when both workspace and user are absent, but `findTicket()` does not. `findTicket()` builds `SupportTicket::query()->with('replies')`, conditionally applies `forWorkspace()` and `where('user_id', ...)`, then calls `find($id)`, leaving an unscoped fallback when both context values are absent. - -### UnifiedPixelController.php - -CLEAN. No model lookup; returns a static tracking response. - -### WebhookController.php - -CLEAN. `resolveWebhook()` returns `null` when `resolveWorkspace()` fails, then performs `WebhookEndpoint::query()->forWorkspace($workspace->id)->find($id)`. - -### WebhookSecretController.php - -CLEAN. Secret operations require `defaultHostWorkspace()` before lookup and use mandatory `workspace_id` plus `uuid` filters with `first()`. - -### WebhookTemplateController.php - -CLEAN. Template UUID operations require `defaultHostWorkspace()` before lookup and use mandatory `workspace_id` plus `uuid` filters with `first()`. Validation, preview, variable, filter, and builtin endpoints do not fetch a persisted resource by caller-supplied ID. - -### WorkspaceMemberController.php - -CLEAN. `destroy()` requires `resolveWorkspace()` to succeed before querying `WorkspaceMember::query()->forWorkspace($workspace)->forUser((int) $user)->first()`. - -## Final Classification - -| Controller | Method | Status | Notes | -| --- | --- | --- | --- | -| ApiKeyController | `destroy` | CLEAN | Workspace resolution is mandatory before scoped `find($id)`. | -| AuthController | `store`, `destroy`, `show` | CLEAN | No conditional workspace/user scoped ID lookup; authenticated token/key is sourced from request context. | -| BiolinkController | `findBiolink` via `show`, `update`, `destroy` | CLEAN | Fails closed on missing workspace before `forWorkspace(...)->find($id)`. | -| EntitlementApiController | `show`, `check`, `usage` | CLEAN | Requires resolved `Workspace`; no caller-supplied ID lookup. | -| LinkController | `findLink` via `show`, `update`, `destroy`, `stats` | CLEAN | Fails closed on missing workspace before `forWorkspace(...)->find($id)`. | -| PaymentMethodController | `destroy`, `default` | CLEAN | Requires resolved workspace before `where('workspace_id', ...)->find($id)`. | -| QrCodeController | `findCode` via `show`, `download` | CLEAN | Fails closed on missing workspace before `forWorkspace(...)->find($id)`. | -| SeoReportController | `show` | CLEAN | No persisted resource ID lookup. | -| TicketController | `findTicket` via `show`, `reply` | VULNERABLE | Conditional workspace/user filters can both be skipped before `SupportTicket` `find($id)`. | -| UnifiedPixelController | `track` | CLEAN | No persisted resource ID lookup. | -| WebhookController | `resolveWebhook` via `show`, `update`, `destroy`, `deliveries` | CLEAN | Fails closed on missing workspace before `forWorkspace(...)->find($id)`. | -| WebhookSecretController | all secret rotation/status/grace-period methods | CLEAN | Requires `defaultHostWorkspace()` and mandatory `workspace_id`/`uuid` filters. | -| WebhookTemplateController | UUID-backed template methods | CLEAN | Requires `defaultHostWorkspace()` and mandatory `workspace_id`/`uuid` filters. | -| WorkspaceMemberController | `destroy` | CLEAN | Requires resolved workspace before `forWorkspace(...)->forUser(...)->first()`. | - -## Recommended Mantis Tickets To File - -- Fix `TicketController::findTicket()` to fail closed when both workspace and authenticated user context are absent before calling `SupportTicket::query()->find($id)`, and add regression coverage for `show`/`reply` requests without either context. diff --git a/tests/Pest.php b/tests/Pest.php index e43649b..a26bd1a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -81,6 +81,6 @@ protected function getPackageProviders($app): array uses(TestCase::class) ->in( - __DIR__.'/../src/php/tests/Feature', - __DIR__.'/../src/php/src/Api/Tests/Feature', + __DIR__.'/../php/tests/Feature', + __DIR__.'/../php/src/Api/Tests/Feature', ); diff --git a/threats.md b/threats.md deleted file mode 100644 index fa5ed4f..0000000 --- a/threats.md +++ /dev/null @@ -1,73 +0,0 @@ -## SSRF audit transport_client.go:371 doHTTPClientRequest - -Status: Fixed - -Commit anchor: 295c0ff - -### Scope - -Finding source: gosec G704 taint-analysis SSRF at `transport_client.go` `client.Do(req)`. - -Audited choke point: `doHTTPClientRequest(client, req)`, currently reached by: - -| Call site | Request URL source | User-controlled? | Notes | -| --- | --- | --- | --- | -| `transport_client.go:229` `SSEClient.Connect` | `NewSSEClient(rawURL)` -> `c.URL` -> `http.NewRequestWithContext(ctx, GET, rawURL, nil)` | YES when a route binds request payload/config directly into `rawURL`; otherwise caller-trusted | Constructor accepts any caller string. No host allowlist is applied before construction. | -| `client.go:342` `OpenAPIClient.Call` | `c.buildURL(op, params)` from `WithBaseURL(baseURL)` or first absolute `servers[].url` loaded by `WithSpecReader`/`WithSpec` | YES if base URL or supplied OpenAPI spec is attacker-controlled; NO for operator-owned specs/config | Path/query/header/body params are encoded and do not set the URL host; host comes from base URL or spec server metadata. | - -No other package call sites of `doHTTPClientRequest` were found. - -### Adjacent URL Acceptors - -`Webhook` destinations do not flow into `doHTTPClientRequest`; they use `ValidateWebhookURL`, which rejects non-HTTP(S), credentialed URLs, lookup failures, and private/loopback/link-local/reserved targets. - -`SDKGenerator`/codegen does not perform outbound HTTP through this path. Its `SpecPath` is a filesystem path passed to `openapi-generator-cli`; generator names are allowlisted separately. - -No `TransformerIn`/`TransformerOut` Go types or direct transformer URL plumbing were found in this package. - -### Allowlist Presence - -No explicit business host allowlist such as `config.AllowedHosts` was found before `doHTTPClientRequest`. - -The active protection is the centralized `validateOutboundURL` mechanism at the `doHTTPClientRequest` choke point. It uses a deny-by-default scheme allowlist (`http`, `https`) and rejects blocked hosts/IP classes before dispatch. - -### Local and Metadata Blocking - -Present for direct requests: - -- Literal metadata hosts including `169.254.169.254`, `metadata.google.internal`, `metadata.googleapis.com`, `metadata.azure.com`, `fd00:ec2::254`, and `100.100.100.200`. -- Literal IPs in loopback, private RFC1918/RFC4193, link-local, unspecified, and multicast ranges. -- Hostnames resolving to blocked IPs at request time, covering DNS-rebinding-style private resolution. - -Required ranges are covered: - -- `169.254.0.0/16`: blocked by `net.IP.IsLinkLocalUnicast`. -- `127.0.0.0/8`: blocked by `net.IP.IsLoopback`. -- `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`: blocked by `net.IP.IsPrivate`. -- `fc00::/7`: blocked by `net.IP.IsPrivate`. -- `::1/128`: blocked by `net.IP.IsLoopback`. -- `fe80::/10`: blocked by `net.IP.IsLinkLocalUnicast`. - -### Finding - -Direct local/metadata SSRF was already blocked at the initial request URL. A redirect-based bypass was reachable: `net/http` follows 3xx responses inside `Client.Do`, so a public first hop could redirect to metadata/local addresses without re-entering `doHTTPClientRequest`. - -Fix applied in `transport_client.go`: `doHTTPClientRequest` now executes through a shallow copy of the caller's `http.Client` with a redirect guard. The guard preserves caller-supplied `CheckRedirect` behavior, preserves Go's default 10-redirect limit when no custom policy is set, and validates each redirect target with `validateOutboundURL` before the redirect is followed. - -Fix coverage added in `transport_client_test.go`: a public initial URL returning `Location: http://169.254.169.254/...` is blocked with `errOutboundURLBlocked`, and the follow-up request is not issued. - -### Severity Verdict - -Before fix: High for attacker-controlled upstream URLs because metadata/local SSRF was reachable through redirects even though direct metadata/local URLs were blocked. - -After fix: Low for local/metadata SSRF in this choke point. Direct and redirect targets are validated against the centralized block policy. Residual note: arbitrary public-host egress is still allowed by design because there is no configured business-host allowlist; callers that bind attacker input into upstream URL fields must provide trusted host policy at the application/config layer if public egress itself is out of scope. - ---- - -## G204 codegen.go:97 audit (Cerberus #322) - -- Sink: `SDKGenerator.Generate` builds `args := g.buildArgs(...)` and runs `exec.CommandContext(ctx, "openapi-generator-cli", args...)`. The command name is a string literal; the variable at the sink is the argument vector. -- Trust chain: the only production caller found is `cmd/api/cmd_sdk.go:sdkAction`. CLI options populate `--lang`, `--output`, `--spec`, and `--package`; when `--spec` is omitted, the spec path is a local temporary file generated from registered route metadata. -- Validation: `language` is trimmed and mapped through the closed `supportedLanguages` allowlist; `PackageName` is constrained by `packageNameRe`; `Available()` resolves the literal `openapi-generator-cli` with `exec.LookPath`. -- API reachability: repo grep found no `TransformerIn`, request body, query parameter, or HTTP route path reaching `SDKGenerator.Generate`; only CLI code, tests, and docs reference it. -- Severity verdict: OPERATOR-ONLY / low. Existing `#nosec G204` in `codegen.go` is justified for the current trust chain. Reassess if a future API endpoint binds request fields to `SDKGenerator`.