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.
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 doessbt 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:
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
coreand the repository layer run againstFakeMongoCollection, an in-memory implementation interpreting the sameFilter/UpdateAST the real driver does - no MongoDB required. The repository specs live inrepositories-testsrather thanrepositories, becausetestkitdepends onrepositoriesand a test dependency the other way would be a cycle. it/and the*DirectRepositoryItSpec/RepositoryItSpecfamilies run against a real MongoDB started via Testcontainers - you need Docker running locally to execute these (sbt "it/testOnly *"). The Atlas Search specs pullmongodb/mongodb-atlas-local(about 700 MB) the first time. On Windows the forks point Testcontainers attcp://localhost:2375, so enable Docker Desktop's "Expose daemon on tcp://localhost:2375 without TLS", or setDOCKER_HOSTyourself.- New runtime backends or codec bridges should be exercised through the shared spec traits
(
RepositoryBackendSpecinrepositories-tests,DirectRepositoryItSpecinit) 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.
binaryCompatibleWithinbuild.sbtnames the releases the artifacts are checked against. It isSet.emptywhile a major version is being prepared, because there is nothing compatible to compare to; the moment that version is on Maven Central it must becomeSet("<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.sbtand 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).