From f15a947f25b57aecf8dbc0851f54f257e64a6ddb Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 7 Mar 2026 21:36:50 +0100 Subject: [PATCH 1/2] refactor: decouple mongodb driver from root module * fixes #91 * relates to #191 Drop go.mongodb.org/mongo-driver/v2 from the root go.mod by introducing an internal minimal BSON codec (bsonlite) that is wire-compatible with mongo-driver v2.5.0. Users who want the real driver can opt in with a blank import: `import _ "github.com/go-openapi/strfmt/enable/mongodb"`. - internal/bsonlite: minimal BSON encoder/decoder for string, DateTime, ObjectID document types (no external dependencies) - enable/mongodb: separate Go module that replaces the lite codec with the real mongo-driver via init() - ObjectId type changed from bsonprim.ObjectID to [12]byte (same underlying type, backward compatible) - All BSON marshal/unmarshal methods rewritten to use bsonlite.C interface - Convert to mono-repo: go.work, CI workflows switched to monorepo variants (ci-workflows v0.2.13), dependabot scans all modules - Integration tests verify both codec paths against real MongoDB Co-Authored-By: Claude Opus 4.6 Signed-off-by: Frederic BIDON --- .github/dependabot.yaml | 6 +- .github/workflows/auto-merge.yml | 2 +- .github/workflows/bump-release.yml | 29 +- .github/workflows/codeql.yml | 2 +- .github/workflows/contributors.yml | 2 +- .github/workflows/go-test.yml | 2 +- .github/workflows/scanner.yml | 2 +- .github/workflows/tag-release.yml | 3 +- README.md | 12 +- bson.go | 69 +- bson_test.go | 5 +- default_test.go | 2 +- doc.go | 2 - docs/MAINTAINERS.md | 39 +- duration.go | 2 +- enable/mongodb/go.mod | 10 + enable/mongodb/go.sum | 6 + enable/mongodb/mongodb.go | 53 ++ go.mod | 1 - go.sum | 6 - go.work | 7 + go.work.sum | 6 + internal/bsonlite/codec.go | 71 +++ internal/bsonlite/lite.go | 213 +++++++ internal/testintegration/go.mod | 6 +- .../testintegration/mongodb/mongo_test.go | 345 +--------- .../mongodb_enabled/mongo_test.go | 19 + .../testintegration/mongotest/mongotest.go | 360 +++++++++++ mongo.go | 589 ++++++++---------- mongo_test.go | 61 +- time.go | 2 +- 31 files changed, 1147 insertions(+), 787 deletions(-) create mode 100644 enable/mongodb/go.mod create mode 100644 enable/mongodb/go.sum create mode 100644 enable/mongodb/mongodb.go create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 internal/bsonlite/codec.go create mode 100644 internal/bsonlite/lite.go create mode 100644 internal/testintegration/mongodb_enabled/mongo_test.go create mode 100644 internal/testintegration/mongotest/mongotest.go diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 9ce426e..918b484 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,7 +1,8 @@ version: 2 updates: - package-ecosystem: "github-actions" - directory: "/" + directories: + - "**/*" schedule: interval: "weekly" day: "friday" @@ -27,7 +28,8 @@ updates: # 2. golang.org-dependencies are auto-merged # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. # 4. other dependencies require a manual merge - directory: "/" + directories: + - "**/*" schedule: interval: "weekly" day: "friday" diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 48e5a57..3b108fe 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -11,5 +11,5 @@ jobs: permissions: contents: write pull-requests: write - uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 secrets: inherit diff --git a/.github/workflows/bump-release.yml b/.github/workflows/bump-release.yml index 57f7f75..0805a80 100644 --- a/.github/workflows/bump-release.yml +++ b/.github/workflows/bump-release.yml @@ -6,21 +6,15 @@ permissions: on: workflow_dispatch: inputs: - bump-patch: - description: Bump a patch version release - type: boolean + bump-type: + description: Type of bump (patch, minor, major) + type: choice + options: + - patch + - minor + - major + default: patch required: false - default: true - bump-minor: - description: Bump a minor version release - type: boolean - required: false - default: false - bump-major: - description: Bump a major version release - type: boolean - required: false - default: false tag-message-title: description: Tag message title to prepend to the release notes required: false @@ -36,11 +30,10 @@ jobs: bump-release: permissions: contents: write - uses: go-openapi/ci-workflows/.github/workflows/bump-release.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + pull-requests: write + uses: go-openapi/ci-workflows/.github/workflows/bump-release-monorepo.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 with: - bump-patch: ${{ inputs.bump-patch }} - bump-minor: ${{ inputs.bump-minor }} - bump-major: ${{ inputs.bump-major }} + bump-type: ${{ inputs.bump-type }} tag-message-title: ${{ inputs.tag-message-title }} tag-message-body: ${{ inputs.tag-message-body }} secrets: inherit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 32fb5e3..12b2c72 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,5 +18,5 @@ jobs: permissions: contents: read security-events: write - uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 secrets: inherit diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 33a469f..e2dd5e1 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -14,5 +14,5 @@ jobs: permissions: pull-requests: write contents: write - uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 secrets: inherit diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 514d891..9ba00d6 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -13,5 +13,5 @@ on: jobs: test: - uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/go-test-monorepo.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 secrets: inherit diff --git a/.github/workflows/scanner.yml b/.github/workflows/scanner.yml index b936bc1..f2f53df 100644 --- a/.github/workflows/scanner.yml +++ b/.github/workflows/scanner.yml @@ -15,5 +15,5 @@ jobs: permissions: contents: read security-events: write - uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # V0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 secrets: inherit diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index b834743..dddab0a 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -13,7 +13,8 @@ jobs: name: Create release permissions: contents: write - uses: go-openapi/ci-workflows/.github/workflows/release.yml@435746a4b72b06b6b6989c309fd2ad8150dbae5a # v0.2.11 + uses: go-openapi/ci-workflows/.github/workflows/release.yml@34a5baa33361844b1d2c70cd4548e3cea83529d9 # v0.2.13 with: tag: ${{ github.ref_name }} + is-monorepo: true secrets: inherit diff --git a/README.md b/README.md index 20935fb..0cab77b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ You may join the discord community by clicking the invite link on the discord ba Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] +* **2026-03-07** : v0.26.0 **dropped dependency to the mongodb driver** + * mongodb users can still use this package without any change + * however, we have frozen the back-compatible support for mongodb driver at v2.5.0 + * users who want to keep-up with future evolutions (possibly incompatible) of this driver + can do so by adding a blank import in their program: `import _ "github.com/go-openapi/strfmt/enable/mongodb"`. + This will switch the behavior to the actual driver, which remains regularly updated as an independent module. + ## Status API is stable. @@ -132,8 +139,9 @@ List of defined types: All format types implement the `database/sql` interfaces `sql.Scanner` and `driver.Valuer`, so they work out of the box with Go's standard `database/sql` package and any SQL driver. -All format types also implement BSON marshaling/unmarshaling for use with MongoDB -(via [`go.mongodb.org/mongo-driver/v2`](https://pkg.go.dev/go.mongodb.org/mongo-driver/v2)). +All format types also implement BSON marshaling/unmarshaling for use with MongoDB. +By default, a built-in minimal codec is used (compatible with mongo-driver v2.5.0). +For full driver support, add `import _ "github.com/go-openapi/strfmt/enable/mongodb"`. > **MySQL / MariaDB caveat for `DateTime`:** > The `go-sql-driver/mysql` driver has hard-coded handling for `time.Time` but does not diff --git a/bson.go b/bson.go index ee07c64..16a83f6 100644 --- a/bson.go +++ b/bson.go @@ -5,9 +5,9 @@ package strfmt import ( "database/sql/driver" + "encoding/hex" + "encoding/json" "fmt" - - bsonprim "go.mongodb.org/mongo-driver/v2/bson" ) func init() { //nolint:gochecknoinits // registers bsonobjectid format in the default registry @@ -17,45 +17,46 @@ func init() { //nolint:gochecknoinits // registers bsonobjectid format in the de // IsBSONObjectID returns true when the string is a valid BSON [ObjectId]. func IsBSONObjectID(str string) bool { - _, err := bsonprim.ObjectIDFromHex(str) + _, err := objectIDFromHex(str) return err == nil } -// ObjectId represents a BSON object ID (alias to [primitive.ObjectID]). +// ObjectId represents a BSON object ID (a 12-byte unique identifier). // -// swagger:[strfmt] bsonobjectid. -type ObjectId bsonprim.ObjectID //nolint:revive +// swagger:strfmt bsonobjectid. +type ObjectId [12]byte //nolint:revive + +// nilObjectID is the zero-value ObjectId. +var nilObjectID ObjectId //nolint:gochecknoglobals // package-level sentinel -// NewObjectId creates a [ObjectId] from a Hex String. +// NewObjectId creates a [ObjectId] from a hexadecimal String. func NewObjectId(hex string) ObjectId { //nolint:revive - oid, err := bsonprim.ObjectIDFromHex(hex) + oid, err := objectIDFromHex(hex) if err != nil { panic(err) } - return ObjectId(oid) + return oid } // MarshalText turns this instance into text. func (id ObjectId) MarshalText() ([]byte, error) { - oid := bsonprim.ObjectID(id) - if oid == bsonprim.NilObjectID { + if id == nilObjectID { return nil, nil } - return []byte(oid.Hex()), nil + return []byte(id.Hex()), nil } // UnmarshalText hydrates this instance from text. func (id *ObjectId) UnmarshalText(data []byte) error { // validation is performed later on if len(data) == 0 { - *id = ObjectId(bsonprim.NilObjectID) + *id = nilObjectID return nil } - oidstr := string(data) - oid, err := bsonprim.ObjectIDFromHex(oidstr) + oid, err := objectIDFromHex(string(data)) if err != nil { return err } - *id = ObjectId(oid) + *id = oid return nil } @@ -76,25 +77,34 @@ func (id *ObjectId) Scan(raw any) error { // Value converts a value to a database driver value. func (id ObjectId) Value() (driver.Value, error) { - return driver.Value(bsonprim.ObjectID(id).Hex()), nil + return driver.Value(id.Hex()), nil +} + +// Hex returns the hex string representation of the [ObjectId]. +func (id ObjectId) Hex() string { + return hex.EncodeToString(id[:]) } func (id ObjectId) String() string { - return bsonprim.ObjectID(id).Hex() + return id.Hex() } // MarshalJSON returns the [ObjectId] as JSON. func (id ObjectId) MarshalJSON() ([]byte, error) { - return bsonprim.ObjectID(id).MarshalJSON() + return json.Marshal(id.Hex()) } // UnmarshalJSON sets the [ObjectId] from JSON. func (id *ObjectId) UnmarshalJSON(data []byte) error { - var obj bsonprim.ObjectID - if err := obj.UnmarshalJSON(data); err != nil { + var hexStr string + if err := json.Unmarshal(data, &hexStr); err != nil { + return err + } + oid, err := objectIDFromHex(hexStr) + if err != nil { return err } - *id = ObjectId(obj) + *id = oid return nil } @@ -112,3 +122,18 @@ func (id *ObjectId) DeepCopy() *ObjectId { id.DeepCopyInto(out) return out } + +// objectIDFromHex parses a 24-character hex string into an [ObjectId]. +func objectIDFromHex(s string) (ObjectId, error) { + const objectIDHexLen = 24 + if len(s) != objectIDHexLen { + return nilObjectID, fmt.Errorf("the provided hex string %q is not a valid ObjectID: %w", s, ErrFormat) + } + b, err := hex.DecodeString(s) + if err != nil { + return nilObjectID, fmt.Errorf("the provided hex string %q is not a valid ObjectID: %w", s, err) + } + var oid ObjectId + copy(oid[:], b) + return oid, nil +} diff --git a/bson_test.go b/bson_test.go index 874c685..924f646 100644 --- a/bson_test.go +++ b/bson_test.go @@ -8,7 +8,6 @@ import ( "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" - "go.mongodb.org/mongo-driver/v2/bson" ) func TestBSONObjectId_fullCycle(t *testing.T) { @@ -39,10 +38,10 @@ func TestBSONObjectId_fullCycle(t *testing.T) { require.NoError(t, err) assert.EqualT(t, id, idCopy) - bsonBytes, err := bson.Marshal(&id) + bsonBytes, err := id.MarshalBSON() require.NoError(t, err) - err = bson.Unmarshal(bsonBytes, &idCopy) + err = idCopy.UnmarshalBSON(bsonBytes) require.NoError(t, err) assert.EqualT(t, id, idCopy) } diff --git a/default_test.go b/default_test.go index ac5db5b..ec6ef5e 100644 --- a/default_test.go +++ b/default_test.go @@ -218,7 +218,7 @@ func TestFormatIPv4(t *testing.T) { func TestFormatIPv6(t *testing.T) { ipv6 := IPv6("::1") str := string("::2") - // TODO: test ipv6 zones + // Proposal for enhancement: test ipv6 zones testStringFormat(t, &ipv6, "ipv6", str, []string{}, []string{"127.0.0.1"}) } diff --git a/doc.go b/doc.go index b8fe177..6652521 100644 --- a/doc.go +++ b/doc.go @@ -2,6 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 // Package strfmt contains custom string formats. -// -// TODO: add info on how to define and register a custom format. package strfmt diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md index 42a60bf..b0bc34e 100644 --- a/docs/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -4,7 +4,15 @@ ## Repo structure -This project is organized as a repo with a single go module. +This project is organized as a mono-repo with multiple go modules: + +| Module | Description | +|--------|-------------| +| `.` (root) | Core `strfmt` package with all format types | +| `enable/mongodb` | Blank-import package that replaces the built-in minimal BSON codec with the real MongoDB driver | +| `internal/testintegration` | Integration tests against real databases (MongoDB, MariaDB, PostgreSQL) | + +A `go.work` file at the root ties all modules together for local development. ## Repo configuration @@ -54,7 +62,7 @@ Coverage threshold status is informative and not blocking. This is because the thresholds are difficult to tune and codecov oftentimes reports false negatives or may fail to upload coverage. -All tests across `go-openapi` use our fork of `stretchr/strfmt` (this repo): `github.com/go-openapi/strfmt`. +All tests across `go-openapi` use our fork of `stretchr/testify`: `github.com/go-openapi/testify`. This allows for minimal test dependencies. > **NOTES** @@ -117,19 +125,20 @@ Reports are centralized in github security reports for code scanning tools. ## Releases -**For single module repos:** - -A bump release workflow can be triggered from the github actions UI to cut a release with a few clicks. +A bump release workflow (mono-repo) can be triggered from the github actions UI to cut a release with a few clicks. -The release process is minimalist: +The release process updates cross-module dependencies (e.g. `enable/mongodb` → root module) +before pushing the desired git tag. -* push a semver tag (i.e v{major}.{minor}.{patch}) to the master branch. -* the CI handles this to generate a github release with release notes +It first creates an auto-merged PR that updates the different `go.mod` files, +then pushes the desired semver tag. * release notes generator: git-cliff -* configuration: the `.cliff.toml` is defined as a share configuration on +* configuration: the `.cliff.toml` is defined as a shared configuration on remote repo [`ci-workflows/.cliff.toml`][remote-cliff-config] +Commits and tags pushed by the workflow bot are PGP-signed ("go-openapi[bot]"). + Commits from maintainers are preferably PGP-signed. Tags are preferably PGP-signed. @@ -140,18 +149,6 @@ The tag message introduces the release notes (e.g. a summary of this release). The release notes generator does not assume that commits are necessarily "conventional commits". -**For mono-repos with multiple modules:** - -The release process is slightly different because we need to update cross-module dependencies -before pushing a tag. - -A bump release workflow (mono-repo) can be triggered from the github actions UI to cut a release with a few clicks. - -It works with the same input as the one for single module repos, and first creates a PR (auto-merged) -that updates the different go.mod files _before_ pushing the desired git tag. - -Commits and tags pushed by the workflow bot are PGP-signed ("go-openapi[bot]"). - ## Other files Standard documentation: diff --git a/duration.go b/duration.go index 81532ef..229aa75 100644 --- a/duration.go +++ b/duration.go @@ -143,7 +143,7 @@ func ParseDuration(cand string) (time.Duration, error) { // Scan reads a Duration value from database driver type. func (d *Duration) Scan(raw any) error { switch v := raw.(type) { - // TODO: case []byte: // ? + // Proposal for enhancement: case []byte: // ? case int64: *d = Duration(v) case float64: diff --git a/enable/mongodb/go.mod b/enable/mongodb/go.mod new file mode 100644 index 0000000..e2dcf2a --- /dev/null +++ b/enable/mongodb/go.mod @@ -0,0 +1,10 @@ +module github.com/go-openapi/strfmt/enable/mongodb + +go 1.24.0 + +require ( + github.com/go-openapi/strfmt v0.25.0 + go.mongodb.org/mongo-driver/v2 v2.5.0 +) + +replace github.com/go-openapi/strfmt => ../.. diff --git a/enable/mongodb/go.sum b/enable/mongodb/go.sum new file mode 100644 index 0000000..65a6c8c --- /dev/null +++ b/enable/mongodb/go.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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= diff --git a/enable/mongodb/mongodb.go b/enable/mongodb/mongodb.go new file mode 100644 index 0000000..d93b5e2 --- /dev/null +++ b/enable/mongodb/mongodb.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package mongodb replaces strfmt's built-in minimal BSON codec with one +// backed by the official MongoDB driver (go.mongodb.org/mongo-driver/v2). +// +// Usage: blank-import this package to enable the real driver codec: +// +// import _ "github.com/go-openapi/strfmt/enable/mongodb" +package mongodb + +import ( + "time" + + "github.com/go-openapi/strfmt/internal/bsonlite" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func init() { //nolint:gochecknoinits // blank-import registration pattern, by design + bsonlite.Replace(driverCodec{}) +} + +// driverCodec implements bsonlite.Codec using the real MongoDB driver. +type driverCodec struct{} + +func (driverCodec) MarshalDoc(value any) ([]byte, error) { + switch v := value.(type) { + case [bsonlite.ObjectIDSize]byte: + return bson.Marshal(bson.M{"data": bson.ObjectID(v)}) + default: + return bson.Marshal(bson.M{"data": v}) + } +} + +func (driverCodec) UnmarshalDoc(data []byte) (any, error) { + var m bson.M + if err := bson.Unmarshal(data, &m); err != nil { + return nil, err + } + + v := m["data"] + + switch val := v.(type) { + case bson.DateTime: + return val.Time(), nil + case bson.ObjectID: + return [bsonlite.ObjectIDSize]byte(val), nil + case time.Time: + return val, nil + default: + return v, nil + } +} diff --git a/go.mod b/go.mod index b388ac5..db5ae74 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/uuid v1.6.0 github.com/oklog/ulid/v2 v2.1.1 - go.mongodb.org/mongo-driver/v2 v2.5.0 golang.org/x/net v0.50.0 ) diff --git a/go.sum b/go.sum index c2ef2c3..e6060d3 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,14 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= 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-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -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= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= diff --git a/go.work b/go.work new file mode 100644 index 0000000..288e765 --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +use ( + . + ./enable/mongodb + ./internal/testintegration +) + +go 1.24.0 diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..b69c2da --- /dev/null +++ b/go.work.sum @@ -0,0 +1,6 @@ +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/bsonlite/codec.go b/internal/bsonlite/codec.go new file mode 100644 index 0000000..424f454 --- /dev/null +++ b/internal/bsonlite/codec.go @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package bsonlite provides a minimal BSON codec for strfmt types. +// +// This codec produces BSON output compatible with go.mongodb.org/mongo-driver/v2 +// (v2.5.0). It handles only the exact BSON patterns used by strfmt: +// single-key {"data": value} documents with string, DateTime, or ObjectID values. +// +// This package is intended to provide a backward-compatible API to users of +// go-openapi/strfmt. It is not intended to be maintained or to follow the +// evolutions of the official MongoDB drivers. For up-to-date MongoDB support, +// import "github.com/go-openapi/strfmt/enable/mongodb" to replace this codec +// with one backed by the real driver. +package bsonlite + +import "time" + +// Codec provides BSON document marshal/unmarshal for strfmt types. +// +// MarshalDoc encodes a single-key BSON document {"data": value}. +// The value must be one of: string, time.Time, or [12]byte (ObjectID). +// +// UnmarshalDoc decodes a BSON document and returns the "data" field's value. +// Returns one of: string, time.Time, or [12]byte depending on the BSON type. +type Codec interface { + MarshalDoc(value any) ([]byte, error) + UnmarshalDoc(data []byte) (any, error) +} + +// C is the active BSON codec. +// +//nolint:gochecknoglobals // replaceable codec, by design +var C Codec = liteCodec{} + +// Replace swaps the active BSON codec with the provided implementation. +// This is intended to be called from enable/mongodb's init(). +// +// Since [Replace] affects the global state of the package, it is not intended for concurrent use. +func Replace(c Codec) { + C = c +} + +// BSON type tags (from the BSON specification). +const ( + TypeString byte = 0x02 + TypeObjectID byte = 0x07 + TypeDateTime byte = 0x09 + TypeNull byte = 0x0A +) + +// ObjectIDSize is the size of a BSON ObjectID in bytes. +const ObjectIDSize = 12 + +// DateTimeToMillis converts a time.Time to BSON DateTime milliseconds. +func DateTimeToMillis(t time.Time) int64 { + const ( + millisec = 1000 + microsec = 1_000_000 + ) + return t.Unix()*millisec + int64(t.Nanosecond())/microsec +} + +// MillisToTime converts BSON DateTime milliseconds to time.Time. +func MillisToTime(millis int64) time.Time { + const ( + millisec = 1000 + nanosPerMs = 1_000_000 + ) + return time.Unix(millis/millisec, millis%millisec*nanosPerMs) +} diff --git a/internal/bsonlite/lite.go b/internal/bsonlite/lite.go new file mode 100644 index 0000000..6b0e0e1 --- /dev/null +++ b/internal/bsonlite/lite.go @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package bsonlite + +import ( + "encoding/binary" + "errors" + "fmt" + "time" +) + +// liteCodec is a minimal BSON codec that handles only the patterns used by strfmt: +// single-key documents of the form {"data": } where value is a string, +// BSON DateTime (time.Time), or BSON ObjectID ([12]byte). +type liteCodec struct{} + +var _ Codec = liteCodec{} + +func (liteCodec) MarshalDoc(value any) ([]byte, error) { + switch v := value.(type) { + case string: + return marshalStringDoc(v), nil + case time.Time: + return marshalDateTimeDoc(v), nil + case [ObjectIDSize]byte: + return marshalObjectIDDoc(v), nil + default: + return nil, fmt.Errorf("bsonlite: unsupported value type %T: %w", value, errUnsupportedType) + } +} + +func (liteCodec) UnmarshalDoc(data []byte) (any, error) { + return unmarshalDoc(data) +} + +// BSON wire format helpers. +// +// Document: int32(size) + elements + 0x00 +// Element: byte(type) + cstring(key) + value +// String: int32(len+1) + bytes + 0x00 +// DateTime: int64 (LE, millis since epoch) +// ObjectID: [12]byte + +const dataKey = "data\x00" + +func marshalStringDoc(s string) []byte { + sBytes := []byte(s) + // doc_size(4) + type(1) + key("data\0"=5) + strlen(4) + string + \0(1) + doc_term(1) + docSize := 4 + 1 + len(dataKey) + 4 + len(sBytes) + 1 + 1 + + buf := make([]byte, docSize) + pos := 0 + + binary.LittleEndian.PutUint32(buf[pos:], uint32(docSize)) //nolint:gosec // size is computed from input, cannot overflow + pos += 4 + + buf[pos] = TypeString + pos++ + + pos += copy(buf[pos:], dataKey) + + binary.LittleEndian.PutUint32(buf[pos:], uint32(len(sBytes)+1)) //nolint:gosec // string length cannot overflow uint32 + pos += 4 + + pos += copy(buf[pos:], sBytes) + buf[pos] = 0 // string null terminator + pos++ + + buf[pos] = 0 // document terminator + + return buf +} + +func marshalDateTimeDoc(t time.Time) []byte { + // doc_size(4) + type(1) + key("data\0"=5) + int64(8) + doc_term(1) + const docSize = 4 + 1 + 5 + 8 + 1 + + buf := make([]byte, docSize) + pos := 0 + + binary.LittleEndian.PutUint32(buf[pos:], docSize) + pos += 4 + + buf[pos] = TypeDateTime + pos++ + + pos += copy(buf[pos:], dataKey) + + millis := DateTimeToMillis(t) + binary.LittleEndian.PutUint64(buf[pos:], uint64(millis)) //nolint:gosec // negative datetime millis are valid + // pos += 8 + + buf[docSize-1] = 0 // document terminator + + return buf +} + +func marshalObjectIDDoc(oid [ObjectIDSize]byte) []byte { + // doc_size(4) + type(1) + key("data\0"=5) + objectid(12) + doc_term(1) + const docSize = 4 + 1 + 5 + ObjectIDSize + 1 + + buf := make([]byte, docSize) + pos := 0 + + binary.LittleEndian.PutUint32(buf[pos:], docSize) + pos += 4 + + buf[pos] = TypeObjectID + pos++ + + pos += copy(buf[pos:], dataKey) + + copy(buf[pos:], oid[:]) + // pos += ObjectIDSize + + buf[docSize-1] = 0 // document terminator + + return buf +} + +var ( + errUnsupportedType = errors.New("bsonlite: unsupported type") + errDocTooShort = errors.New("bsonlite: document too short") + errDocSize = errors.New("bsonlite: document size mismatch") + errNoTerminator = errors.New("bsonlite: missing key terminator") + errTruncated = errors.New("bsonlite: truncated value") + errDataNotFound = errors.New("bsonlite: \"data\" field not found") +) + +func unmarshalDoc(raw []byte) (any, error) { + const minDocSize = 5 // int32(size) + terminator + + if len(raw) < minDocSize { + return nil, errDocTooShort + } + + docSize := int(binary.LittleEndian.Uint32(raw[:4])) + if docSize != len(raw) { + return nil, errDocSize + } + + pos := 4 + + for pos < docSize-1 { + if pos >= len(raw) { + return nil, errTruncated + } + typeByte := raw[pos] + pos++ + + // Read key (cstring: bytes until 0x00). + keyStart := pos + for pos < len(raw) && raw[pos] != 0 { + pos++ + } + if pos >= len(raw) { + return nil, errNoTerminator + } + key := string(raw[keyStart:pos]) + pos++ // skip null terminator + + val, newPos, err := readValue(typeByte, raw, pos) + if err != nil { + return nil, err + } + pos = newPos + + if key == "data" { + return val, nil + } + } + + return nil, errDataNotFound +} + +func readValue(typeByte byte, raw []byte, pos int) (any, int, error) { + switch typeByte { + case TypeString: + if pos+4 > len(raw) { + return nil, 0, errTruncated + } + strLen := int(binary.LittleEndian.Uint32(raw[pos:])) + pos += 4 + if pos+strLen > len(raw) || strLen < 1 { + return nil, 0, errTruncated + } + s := string(raw[pos : pos+strLen-1]) // exclude null terminator + return s, pos + strLen, nil + + case TypeObjectID: + if pos+ObjectIDSize > len(raw) { + return nil, 0, errTruncated + } + var oid [ObjectIDSize]byte + copy(oid[:], raw[pos:pos+ObjectIDSize]) + return oid, pos + ObjectIDSize, nil + + case TypeDateTime: + const dateTimeSize = 8 + if pos+dateTimeSize > len(raw) { + return nil, 0, errTruncated + } + millis := int64(binary.LittleEndian.Uint64(raw[pos:])) //nolint:gosec // negative datetime millis are valid + return MillisToTime(millis), pos + dateTimeSize, nil + + case TypeNull: + return nil, pos, nil + + default: + return nil, 0, fmt.Errorf("bsonlite: unsupported BSON type 0x%02x: %w", typeByte, errUnsupportedType) + } +} diff --git a/internal/testintegration/go.mod b/internal/testintegration/go.mod index eb7680d..a9e09de 100644 --- a/internal/testintegration/go.mod +++ b/internal/testintegration/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/go-openapi/strfmt v0.25.0 + github.com/go-openapi/strfmt/enable/mongodb v0.0.0 github.com/go-openapi/testify/v2 v2.4.0 github.com/go-sql-driver/mysql v1.9.3 github.com/jackc/pgx/v5 v5.8.0 @@ -30,4 +31,7 @@ require ( golang.org/x/text v0.34.0 // indirect ) -replace github.com/go-openapi/strfmt => ../.. +replace ( + github.com/go-openapi/strfmt => ../.. + github.com/go-openapi/strfmt/enable/mongodb => ../../enable/mongodb +) diff --git a/internal/testintegration/mongodb/mongo_test.go b/internal/testintegration/mongodb/mongo_test.go index 2e6d8f1..a122e5b 100644 --- a/internal/testintegration/mongodb/mongo_test.go +++ b/internal/testintegration/mongodb/mongo_test.go @@ -3,351 +3,16 @@ //go:build testintegration +// Package mongodb_test runs MongoDB integration tests using the default +// (bsonlite) codec — no enable/mongodb blank import. package mongodb_test import ( - "context" - "encoding/base64" - "os" "testing" - "time" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" - "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/mongo" - "go.mongodb.org/mongo-driver/v2/mongo/options" + "github.com/go-openapi/strfmt/internal/testintegration/mongotest" ) -func mongoURI() string { - if uri := os.Getenv("MONGODB_URI"); uri != "" { - return uri - } - return "mongodb://localhost:27017" -} - -func setup(t *testing.T) *mongo.Collection { - t.Helper() - - client, err := mongo.Connect(options.Client().ApplyURI(mongoURI())) - require.NoError(t, err) - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - require.NoError(t, client.Ping(ctx, nil)) - - db := client.Database("strfmt_integration_test") - coll := db.Collection(t.Name()) - - t.Cleanup(func() { - _ = coll.Drop(context.Background()) - _ = client.Disconnect(context.Background()) - }) - - return coll -} - -// roundTrip inserts a document containing the value into MongoDB, -// reads it back, and returns the result document. -func roundTrip(t *testing.T, coll *mongo.Collection, doc bson.M) bson.M { - t.Helper() - ctx := context.Background() - - _, err := coll.InsertOne(ctx, doc) - require.NoError(t, err) - - var result bson.M - err = coll.FindOne(ctx, bson.M{"_id": doc["_id"]}).Decode(&result) - require.NoError(t, err) - - return result -} - -func TestDate(t *testing.T) { - coll := setup(t) - original := strfmt.Date(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) - - doc := bson.M{"_id": "date_test", "value": original} - result := roundTrip(t, coll, doc) - - raw, ok := result["value"].(bson.D) - require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) - - rawBytes, err := bson.Marshal(raw) - require.NoError(t, err) - - var got strfmt.Date - require.NoError(t, bson.Unmarshal(rawBytes, &got)) - - assert.EqualT(t, original.String(), got.String()) -} - -func TestDateTime(t *testing.T) { - coll := setup(t) - original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC)) - - doc := bson.M{"_id": "datetime_test", "value": original} - result := roundTrip(t, coll, doc) - - // DateTime uses MarshalBSONValue, so MongoDB stores it as a native datetime. - dt, ok := result["value"].(bson.DateTime) - require.TrueT(t, ok, "expected bson.DateTime, got %T", result["value"]) - - got := strfmt.DateTime(dt.Time()) - - assert.EqualT(t, time.Time(original).UTC().UnixMilli(), time.Time(got).UTC().UnixMilli()) -} - -func TestDuration(t *testing.T) { - coll := setup(t) - original := strfmt.Duration(42 * time.Second) - - doc := bson.M{"_id": "duration_test", "value": original} - result := roundTrip(t, coll, doc) - - raw, ok := result["value"].(bson.D) - require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) - - rawBytes, err := bson.Marshal(raw) - require.NoError(t, err) - - var got strfmt.Duration - require.NoError(t, bson.Unmarshal(rawBytes, &got)) - - assert.EqualT(t, original, got) -} - -func TestBase64(t *testing.T) { - coll := setup(t) - payload := []byte("hello world with special chars: éàü") - original := strfmt.Base64(payload) - - doc := bson.M{"_id": "base64_test", "value": original} - result := roundTrip(t, coll, doc) - - raw, ok := result["value"].(bson.D) - require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) - - rawBytes, err := bson.Marshal(raw) - require.NoError(t, err) - - var got strfmt.Base64 - require.NoError(t, bson.Unmarshal(rawBytes, &got)) - - assert.EqualT(t, base64.StdEncoding.EncodeToString(original), base64.StdEncoding.EncodeToString(got)) -} - -func TestULID(t *testing.T) { - coll := setup(t) - original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") - require.NoError(t, err) - - doc := bson.M{"_id": "ulid_test", "value": original} - result := roundTrip(t, coll, doc) - - raw, ok := result["value"].(bson.D) - require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) - - rawBytes, err := bson.Marshal(raw) - require.NoError(t, err) - - var got strfmt.ULID - require.NoError(t, bson.Unmarshal(rawBytes, &got)) - - assert.EqualT(t, original, got) -} - -func TestObjectId(t *testing.T) { - coll := setup(t) - original := strfmt.NewObjectId("507f1f77bcf86cd799439011") - - doc := bson.M{"_id": "objectid_test", "value": original} - result := roundTrip(t, coll, doc) - - // ObjectId uses MarshalBSONValue, so MongoDB stores it as a native ObjectID. - oid, ok := result["value"].(bson.ObjectID) - require.TrueT(t, ok, "expected bson.ObjectID, got %T", result["value"]) - - got := strfmt.ObjectId(oid) - - assert.EqualT(t, original, got) -} - -// stringFormatRoundTrip is a helper for types that serialize as embedded BSON documents -// with a "data" string field (most strfmt string-based types). -func stringFormatRoundTrip(t *testing.T, coll *mongo.Collection, id string, input bson.Marshaler, output bson.Unmarshaler, _ string) { - t.Helper() - - doc := bson.M{"_id": id, "value": input} - result := roundTrip(t, coll, doc) - - raw, ok := result["value"].(bson.D) - require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) - - rawBytes, err := bson.Marshal(raw) - require.NoError(t, err) - - require.NoError(t, bson.Unmarshal(rawBytes, output)) -} - -func TestURI(t *testing.T) { - coll := setup(t) - original := strfmt.URI("https://example.com/path?q=1") - var got strfmt.URI - stringFormatRoundTrip(t, coll, "uri_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestEmail(t *testing.T) { - coll := setup(t) - original := strfmt.Email("user@example.com") - var got strfmt.Email - stringFormatRoundTrip(t, coll, "email_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestHostname(t *testing.T) { - coll := setup(t) - original := strfmt.Hostname("example.com") - var got strfmt.Hostname - stringFormatRoundTrip(t, coll, "hostname_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestIPv4(t *testing.T) { - coll := setup(t) - original := strfmt.IPv4("192.168.1.1") - var got strfmt.IPv4 - stringFormatRoundTrip(t, coll, "ipv4_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestIPv6(t *testing.T) { - coll := setup(t) - original := strfmt.IPv6("::1") - var got strfmt.IPv6 - stringFormatRoundTrip(t, coll, "ipv6_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestCIDR(t *testing.T) { - coll := setup(t) - original := strfmt.CIDR("192.168.1.0/24") - var got strfmt.CIDR - stringFormatRoundTrip(t, coll, "cidr_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestMAC(t *testing.T) { - coll := setup(t) - original := strfmt.MAC("01:02:03:04:05:06") - var got strfmt.MAC - stringFormatRoundTrip(t, coll, "mac_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestUUID(t *testing.T) { - coll := setup(t) - original := strfmt.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") - var got strfmt.UUID - stringFormatRoundTrip(t, coll, "uuid_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestUUID3(t *testing.T) { - coll := setup(t) - original := strfmt.UUID3("bcd02ab7-6beb-3467-84c0-3bdbea962817") - var got strfmt.UUID3 - stringFormatRoundTrip(t, coll, "uuid3_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestUUID4(t *testing.T) { - coll := setup(t) - original := strfmt.UUID4("025b0d74-00a2-4885-af46-084e7fbd0701") - var got strfmt.UUID4 - stringFormatRoundTrip(t, coll, "uuid4_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestUUID5(t *testing.T) { - coll := setup(t) - original := strfmt.UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d") - var got strfmt.UUID5 - stringFormatRoundTrip(t, coll, "uuid5_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestUUID7(t *testing.T) { - coll := setup(t) - original := strfmt.UUID7("01943ff8-3e9e-7be4-8921-de6a1e04d599") - var got strfmt.UUID7 - stringFormatRoundTrip(t, coll, "uuid7_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestISBN(t *testing.T) { - coll := setup(t) - original := strfmt.ISBN("0321751043") - var got strfmt.ISBN - stringFormatRoundTrip(t, coll, "isbn_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestISBN10(t *testing.T) { - coll := setup(t) - original := strfmt.ISBN10("0321751043") - var got strfmt.ISBN10 - stringFormatRoundTrip(t, coll, "isbn10_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestISBN13(t *testing.T) { - coll := setup(t) - original := strfmt.ISBN13("978-0321751041") - var got strfmt.ISBN13 - stringFormatRoundTrip(t, coll, "isbn13_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestCreditCard(t *testing.T) { - coll := setup(t) - original := strfmt.CreditCard("4111-1111-1111-1111") - var got strfmt.CreditCard - stringFormatRoundTrip(t, coll, "creditcard_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestSSN(t *testing.T) { - coll := setup(t) - original := strfmt.SSN("111-11-1111") - var got strfmt.SSN - stringFormatRoundTrip(t, coll, "ssn_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestHexColor(t *testing.T) { - coll := setup(t) - original := strfmt.HexColor("#FFFFFF") - var got strfmt.HexColor - stringFormatRoundTrip(t, coll, "hexcolor_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestRGBColor(t *testing.T) { - coll := setup(t) - original := strfmt.RGBColor("rgb(255,255,255)") - var got strfmt.RGBColor - stringFormatRoundTrip(t, coll, "rgbcolor_test", original, &got, string(original)) - assert.EqualT(t, original, got) -} - -func TestPassword(t *testing.T) { - coll := setup(t) - original := strfmt.Password("super secret stuff here") - var got strfmt.Password - stringFormatRoundTrip(t, coll, "password_test", original, &got, string(original)) - assert.EqualT(t, original, got) +func TestMongoDBLiteCodec(t *testing.T) { + mongotest.RunAllTests(t) } diff --git a/internal/testintegration/mongodb_enabled/mongo_test.go b/internal/testintegration/mongodb_enabled/mongo_test.go new file mode 100644 index 0000000..919e0d9 --- /dev/null +++ b/internal/testintegration/mongodb_enabled/mongo_test.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +//go:build testintegration + +// Package mongodb_enabled_test runs MongoDB integration tests with the real +// MongoDB driver codec enabled via blank import. +package mongodb_enabled_test + +import ( + "testing" + + _ "github.com/go-openapi/strfmt/enable/mongodb" + "github.com/go-openapi/strfmt/internal/testintegration/mongotest" +) + +func TestMongoDBDriverCodec(t *testing.T) { + mongotest.RunAllTests(t) +} diff --git a/internal/testintegration/mongotest/mongotest.go b/internal/testintegration/mongotest/mongotest.go new file mode 100644 index 0000000..d02cb55 --- /dev/null +++ b/internal/testintegration/mongotest/mongotest.go @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +//go:build testintegration + +// Package mongotest provides shared MongoDB integration test logic. +// It is used by both mongodb/ (lite codec) and mongodb_enabled/ (real driver codec) +// test packages to verify that strfmt types round-trip correctly through MongoDB. +package mongotest + +import ( + "context" + "encoding/base64" + "os" + "testing" + "time" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func mongoURI() string { + if uri := os.Getenv("MONGODB_URI"); uri != "" { + return uri + } + return "mongodb://localhost:27017" +} + +// Setup connects to MongoDB and returns a test collection. +func Setup(t *testing.T) *mongo.Collection { + t.Helper() + + client, err := mongo.Connect(options.Client().ApplyURI(mongoURI())) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + require.NoError(t, client.Ping(ctx, nil)) + + db := client.Database("strfmt_integration_test") + coll := db.Collection(t.Name()) + + t.Cleanup(func() { + _ = coll.Drop(context.Background()) + _ = client.Disconnect(context.Background()) + }) + + return coll +} + +// roundTrip inserts a document containing the value into MongoDB, +// reads it back, and returns the result document. +func roundTrip(t *testing.T, coll *mongo.Collection, doc bson.M) bson.M { + t.Helper() + ctx := context.Background() + + _, err := coll.InsertOne(ctx, doc) + require.NoError(t, err) + + var result bson.M + err = coll.FindOne(ctx, bson.M{"_id": doc["_id"]}).Decode(&result) + require.NoError(t, err) + + return result +} + +// stringFormatRoundTrip is a helper for types that serialize as embedded BSON documents +// with a "data" string field (most strfmt string-based types). +func stringFormatRoundTrip(t *testing.T, coll *mongo.Collection, id string, input bson.Marshaler, output bson.Unmarshaler) { + t.Helper() + + doc := bson.M{"_id": id, "value": input} + result := roundTrip(t, coll, doc) + + raw, ok := result["value"].(bson.D) + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + + rawBytes, err := bson.Marshal(raw) + require.NoError(t, err) + + require.NoError(t, bson.Unmarshal(rawBytes, output)) +} + +// RunAllTests runs all strfmt type round-trip tests against the given MongoDB collection. +func RunAllTests(t *testing.T) { + t.Run("Date", func(t *testing.T) { + coll := Setup(t) + original := strfmt.Date(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC)) + + doc := bson.M{"_id": "date_test", "value": original} + result := roundTrip(t, coll, doc) + + raw, ok := result["value"].(bson.D) + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + + rawBytes, err := bson.Marshal(raw) + require.NoError(t, err) + + var got strfmt.Date + require.NoError(t, bson.Unmarshal(rawBytes, &got)) + + assert.EqualT(t, original.String(), got.String()) + }) + + t.Run("DateTime", func(t *testing.T) { + coll := Setup(t) + original := strfmt.DateTime(time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC)) + + doc := bson.M{"_id": "datetime_test", "value": original} + result := roundTrip(t, coll, doc) + + // DateTime uses MarshalBSONValue, so MongoDB stores it as a native datetime. + dt, ok := result["value"].(bson.DateTime) + require.TrueT(t, ok, "expected bson.DateTime, got %T", result["value"]) + + got := strfmt.DateTime(dt.Time()) + + assert.EqualT(t, time.Time(original).UTC().UnixMilli(), time.Time(got).UTC().UnixMilli()) + }) + + t.Run("Duration", func(t *testing.T) { + coll := Setup(t) + original := strfmt.Duration(42 * time.Second) + + doc := bson.M{"_id": "duration_test", "value": original} + result := roundTrip(t, coll, doc) + + raw, ok := result["value"].(bson.D) + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + + rawBytes, err := bson.Marshal(raw) + require.NoError(t, err) + + var got strfmt.Duration + require.NoError(t, bson.Unmarshal(rawBytes, &got)) + + assert.EqualT(t, original, got) + }) + + t.Run("Base64", func(t *testing.T) { + coll := Setup(t) + payload := []byte("hello world with special chars: éàü") + original := strfmt.Base64(payload) + + doc := bson.M{"_id": "base64_test", "value": original} + result := roundTrip(t, coll, doc) + + raw, ok := result["value"].(bson.D) + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + + rawBytes, err := bson.Marshal(raw) + require.NoError(t, err) + + var got strfmt.Base64 + require.NoError(t, bson.Unmarshal(rawBytes, &got)) + + assert.EqualT(t, base64.StdEncoding.EncodeToString(original), base64.StdEncoding.EncodeToString(got)) + }) + + t.Run("ULID", func(t *testing.T) { + coll := Setup(t) + original, err := strfmt.ParseULID("01ARZ3NDEKTSV4RRFFQ69G5FAV") + require.NoError(t, err) + + doc := bson.M{"_id": "ulid_test", "value": original} + result := roundTrip(t, coll, doc) + + raw, ok := result["value"].(bson.D) + require.TrueT(t, ok, "expected bson.D for value, got %T", result["value"]) + + rawBytes, err := bson.Marshal(raw) + require.NoError(t, err) + + var got strfmt.ULID + require.NoError(t, bson.Unmarshal(rawBytes, &got)) + + assert.EqualT(t, original, got) + }) + + t.Run("ObjectId", func(t *testing.T) { + coll := Setup(t) + original := strfmt.NewObjectId("507f1f77bcf86cd799439011") + + doc := bson.M{"_id": "objectid_test", "value": original} + result := roundTrip(t, coll, doc) + + // ObjectId uses MarshalBSONValue, so MongoDB stores it as a native ObjectID. + oid, ok := result["value"].(bson.ObjectID) + require.TrueT(t, ok, "expected bson.ObjectID, got %T", result["value"]) + + got := strfmt.ObjectId(oid) + + assert.EqualT(t, original, got) + }) + + t.Run("URI", func(t *testing.T) { + coll := Setup(t) + original := strfmt.URI("https://example.com/path?q=1") + var got strfmt.URI + stringFormatRoundTrip(t, coll, "uri_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("Email", func(t *testing.T) { + coll := Setup(t) + original := strfmt.Email("user@example.com") + var got strfmt.Email + stringFormatRoundTrip(t, coll, "email_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("Hostname", func(t *testing.T) { + coll := Setup(t) + original := strfmt.Hostname("example.com") + var got strfmt.Hostname + stringFormatRoundTrip(t, coll, "hostname_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("IPv4", func(t *testing.T) { + coll := Setup(t) + original := strfmt.IPv4("192.168.1.1") + var got strfmt.IPv4 + stringFormatRoundTrip(t, coll, "ipv4_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("IPv6", func(t *testing.T) { + coll := Setup(t) + original := strfmt.IPv6("::1") + var got strfmt.IPv6 + stringFormatRoundTrip(t, coll, "ipv6_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("CIDR", func(t *testing.T) { + coll := Setup(t) + original := strfmt.CIDR("192.168.1.0/24") + var got strfmt.CIDR + stringFormatRoundTrip(t, coll, "cidr_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("MAC", func(t *testing.T) { + coll := Setup(t) + original := strfmt.MAC("01:02:03:04:05:06") + var got strfmt.MAC + stringFormatRoundTrip(t, coll, "mac_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("UUID", func(t *testing.T) { + coll := Setup(t) + original := strfmt.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e") + var got strfmt.UUID + stringFormatRoundTrip(t, coll, "uuid_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("UUID3", func(t *testing.T) { + coll := Setup(t) + original := strfmt.UUID3("bcd02ab7-6beb-3467-84c0-3bdbea962817") + var got strfmt.UUID3 + stringFormatRoundTrip(t, coll, "uuid3_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("UUID4", func(t *testing.T) { + coll := Setup(t) + original := strfmt.UUID4("025b0d74-00a2-4885-af46-084e7fbd0701") + var got strfmt.UUID4 + stringFormatRoundTrip(t, coll, "uuid4_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("UUID5", func(t *testing.T) { + coll := Setup(t) + original := strfmt.UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d") + var got strfmt.UUID5 + stringFormatRoundTrip(t, coll, "uuid5_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("UUID7", func(t *testing.T) { + coll := Setup(t) + original := strfmt.UUID7("01943ff8-3e9e-7be4-8921-de6a1e04d599") + var got strfmt.UUID7 + stringFormatRoundTrip(t, coll, "uuid7_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("ISBN", func(t *testing.T) { + coll := Setup(t) + original := strfmt.ISBN("0321751043") + var got strfmt.ISBN + stringFormatRoundTrip(t, coll, "isbn_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("ISBN10", func(t *testing.T) { + coll := Setup(t) + original := strfmt.ISBN10("0321751043") + var got strfmt.ISBN10 + stringFormatRoundTrip(t, coll, "isbn10_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("ISBN13", func(t *testing.T) { + coll := Setup(t) + original := strfmt.ISBN13("978-0321751041") + var got strfmt.ISBN13 + stringFormatRoundTrip(t, coll, "isbn13_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("CreditCard", func(t *testing.T) { + coll := Setup(t) + original := strfmt.CreditCard("4111-1111-1111-1111") + var got strfmt.CreditCard + stringFormatRoundTrip(t, coll, "creditcard_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("SSN", func(t *testing.T) { + coll := Setup(t) + original := strfmt.SSN("111-11-1111") + var got strfmt.SSN + stringFormatRoundTrip(t, coll, "ssn_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("HexColor", func(t *testing.T) { + coll := Setup(t) + original := strfmt.HexColor("#FFFFFF") + var got strfmt.HexColor + stringFormatRoundTrip(t, coll, "hexcolor_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("RGBColor", func(t *testing.T) { + coll := Setup(t) + original := strfmt.RGBColor("rgb(255,255,255)") + var got strfmt.RGBColor + stringFormatRoundTrip(t, coll, "rgbcolor_test", original, &got) + assert.EqualT(t, original, got) + }) + + t.Run("Password", func(t *testing.T) { + coll := Setup(t) + original := strfmt.Password("super secret stuff here") + var got strfmt.Password + stringFormatRoundTrip(t, coll, "password_test", original, &got) + assert.EqualT(t, original, got) + }) +} diff --git a/mongo.go b/mongo.go index 3be76f0..be904ff 100644 --- a/mongo.go +++ b/mongo.go @@ -9,68 +9,89 @@ import ( "fmt" "time" + "github.com/go-openapi/strfmt/internal/bsonlite" "github.com/oklog/ulid/v2" - "go.mongodb.org/mongo-driver/v2/bson" ) +// bsonMarshaler is satisfied by types implementing MarshalBSON. +type bsonMarshaler interface { + MarshalBSON() ([]byte, error) +} + +// bsonUnmarshaler is satisfied by types implementing UnmarshalBSON. +type bsonUnmarshaler interface { + UnmarshalBSON(data []byte) error +} + +// bsonValueMarshaler is satisfied by types implementing MarshalBSONValue. +type bsonValueMarshaler interface { + MarshalBSONValue() (byte, []byte, error) +} + +// bsonValueUnmarshaler is satisfied by types implementing UnmarshalBSONValue. +type bsonValueUnmarshaler interface { + UnmarshalBSONValue(tpe byte, data []byte) error +} + +// Compile-time interface checks. var ( - _ bson.Marshaler = Date{} - _ bson.Unmarshaler = &Date{} - _ bson.Marshaler = Base64{} - _ bson.Unmarshaler = &Base64{} - _ bson.Marshaler = Duration(0) - _ bson.Unmarshaler = (*Duration)(nil) - _ bson.Marshaler = DateTime{} - _ bson.Unmarshaler = &DateTime{} - _ bson.Marshaler = ULID{} - _ bson.Unmarshaler = &ULID{} - _ bson.Marshaler = URI("") - _ bson.Unmarshaler = (*URI)(nil) - _ bson.Marshaler = Email("") - _ bson.Unmarshaler = (*Email)(nil) - _ bson.Marshaler = Hostname("") - _ bson.Unmarshaler = (*Hostname)(nil) - _ bson.Marshaler = IPv4("") - _ bson.Unmarshaler = (*IPv4)(nil) - _ bson.Marshaler = IPv6("") - _ bson.Unmarshaler = (*IPv6)(nil) - _ bson.Marshaler = CIDR("") - _ bson.Unmarshaler = (*CIDR)(nil) - _ bson.Marshaler = MAC("") - _ bson.Unmarshaler = (*MAC)(nil) - _ bson.Marshaler = Password("") - _ bson.Unmarshaler = (*Password)(nil) - _ bson.Marshaler = UUID("") - _ bson.Unmarshaler = (*UUID)(nil) - _ bson.Marshaler = UUID3("") - _ bson.Unmarshaler = (*UUID3)(nil) - _ bson.Marshaler = UUID4("") - _ bson.Unmarshaler = (*UUID4)(nil) - _ bson.Marshaler = UUID5("") - _ bson.Unmarshaler = (*UUID5)(nil) - _ bson.Marshaler = UUID7("") - _ bson.Unmarshaler = (*UUID7)(nil) - _ bson.Marshaler = ISBN("") - _ bson.Unmarshaler = (*ISBN)(nil) - _ bson.Marshaler = ISBN10("") - _ bson.Unmarshaler = (*ISBN10)(nil) - _ bson.Marshaler = ISBN13("") - _ bson.Unmarshaler = (*ISBN13)(nil) - _ bson.Marshaler = CreditCard("") - _ bson.Unmarshaler = (*CreditCard)(nil) - _ bson.Marshaler = SSN("") - _ bson.Unmarshaler = (*SSN)(nil) - _ bson.Marshaler = HexColor("") - _ bson.Unmarshaler = (*HexColor)(nil) - _ bson.Marshaler = RGBColor("") - _ bson.Unmarshaler = (*RGBColor)(nil) - _ bson.Marshaler = ObjectId{} - _ bson.Unmarshaler = &ObjectId{} - - _ bson.ValueMarshaler = DateTime{} - _ bson.ValueUnmarshaler = &DateTime{} - _ bson.ValueMarshaler = ObjectId{} - _ bson.ValueUnmarshaler = &ObjectId{} + _ bsonMarshaler = Date{} + _ bsonUnmarshaler = &Date{} + _ bsonMarshaler = Base64{} + _ bsonUnmarshaler = &Base64{} + _ bsonMarshaler = Duration(0) + _ bsonUnmarshaler = (*Duration)(nil) + _ bsonMarshaler = DateTime{} + _ bsonUnmarshaler = &DateTime{} + _ bsonMarshaler = ULID{} + _ bsonUnmarshaler = &ULID{} + _ bsonMarshaler = URI("") + _ bsonUnmarshaler = (*URI)(nil) + _ bsonMarshaler = Email("") + _ bsonUnmarshaler = (*Email)(nil) + _ bsonMarshaler = Hostname("") + _ bsonUnmarshaler = (*Hostname)(nil) + _ bsonMarshaler = IPv4("") + _ bsonUnmarshaler = (*IPv4)(nil) + _ bsonMarshaler = IPv6("") + _ bsonUnmarshaler = (*IPv6)(nil) + _ bsonMarshaler = CIDR("") + _ bsonUnmarshaler = (*CIDR)(nil) + _ bsonMarshaler = MAC("") + _ bsonUnmarshaler = (*MAC)(nil) + _ bsonMarshaler = Password("") + _ bsonUnmarshaler = (*Password)(nil) + _ bsonMarshaler = UUID("") + _ bsonUnmarshaler = (*UUID)(nil) + _ bsonMarshaler = UUID3("") + _ bsonUnmarshaler = (*UUID3)(nil) + _ bsonMarshaler = UUID4("") + _ bsonUnmarshaler = (*UUID4)(nil) + _ bsonMarshaler = UUID5("") + _ bsonUnmarshaler = (*UUID5)(nil) + _ bsonMarshaler = UUID7("") + _ bsonUnmarshaler = (*UUID7)(nil) + _ bsonMarshaler = ISBN("") + _ bsonUnmarshaler = (*ISBN)(nil) + _ bsonMarshaler = ISBN10("") + _ bsonUnmarshaler = (*ISBN10)(nil) + _ bsonMarshaler = ISBN13("") + _ bsonUnmarshaler = (*ISBN13)(nil) + _ bsonMarshaler = CreditCard("") + _ bsonUnmarshaler = (*CreditCard)(nil) + _ bsonMarshaler = SSN("") + _ bsonUnmarshaler = (*SSN)(nil) + _ bsonMarshaler = HexColor("") + _ bsonUnmarshaler = (*HexColor)(nil) + _ bsonMarshaler = RGBColor("") + _ bsonUnmarshaler = (*RGBColor)(nil) + _ bsonMarshaler = ObjectId{} + _ bsonUnmarshaler = &ObjectId{} + + _ bsonValueMarshaler = DateTime{} + _ bsonValueUnmarshaler = &DateTime{} + _ bsonValueMarshaler = ObjectId{} + _ bsonValueUnmarshaler = &ObjectId{} ) const ( @@ -80,98 +101,104 @@ const ( ) func (d Date) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": d.String()}) + return bsonlite.C.MarshalDoc(d.String()) } func (d *Date) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - if data, ok := m["data"].(string); ok { - rd, err := time.ParseInLocation(RFC3339FullDate, data, DefaultTimeLocation) - if err != nil { - return err - } - *d = Date(rd) - return nil + s, ok := v.(string) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes value as Date: %w", ErrFormat) } - return fmt.Errorf("couldn't unmarshal bson bytes value as Date: %w", ErrFormat) + rd, err := time.ParseInLocation(RFC3339FullDate, s, DefaultTimeLocation) + if err != nil { + return err + } + *d = Date(rd) + return nil } // MarshalBSON document from this value. func (b Base64) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": b.String()}) + return bsonlite.C.MarshalDoc(b.String()) } // UnmarshalBSON document into this value. func (b *Base64) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - if bd, ok := m["data"].(string); ok { - vb, err := base64.StdEncoding.DecodeString(bd) - if err != nil { - return err - } - *b = Base64(vb) - return nil + s, ok := v.(string) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes as base64: %w", ErrFormat) + } + + vb, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err } - return fmt.Errorf("couldn't unmarshal bson bytes as base64: %w", ErrFormat) + *b = Base64(vb) + return nil } func (d Duration) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": d.String()}) + return bsonlite.C.MarshalDoc(d.String()) } func (d *Duration) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - if data, ok := m["data"].(string); ok { - rd, err := ParseDuration(data) - if err != nil { - return err - } - *d = Duration(rd) - return nil + s, ok := v.(string) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes value as Duration: %w", ErrFormat) } - return fmt.Errorf("couldn't unmarshal bson bytes value as Date: %w", ErrFormat) + rd, err := ParseDuration(s) + if err != nil { + return err + } + *d = Duration(rd) + return nil } // MarshalBSON renders the [DateTime] as a BSON document. func (t DateTime) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": t}) + tNorm := NormalizeTimeForMarshal(time.Time(t)) + return bsonlite.C.MarshalDoc(tNorm) } // UnmarshalBSON reads the [DateTime] from a BSON document. func (t *DateTime) UnmarshalBSON(data []byte) error { - var obj struct { - Data DateTime - } - - if err := bson.Unmarshal(data, &obj); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - *t = obj.Data - + tv, ok := v.(time.Time) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes value as DateTime: %w", ErrFormat) + } + *t = DateTime(tv) return nil } +// MarshalBSONValue marshals a [DateTime] as a BSON DateTime value (type 0x09), +// an int64 representing milliseconds since epoch. +// // MarshalBSONValue is an interface implemented by types that can marshal themselves -// into a BSON document represented as bytes. The bytes returned must be a valid -// BSON document if the error is nil. +// into a BSON document represented as bytes. // -// Marshals a [DateTime] as a bson.[bson.TypeDateTime], an int64 representing -// milliseconds since epoch. +// The bytes returned must be a valid BSON document if the error is nil. func (t DateTime) MarshalBSONValue() (byte, []byte, error) { // UnixNano cannot be used directly, the result of calling UnixNano on the zero // Time is undefined. Thats why we use time.Nanosecond() instead. @@ -181,15 +208,12 @@ func (t DateTime) MarshalBSONValue() (byte, []byte, error) { buf := make([]byte, bsonDateTimeSize) binary.LittleEndian.PutUint64(buf, uint64(i64)) //nolint:gosec // it's okay to handle negative int64 this way - return byte(bson.TypeDateTime), buf, nil + return bsonlite.TypeDateTime, buf, nil } -// UnmarshalBSONValue is an interface implemented by types that can unmarshal a -// BSON value representation of themselves. The BSON bytes and type can be -// assumed to be valid. UnmarshalBSONValue must copy the BSON value bytes if it -// wishes to retain the data after returning. +// UnmarshalBSONValue unmarshals a BSON DateTime value into this [DateTime]. func (t *DateTime) UnmarshalBSONValue(tpe byte, data []byte) error { - if tpe == byte(bson.TypeNull) { + if tpe == bsonlite.TypeNull { *t = DateTime{} return nil } @@ -206,438 +230,371 @@ func (t *DateTime) UnmarshalBSONValue(tpe byte, data []byte) error { // MarshalBSON document from this value. func (u ULID) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *ULID) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - if ud, ok := m["data"].(string); ok { - id, err := ulid.ParseStrict(ud) - if err != nil { - return fmt.Errorf("couldn't parse bson bytes as ULID: %w: %w", err, ErrFormat) - } - u.ULID = id - return nil + s, ok := v.(string) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes as ULID: %w", ErrFormat) + } + + id, err := ulid.ParseStrict(s) + if err != nil { + return fmt.Errorf("couldn't parse bson bytes as ULID: %w: %w", err, ErrFormat) } - return fmt.Errorf("couldn't unmarshal bson bytes as ULID: %w", ErrFormat) + u.ULID = id + return nil +} + +// unmarshalBSONString is a helper for string-based strfmt types. +func unmarshalBSONString(data []byte, typeName string) (string, error) { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { + return "", err + } + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("couldn't unmarshal bson bytes as %s: %w", typeName, ErrFormat) + } + return s, nil } // MarshalBSON document from this value. func (u URI) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *URI) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "uri") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = URI(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as uri: %w", ErrFormat) + *u = URI(s) + return nil } // MarshalBSON document from this value. func (e Email) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": e.String()}) + return bsonlite.C.MarshalDoc(e.String()) } // UnmarshalBSON document into this value. func (e *Email) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "email") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *e = Email(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as email: %w", ErrFormat) + *e = Email(s) + return nil } // MarshalBSON document from this value. func (h Hostname) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": h.String()}) + return bsonlite.C.MarshalDoc(h.String()) } // UnmarshalBSON document into this value. func (h *Hostname) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "hostname") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *h = Hostname(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as hostname: %w", ErrFormat) + *h = Hostname(s) + return nil } // MarshalBSON document from this value. func (u IPv4) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *IPv4) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "ipv4") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = IPv4(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as ipv4: %w", ErrFormat) + *u = IPv4(s) + return nil } // MarshalBSON document from this value. func (u IPv6) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *IPv6) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "ipv6") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = IPv6(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as ipv6: %w", ErrFormat) + *u = IPv6(s) + return nil } // MarshalBSON document from this value. func (u CIDR) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *CIDR) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "CIDR") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = CIDR(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as CIDR: %w", ErrFormat) + *u = CIDR(s) + return nil } // MarshalBSON document from this value. func (u MAC) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *MAC) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "MAC") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = MAC(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as MAC: %w", ErrFormat) + *u = MAC(s) + return nil } // MarshalBSON document from this value. func (r Password) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": r.String()}) + return bsonlite.C.MarshalDoc(r.String()) } // UnmarshalBSON document into this value. func (r *Password) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "Password") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *r = Password(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as Password: %w", ErrFormat) + *r = Password(s) + return nil } // MarshalBSON document from this value. func (u UUID) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *UUID) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "UUID") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = UUID(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as UUID: %w", ErrFormat) + *u = UUID(s) + return nil } // MarshalBSON document from this value. func (u UUID3) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *UUID3) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "UUID3") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = UUID3(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as UUID3: %w", ErrFormat) + *u = UUID3(s) + return nil } // MarshalBSON document from this value. func (u UUID4) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *UUID4) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "UUID4") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = UUID4(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as UUID4: %w", ErrFormat) + *u = UUID4(s) + return nil } // MarshalBSON document from this value. func (u UUID5) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *UUID5) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "UUID5") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = UUID5(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as UUID5: %w", ErrFormat) + *u = UUID5(s) + return nil } // MarshalBSON document from this value. func (u UUID7) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *UUID7) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "UUID7") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = UUID7(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as UUID7: %w", ErrFormat) + *u = UUID7(s) + return nil } // MarshalBSON document from this value. func (u ISBN) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *ISBN) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "ISBN") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = ISBN(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as ISBN: %w", ErrFormat) + *u = ISBN(s) + return nil } // MarshalBSON document from this value. func (u ISBN10) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *ISBN10) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "ISBN10") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = ISBN10(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as ISBN10: %w", ErrFormat) + *u = ISBN10(s) + return nil } // MarshalBSON document from this value. func (u ISBN13) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *ISBN13) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "ISBN13") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = ISBN13(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as ISBN13: %w", ErrFormat) + *u = ISBN13(s) + return nil } // MarshalBSON document from this value. func (u CreditCard) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *CreditCard) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "CreditCard") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = CreditCard(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as CreditCard: %w", ErrFormat) + *u = CreditCard(s) + return nil } // MarshalBSON document from this value. func (u SSN) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": u.String()}) + return bsonlite.C.MarshalDoc(u.String()) } // UnmarshalBSON document into this value. func (u *SSN) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "SSN") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *u = SSN(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as SSN: %w", ErrFormat) + *u = SSN(s) + return nil } // MarshalBSON document from this value. func (h HexColor) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": h.String()}) + return bsonlite.C.MarshalDoc(h.String()) } // UnmarshalBSON document into this value. func (h *HexColor) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "HexColor") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *h = HexColor(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as HexColor: %w", ErrFormat) + *h = HexColor(s) + return nil } // MarshalBSON document from this value. func (r RGBColor) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": r.String()}) + return bsonlite.C.MarshalDoc(r.String()) } // UnmarshalBSON document into this value. func (r *RGBColor) UnmarshalBSON(data []byte) error { - var m bson.M - if err := bson.Unmarshal(data, &m); err != nil { + s, err := unmarshalBSONString(data, "RGBColor") + if err != nil { return err } - - if ud, ok := m["data"].(string); ok { - *r = RGBColor(ud) - return nil - } - return fmt.Errorf("couldn't unmarshal bson bytes as RGBColor: %w", ErrFormat) + *r = RGBColor(s) + return nil } // MarshalBSON renders the object id as a BSON document. func (id ObjectId) MarshalBSON() ([]byte, error) { - return bson.Marshal(bson.M{"data": bson.ObjectID(id)}) + return bsonlite.C.MarshalDoc([12]byte(id)) } // UnmarshalBSON reads the objectId from a BSON document. func (id *ObjectId) UnmarshalBSON(data []byte) error { - var obj struct { - Data bson.ObjectID - } - if err := bson.Unmarshal(data, &obj); err != nil { + v, err := bsonlite.C.UnmarshalDoc(data) + if err != nil { return err } - *id = ObjectId(obj.Data) + + oid, ok := v.([12]byte) + if !ok { + return fmt.Errorf("couldn't unmarshal bson bytes as ObjectId: %w", ErrFormat) + } + *id = ObjectId(oid) return nil } -// MarshalBSONValue is an interface implemented by types that can marshal themselves -// into a BSON document represented as bytes. The bytes returned must be a valid -// BSON document if the error is nil. +// MarshalBSONValue marshals the [ObjectId] as a raw BSON ObjectID value. func (id ObjectId) MarshalBSONValue() (byte, []byte, error) { - oid := bson.ObjectID(id) - return byte(bson.TypeObjectID), oid[:], nil + oid := [12]byte(id) + return bsonlite.TypeObjectID, oid[:], nil } -// UnmarshalBSONValue is an interface implemented by types that can unmarshal a -// BSON value representation of themselves. The BSON bytes and type can be -// assumed to be valid. UnmarshalBSONValue must copy the BSON value bytes if it -// wishes to retain the data after returning. +// UnmarshalBSONValue unmarshals a raw BSON ObjectID value into this [ObjectId]. func (id *ObjectId) UnmarshalBSONValue(_ byte, data []byte) error { - var oid bson.ObjectID + var oid [12]byte copy(oid[:], data) *id = ObjectId(oid) return nil diff --git a/mongo_test.go b/mongo_test.go index 4fbfa37..defec76 100644 --- a/mongo_test.go +++ b/mongo_test.go @@ -11,24 +11,23 @@ import ( "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" "github.com/google/uuid" - "go.mongodb.org/mongo-driver/v2/bson" ) type testableBSONFormat interface { testableFormat - bson.Marshaler - bson.Unmarshaler + bsonMarshaler + bsonUnmarshaler } func TestBSONDate(t *testing.T) { dateOriginal := Date(time.Date(2014, 10, 10, 0, 0, 0, 0, time.UTC)) - bsonData, err := bson.Marshal(&dateOriginal) + bsonData, err := dateOriginal.MarshalBSON() require.NoError(t, err) var dateCopy Date - err = bson.Unmarshal(bsonData, &dateCopy) + err = dateCopy.UnmarshalBSON(bsonData) require.NoError(t, err) assert.EqualT(t, dateOriginal, dateCopy) } @@ -38,22 +37,22 @@ func TestBSONBase64(t *testing.T) { b := []byte(b64) subj := Base64(b) - bsonData, err := bson.Marshal(subj) + bsonData, err := subj.MarshalBSON() require.NoError(t, err) var b64Copy Base64 - err = bson.Unmarshal(bsonData, &b64Copy) + err = b64Copy.UnmarshalBSON(bsonData) require.NoError(t, err) assert.Equal(t, subj, b64Copy) } func TestBSONDuration(t *testing.T) { dur := Duration(42) - bsonData, err := bson.Marshal(&dur) + bsonData, err := dur.MarshalBSON() require.NoError(t, err) var durCopy Duration - err = bson.Unmarshal(bsonData, &durCopy) + err = durCopy.UnmarshalBSON(bsonData) require.NoError(t, err) assert.EqualT(t, dur, durCopy) } @@ -63,27 +62,14 @@ func TestBSONDateTime(t *testing.T) { t.Logf("Case #%d", caseNum) dt := DateTime(example.time) - bsonData, err := bson.Marshal(&dt) + bsonData, err := dt.MarshalBSON() require.NoError(t, err) var dtCopy DateTime - err = bson.Unmarshal(bsonData, &dtCopy) + err = dtCopy.UnmarshalBSON(bsonData) require.NoError(t, err) // BSON DateTime type loses timezone information, so compare UTC() assert.EqualT(t, time.Time(dt).UTC(), time.Time(dtCopy).UTC()) - - // Check value marshaling explicitly - m := bson.M{"data": dt} - bsonData, err = bson.Marshal(&m) - require.NoError(t, err) - - var mCopy bson.M - err = bson.Unmarshal(bsonData, &mCopy) - require.NoError(t, err) - - data, ok := m["data"].(DateTime) - assert.TrueT(t, ok) - assert.EqualT(t, time.Time(dt).UTC(), time.Time(data).UTC()) } } @@ -93,35 +79,22 @@ func TestBSONULID(t *testing.T) { t.Parallel() ulid, _ := ParseULID(testUlid) - bsonData, err := bson.Marshal(&ulid) + bsonData, err := ulid.MarshalBSON() require.NoError(t, err) var ulidUnmarshaled ULID - err = bson.Unmarshal(bsonData, &ulidUnmarshaled) + err = ulidUnmarshaled.UnmarshalBSON(bsonData) require.NoError(t, err) assert.EqualT(t, ulid, ulidUnmarshaled) - - // Check value marshaling explicitly - m := bson.M{"data": ulid} - bsonData, err = bson.Marshal(&m) - require.NoError(t, err) - - var mUnmarshaled bson.M - err = bson.Unmarshal(bsonData, &mUnmarshaled) - require.NoError(t, err) - - data, ok := m["data"].(ULID) - assert.TrueT(t, ok) - assert.EqualT(t, ulid, data) }) t.Run("negative", func(t *testing.T) { t.Parallel() - uuid := UUID("00000000-0000-0000-0000-000000000000") - bsonData, err := bson.Marshal(&uuid) + uid := UUID("00000000-0000-0000-0000-000000000000") + bsonData, err := uid.MarshalBSON() require.NoError(t, err) var ulidUnmarshaled ULID - err = bson.Unmarshal(bsonData, &ulidUnmarshaled) + err = ulidUnmarshaled.UnmarshalBSON(bsonData) require.Error(t, err) }) } @@ -282,12 +255,12 @@ func testBSONStringFormat(t *testing.T, what testableBSONFormat, format, with st require.NoError(t, err) // bson encoding interface - bsonData, err := bson.Marshal(what) + bsonData, err := what.MarshalBSON() require.NoError(t, err) resetValue(t, format, what) - err = bson.Unmarshal(bsonData, what) + err = what.UnmarshalBSON(bsonData) require.NoError(t, err) val := reflect.Indirect(reflect.ValueOf(what)) strVal := val.String() diff --git a/time.go b/time.go index d7424b5..c07ae04 100644 --- a/time.go +++ b/time.go @@ -169,7 +169,7 @@ func (t *DateTime) UnmarshalText(text []byte) error { // Scan scans a [DateTime] value from database driver type. func (t *DateTime) Scan(raw any) error { - // TODO: case int64: and case float64: ? + // Proposal for enhancement: case int64: and case float64: ? switch v := raw.(type) { case []byte: return t.UnmarshalText(v) From 21159c090b4c70fce75714c910aa2f2530ec6b4f Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 7 Mar 2026 22:21:55 +0100 Subject: [PATCH 2/2] fix(ci): use stable Go for integration tests The go.mod directive (go 1.24) causes setup-go to install go1.24.0, which triggers a panic in testify/v2 goroutine leak detector init(). Use go-version: stable (go1.26) until testify is patched. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Frederic BIDON --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 0634152..f39cf85 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version: stable - name: Run integration tests working-directory: internal/testintegration