SENDLINE open source
open source / mongo4s / contributing

Contributing to mongo4s

Thanks for taking the time to contribute! This document covers what you need to build the project, run its tests, and where to make changes for common kinds of contributions.

Documentation

README.md is checked two ways. ReadmeSnippets.scala under examples/ re-types most of its Scala blocks so they compile, and ReadmeReferencesSpec reads the README itself and asserts that every Object.member it names is defined in the source that owns it.

The second exists because the first is a copy: a snippet re-typed in examples/ can be corrected there while the README keeps the old spelling, which is how a geo2dsphere that no code has ever had survived in the README next to a geo2DSphere that compiles. The reference check reads the README, so a rename in the code fails it.

It is a guard, not a compiler — it verifies names, not that a block would type-check. Adding a new AST object to the README means adding it to that spec's map; a mapped source that goes missing fails the check rather than silently skipping it.

Building

JDK 25 (LTS) is required, and the build checks it before doing anything: on an older JDK it stops with a message naming the version it found rather than letting four modules compile and mongo4s-kyo fail with UnsupportedClassVersionError: class file version 69.0. Scala 3.9 itself would run on JDK 17, but kyo ships Java 25 class files and its Frame macro loads them inside the compiler, so the whole build follows the module with the highest floor rather than publishing artifacts built against different JDKs. What the build publishes targets Java 17 regardless; only an application on mongo4s-kyo needs JDK 25 at runtime, for kyo's sake. cs java --jvm 25 or brew install openjdk@25 gets you one; CI uses the same.

Publishing credentials live in ~/.sbt/sonatype_credentials, outside the repository: one file for every project that publishes, out of reach of git clean -xfd, and with no ignore rule that has to be right. A checkout without it builds and tests exactly the same — the build only adds the credentials when the file is there.

The project is built with sbt. Every module targets the same Scala version, 3.9 LTS — set once as Versions.scala3 in project/Dependencies.scala. Until 3.0.0 the build was split across two Scala versions, because kyo, rapid and bson-calypso needed a compiler newer than the then-current LTS; 3.9 being an LTS is what removed that split.

it is the exception, on purpose: it compiles on Versions.scala3Next (3.10) against the library compiled by 3.9. Inline code and macros expand in their caller, so what a user on a newer compiler sees is only visible from a module built by that compiler — the ArrowAssoc deprecation 5.0.1 fixed passed every check on 3.9, and on a 3.10 build of the library's own sources too. Keep scala3Next on the newest Scala 3 release.

bash
sbt compile                 # compile everything
sbt "testOnly *"            # run the unit tests of every aggregated module
sbt "it/testOnly *"         # run the integration tests — needs Docker
sbt "scalafmtCheckAll; scalafmtSbtCheck; it/scalafmtCheck; it/Test/scalafmtCheck"  # verify formatting, as CI does

sbt 2 takes several tasks as one quoted string separated by ; — sbt scalafmtCheckAll test is parsed as one command and rejected. It also caches test runs and replays a cached run's log, so a repeated test with unchanged inputs runs nothing; testOnly * is what CI uses, and varying the filter (testOnly mongo4s.*) forces a real run. scalafmtCheckAll does not cover it/ — it is not aggregated — nor the .sbt files, which is why the format check above names them.

To run a single module's tests:

bash
sbt core/test
sbt "repositoriesTests/testOnly mongo4s.repositories.CodecRepositorySpec"

Project layout

Path What lives there
bson/core BsonEncoder/BsonDecoder/BsonDocumentCodec — the scalar + document codec seam every bridge targets
bson/medeia, bson/zio, bson/calypso bridges from a third-party codec library into mongo4s.bson.BsonDocumentCodec
bson/direct WireCodec — mongo4s's own AST-free codec, no third-party dependency
core MongoClient/MongoDatabase/MongoCollection, Field/Filter/Update/PrimaryKey/WithId — runtime- and codec-agnostic
runtime/cats, runtime/zio, runtime/kyo, runtime/rapid given Effect[F] + given RsBridge[F, S] per runtime
repositories BaseMongoRepository, Repository, Page
testkit FakeMongoCollection and FakeRepository, published so consumers can unit-test against them too
repositories-tests cross-cutting tests that need more than one runtime or codec bridge in scope at once
it tests against a real MongoDB via Testcontainers — needs Docker
examples runnable end-to-end examples, one per runtime/codec combination
benchmarks JMH benchmarks comparing runtimes, codecs, and mongo4s against mongo4cats

Adding a new BSON codec bridge

A bridge only needs to produce mongo4s.bson.BsonEncoder[A] / BsonDecoder[A] for scalars, and BsonDocumentCodec[A] for whole documents, from whatever typeclasses the target library already exposes — see bson/medeia, bson/zio, or bson/calypso for the pattern. Each bridge module also gets a domain.scala exposing type/val aliases for the target library's own types (e.g. mongo4s.bson.calypso.CalypsoEncoder), so users importing mongo4s.bson.<bridge>.* never need to write import some.library.Encoder as ... themselves.

Adding a new runtime backend

A runtime backend needs a given Effect[F] (pure/delay/map/flatMap/raiseError/ handleErrorWith/guaranteeCase — those seven are the abstract members; everything else has a default and may be overridden) and a given RsBridge[F, S] (one/option/list/unit/stream, bridging a org.reactivestreams.Publisher[A] into F[A]/S[A]) — see runtime/cats for the reference implementation.

Override bracketCase if the runtime has a real bracket primitive. Its default is flatMap(acquire)(a => guaranteeCase(use(a))(…)), which registers the finalizer only after acquisition has already returned — a fiber cancelled in that window drops the resource. cats and zio override it with primitives that acquire uninterruptibly; kyo and rapid do not, because their own bracket helpers (Sync.acquireReleaseWith, and rapid's guarantee) are built the same acquire.map(register) way, so overriding would buy nothing. Check what the runtime actually offers before assuming the default is safe there. If the runtime can't derive a Tag-like witness for a fully generic type parameter (as with kyo), gate stream behind mongo4s.Streamable[S, A] rather than throwing — see runtime/kyo/KyoBridgeInstance.scala for how that constraint is satisfied and threaded through.

Tests

  • Unit tests for core and the repository layer run against FakeMongoCollection, an in-memory implementation interpreting the same Filter/Update AST the real driver does - no MongoDB required. The repository specs live in repositories-tests rather than repositories, because testkit depends on repositories and a test dependency the other way would be a cycle.
  • it/ and the *DirectRepositoryItSpec/RepositoryItSpec families run against a real MongoDB started via Testcontainers - you need Docker running locally to execute these (sbt "it/testOnly *"). The Atlas Search specs pull mongodb/mongodb-atlas-local (about 700 MB) the first time. On Windows the forks point Testcontainers at tcp://localhost:2375, so enable Docker Desktop's "Expose daemon on tcp://localhost:2375 without TLS", or set DOCKER_HOST yourself.
  • New runtime backends or codec bridges should be exercised through the shared spec traits (RepositoryBackendSpec in repositories-tests, DirectRepositoryItSpec in it) rather than one-off tests, so they get the same coverage as every existing backend for free.

Code style

Formatting is enforced by scalafmt (.scalafmt.conf) and checked in CI. Run sbt scalafmtAll before opening a PR.

The code carries no comments and no scaladoc. Make it type-safe and obvious instead; where the logic is subtle enough that it needs explaining, the explanation belongs in the docs — README.md, DESIGN.md or COMPATIBILITY.md — not in a comment nobody reading the docs will find. examples/ is the exception: its section markers are what tie the code to the README.

The build turns on more warnings than the compiler does by default, because "the compiler is the test" only holds if it is asked. Beyond -Wunused:all and -Wvalue-discard: -WunstableInlineAccessors (the one that guards the binary-compatibility promise — see DESIGN.md), -Wshadow:all, -Winfer-union, -Wimplausible-patterns and -Wrecurse-with-default. Any warning fails the build (-Werror), except under the -source:future check, whose point is to see what a future compiler will say. One message is silenced: kyo's Frame macro expands to @nowarn annotations that suppress nothing, which is kyo's to fix, not ours.

Releasing

Three steps are easy to forget because nothing fails when they are missed:

  • Move the MiMa baseline. binaryCompatibleWith in build.sbt names the releases the artifacts are checked against. It is Set.empty while a major version is being prepared, because there is nothing compatible to compare to; the moment that version is on Maven Central it must become Set("<that version>"), or the compatibility promise is asserted and never checked.
  • Record any deliberate break. A break inside a major version needs a MiMa exclusion in build.sbt and an entry in COMPATIBILITY.md. A major release needs no filters, but its migration guide still belongs there.
  • Add the release to CHANGELOG.md, in Added / Changed / Fixed order. The GitHub release notes can be the same content in prose; the changelog is the version that stays with the source.

The site at oss.sendline.now/mongo4s is built in its own repository from this README and the documents it links, so a change to the docs here is what the site picks up.

6.0.0 goes to Central twice, as two deployments. The first is the release itself under now.sendline (publishSigned). The second is a relocation under org.mongo4s (publishRelocation): it sets relocated for every module, publishes a pom naming now.sendline and no jars, and then unsets it. Both stage into target/sona-staging, which keeps whatever earlier releases left there, so empty it before each deployment. No later release publishes under org.mongo4s.

Opening a pull request

  • Keep PRs focused - one runtime backend, one codec bridge, or one bug fix per PR is easier to review than a mix.
  • Add or extend tests for anything behavioral you change.
  • The format check and sbt "testOnly *" should pass locally before you push (integration tests need Docker and aren't required for most changes, but CI will run them too).
built from CONTRIBUTING.md at v6.0.0 edit this page on GitHub