Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8333314
refactor(scala-3,core): GenCodec implicit class → extension (4 privat…
halotukozak Jun 1, 2026
4467a1d
refactor(scala-3,mongo): UpdateOperatorsDsl implicit class → given Co…
halotukozak Jun 1, 2026
d1e0195
refactor(scala-3,mongo): QueryOperatorsDsl implicit class → extension
halotukozak Jun 1, 2026
92a8eb2
refactor(scala-3,mongo): MongoEntityCompanion macroDslExtensions impl…
halotukozak Jun 1, 2026
2e382f3
refactor(scala-3,mongo): MongoPolyDataCompanion macroDslExtensions im…
halotukozak Jun 1, 2026
88de8c7
refactor(scala-3,mongo): MongoFormat assume* implicit class → extensi…
halotukozak Jun 1, 2026
c7c423c
refactor(scala-3,mongo): MongoPropertyRef CollectionRefOps/Dictionary…
halotukozak Jun 1, 2026
c3a3fc1
docs(migration): record implicit class → extension source-compat impact
halotukozak Jun 1, 2026
df194af
style(scala-3,mongo): import targetName, drop fully-qualified annotation
halotukozak Jun 1, 2026
820ff26
docs(scala-3,mongo): TODO note for ForCollection conversion → extension
halotukozak Jun 1, 2026
f8ca7e6
refactor(scala-3,mongo): ReactiveMongoExtensions implicit def + AnyVa…
halotukozak Jun 1, 2026
72e7bd5
refactor(scala-3,mongo): MongoOps DBOps/FindIterableOps → extension
halotukozak Jun 1, 2026
71c5ecd
refactor(scala-3,core): JavaTimeInterop InstantOps → extension
halotukozak Jun 1, 2026
fe96455
refactor(scala-3,core): Java8CollectionUtils implicit def + AnyVal → …
halotukozak Jun 1, 2026
66ada4e
refactor(scala-3,core): GuavaInterop implicit def + AnyVal → extension
halotukozak Jun 1, 2026
16666fb
refactor(scala-3,core): JOptionalUtils implicit def + AnyVal → extens…
halotukozak Jun 1, 2026
7ce1d00
refactor(scala-3,core): JStreamUtils implicit def + AnyVal → extension
halotukozak Jun 1, 2026
d40eaeb
refactor(scala-3,core): TaskExtensions TaskOps/TaskCompanionOps → ext…
halotukozak Jun 1, 2026
a4f30ed
refactor(scala-3,core): JCollectionUtils pairIterableOps implicit def…
halotukozak Jun 1, 2026
a30ff9e
refactor(scala-3,core): Opt.LazyOptOps implicit def + AnyVal → extension
halotukozak Jun 1, 2026
06aed30
refactor(scala-3,core): SharedExtensions implicit def + AnyVal → exte…
halotukozak Jun 1, 2026
dd2e5fb
refactor(scala-3,core): JsInterop UndefOrOps/JsOptOps implicit def + …
halotukozak Jun 1, 2026
caaa3df
style(scala-3): scalafmt fixups for slice 3.1 extension sweep
halotukozak Jun 1, 2026
f37b442
docs(migration): record de-facto-implicit-class → extension sweep (sl…
halotukozak Jun 1, 2026
af4d454
refactor(scala-3,mongo): convert UpdateOperatorsDsl.ForCollection to …
halotukozak Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,39 @@ the bottom of this file. Restoration ships incrementally per feature area.
compiles).
- `enum` was renamed to `e` at one call site in `GenKeyCodec` (`enum` is reserved in Scala 3).
- `@targetName` annotation added to `CloseableIterator` overloaded methods.
- `GenCodec` internal value-class wrappers (`IterableOps`, `PairIterableOps`, `ListInputOps`, `ObjectInputOps`)
converted from `private implicit class … extends AnyVal` to Scala 3 `extension` blocks. All four are package-private
internal helpers — call-site transparent (no downstream API impact).
- De-facto-implicit-class pattern (`implicit def xOps(x: T): XOps = new XOps(x)` paired with
`class XOps(private val x: T) extends AnyVal { ... }`) swept to `extension` blocks across `core`:
- `SharedExtensions` — 18 wrapper pairs (UniversalOps, LazyUniversalOps, NullableOps, StringOps, IntOps, FutureOps,
LazyFutureOps, OptionOps, TryOps, LazyTryOps, PartialFunctionOps, SetOps, IterableOnceOps, IterableOps,
PairIterableOnceOps, MapOps, IteratorOps, plus the `Future.type`/`Try.type`/`Iterator.type` companion extensions).
`SharedExtensionsUtils` companion object eliminated. Helper singletons `FutureCompanionOps`, `TryCompanionOps`,
`IteratorCompanionOps`, `PartialFunctionOps`, `MapOps.Entry` promoted to `object SharedExtensions` companion.
`OrderingOps` and `IteratorOps.distinct/distinctBy` dropped per existing MiMa exclusions (already unreachable).
- `Opt.LazyOptOps` — by-name extension `extension [A](opt: => Opt[A])`.
- `concurrent/TaskExtensions` — `TaskOps` and `TaskCompanionOps` (`extension [T](task: Task[T])` and
`extension (task: Task.type)`). Inner `import ObservableExtensions.observableOps` hoisted above the trait
(Pitfall 6: extension body cannot contain imports).
- `jiop/Java8CollectionUtils`, `jiop/JOptionalUtils`, `jiop/JStreamUtils`, `jiop/JavaTimeInterop`, `jiop/GuavaInterop`,
`jiop/JCollectionUtils.pairIterableOps` — all wrapper pairs swept to `extension`. JOptionalUtils consolidates the
4 `O[T] → JOptional[T]` wrappers (Option/Opt/NOpt/OptArg) into ONE generic
`extension [O[_], T](opt: O[T])(using OptionLike.Aux[O[T], T])` per fork shape — avoids both the erasure clash
between value-class wrappers AND the `Seq.asJava` shadowing by an over-specific `extension (option: Option[T])`.
`JavaInteropTest` gained `import JavaInterop._` so JOptionalUtils' `asScala` extension on `JOptional` competes at
equal import-rank with GuavaInterop's `asScala` on `ListenableFuture` (Scala 3 prefers imported extensions over
package-object-mixed-in extensions and won't backtrack on first-tried mismatch).
- `jsiop/JsInterop` — `UndefOrOps` and `JsOptOps` swept (`jsDateTimestampConversions` left as `implicit def` —
slice 3.3 territory).
- `mongo/sync/MongoOps` — `DBOps`, `FindIterableOps` swept (`object MongoOps extends MongoOps` added for
`import MongoOps._` callers).
- `mongo/reactive/ReactiveMongoExtensions` — `PublisherOps` swept.

Public-API impact: the wrapper types (e.g. `SharedExtensions.UniversalOps`, `JOptionalUtils.optional2AsScala`,
`MongoOps.DBOps`) no longer exist as named types. Downstream code that referenced these by name (e.g.
`new UniversalOps(x)`) will not compile. Extension-method call sites (`x.opt`, `x.discard`, `optional.toOpt`,
`db.getCollection(...)`) remain transparent.

### mongo

Expand All @@ -63,6 +96,23 @@ the bottom of this file. Restoration ships incrementally per feature area.
Scala 3 forbids type projections on non-concrete prefixes). Public-API signature change.
- `BsonValueOutput.write` / `BsonValueInput.read` call sites require explicit `using` keyword.
- `MongoPolyDataCompanion` / `TypedMapFormat` / `TypedMapRefOps` widened from `K[_]` / `D[_]` to `K[Any]` / `D[Any]`.
- `implicit class XOps[…] extends AnyVal` blocks converted to Scala 3 `extension` blocks across `mongo/typed`:
- `MongoEntityCompanion.macroDslExtensions` and `MongoPolyDataCompanion.macroDslExtensions` — `extension (value: T)` /
`extension [T](value: D[T])`. Call-site transparent; downstream code that referenced these wrapper classes by name
(e.g. `new macroDslExtensions(x)`) will no longer compile (they no longer exist as named types).
- `MongoFormat.{collectionFormatOps, dictionaryFormatOps, typedMapFormatOps}` — `assume*` helpers exposed via
`extension`. Call-site transparent.
- `MongoPropertyRef.{CollectionRefOps, DictionaryRefOps, TypedMapRefOps}` — `extension` blocks. The typed-map variant
received `@scala.annotation.targetName("typedMapApply")` to disambiguate from the dictionary variant's `apply(K)`
(both erase to `apply(Object)` once promoted from value-class wrapping to extension methods sharing the companion's
namespace).
- `QueryOperatorsDsl.{VanillaQueryOperatorsDsl.ForCollection, QueryOperatorsDsl.ForCollection}` — `extension` blocks
(no named-argument inference issue). Inner helper `format` renamed to `elemFormat` to match fork shape.
- `UpdateOperatorsDsl.ForCollection` — exposed via `given Conversion[UpdateOperatorsDsl[C[T], R], ForCollection[C, T, R]]`
instead of `extension` because plain extension methods cannot infer `C`/`T` from named-argument call sites such as
`push(sort = ...)`. The `ForCollection` type is now a regular `class` (was `implicit class … extends AnyVal`);
downstream code that constructed `new ForCollection(dsl)` directly still compiles, but the previous value-class
`AnyVal` erasure is gone — boxing now occurs at conversion time. `scala.language.implicitConversions` import added.

### hocon

Expand Down
19 changes: 5 additions & 14 deletions core/js/src/main/scala/com/avsystem/commons/jsiop/JsInterop.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.avsystem.commons
package jsiop

import com.avsystem.commons.jsiop.JsInterop.{JsOptOps, UndefOrOps}
import com.avsystem.commons.misc.TimestampConversions

import scala.scalajs.js
Expand All @@ -11,20 +10,12 @@ trait JsInterop {
implicit def jsDateTimestampConversions(jsDate: js.Date): TimestampConversions =
new TimestampConversions(jsDate.getTime().toLong)

implicit def undefOrOps[A](undefOr: UndefOr[A]): UndefOrOps[A] =
new UndefOrOps(undefOr)

implicit def jsOptOps[A](opt: Opt[A]): JsOptOps[A] =
new JsOptOps(opt.orNull[Any].asInstanceOf[A])
}
object JsInterop extends JsInterop {
class UndefOrOps[A](private val value: UndefOr[A]) extends AnyVal {
def toOpt: Opt[A] = if (value.isDefined) Opt(value.get) else Opt.Empty
extension [A](undefOr: UndefOr[A]) {
def toOpt: Opt[A] = if (undefOr.isDefined) Opt(undefOr.get) else Opt.Empty
}

class JsOptOps[A](private val raw: A) extends AnyVal {
private def value = Opt(raw)

def orUndefined: UndefOr[A] = if (value.isDefined) js.defined(value.get) else js.undefined
extension [A](opt: Opt[A]) {
def orUndefined: UndefOr[A] = if (opt.isDefined) js.defined(opt.get) else js.undefined
}
}
object JsInterop extends JsInterop
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,21 @@ trait GuavaInterop {
def gSupplier[T](expr: => T): GSupplier[T] = () => expr
def gPredicate[T](pred: T => Boolean): GPredicate[T] = pred(_)

implicit def toDecorateAsScala[T](gfut: ListenableFuture[T]): DecorateFutureAsScala[T] =
new DecorateFutureAsScala(gfut)

implicit def toDecorateAsScalaPromise[T](gfut: SettableFuture[T]): DecorateSettableFutureAsScala[T] =
new DecorateSettableFutureAsScala(gfut)

implicit def toDecorateAsGuava[T](fut: Future[T]): DecorateFutureAsGuava[T] =
new DecorateFutureAsGuava(fut)
}

object GuavaInterop extends GuavaInterop {
class DecorateFutureAsScala[T](private val gfut: ListenableFuture[T]) extends AnyVal {
extension [T](gfut: ListenableFuture[T]) {
def asScala: Future[T] = gfut match {
case FutureAsListenableFuture(fut) => fut
case _ => ListenableFutureAsScala(gfut)
}

def asScalaUnit: Future[Unit] =
asScala.toUnit
gfut.asScala.toUnit
}

class DecorateSettableFutureAsScala[T](private val gfut: SettableFuture[T]) extends AnyVal {
extension [T](gfut: SettableFuture[T]) {
def asScalaPromise: Promise[T] = new SettableFutureAsPromise(gfut)
}

class DecorateFutureAsGuava[T](private val fut: Future[T]) extends AnyVal {
extension [T](fut: Future[T]) {
def asGuava: ListenableFuture[T] = fut match {
case ListenableFutureAsScala(gfut) => gfut
case _ => FutureAsListenableFuture(fut)
Expand All @@ -53,7 +42,9 @@ object GuavaInterop extends GuavaInterop {
def asGuavaVoid: ListenableFuture[Void] =
fut.toVoid.asGuava
}
}

object GuavaInterop extends GuavaInterop {
private case class ListenableFutureAsScala[+T](gfut: ListenableFuture[T @uncheckedVariance]) extends Future[T] {
def isCompleted: Boolean =
gfut.isDone
Expand Down
161 changes: 45 additions & 116 deletions core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala
Original file line number Diff line number Diff line change
@@ -1,187 +1,116 @@
package com.avsystem.commons
package jiop

import com.avsystem.commons.meta.OptionLike

import java.{util => ju}

trait JOptionalUtils {

import JOptionalUtils._

type JOptional[T] = ju.Optional[T]
type JOptionalDouble = ju.OptionalDouble
type JOptionalInt = ju.OptionalInt
type JOptionalLong = ju.OptionalLong

implicit def optional2AsScala[T](optional: JOptional[T]): optional2AsScala[T] =
new optional2AsScala(optional)

implicit def optionalDouble2AsScala(optional: JOptionalDouble): optionalDouble2AsScala =
new optionalDouble2AsScala(optional)

implicit def optionalInt2AsScala(optional: JOptionalInt): optionalInt2AsScala =
new optionalInt2AsScala(optional)

implicit def optionalLong2AsScala(optional: JOptionalLong): optionalLong2AsScala =
new optionalLong2AsScala(optional)

implicit def option2AsJava[T](option: Option[T]): option2AsJava[T] =
new option2AsJava(option)

implicit def opt2AsJava[T](opt: Opt[T]): opt2AsJava[T] =
new opt2AsJava((opt: Opt[Any]).orNull)

// NOTE: losing information about NOpt(null) vs NOpt.Empty but this is fine because JOptional doesn't distinguish
implicit def nopt2AsJava[T](opt: NOpt[T]): nopt2AsJava[T] =
new nopt2AsJava((opt: NOpt[Any]).orNull)

implicit def optArg2AsJava[T](opt: OptArg[T]): optArg2AsJava[T] =
new optArg2AsJava((opt: OptArg[Any]).orNull)

implicit def option2AsJavaDouble(option: Option[Double]): option2AsJavaDouble =
new option2AsJavaDouble(option)

implicit def option2AsJavaInt(option: Option[Int]): option2AsJavaInt =
new option2AsJavaInt(option)

implicit def option2AsJavaLong(option: Option[Long]): option2AsJavaLong =
new option2AsJavaLong(option)

object JOptional {
def apply[T](nullable: T): JOptional[T] = ju.Optional.ofNullable(nullable)

def empty[T]: JOptional[T] = ju.Optional.empty[T]()
}

object JOptionalDouble {
def apply(value: Double): JOptionalDouble = ju.OptionalDouble.of(value)

def empty: JOptionalDouble = ju.OptionalDouble.empty()
}

object JOptionalInt {
def apply(value: Int): JOptionalInt = ju.OptionalInt.of(value)

def empty: JOptionalInt = ju.OptionalInt.empty()
}

object JOptionalLong {
def apply(value: Long): JOptionalLong = ju.OptionalLong.of(value)

def empty: JOptionalLong = ju.OptionalLong.empty()
}

}

object JOptionalUtils {

final class optional2AsScala[T](private val optional: JOptional[T]) extends AnyVal {
extension [T](optional: JOptional[T]) {
def toOption: Option[T] =
if (optional.isPresent) Some(optional.get) else None

def toOpt: Opt[T] =
if (optional.isPresent) Opt(optional.get) else Opt.Empty

def asScala: Option[T] = toOption
def asScala: Option[T] = optional.toOption
}

final class optionalDouble2AsScala(private val optional: JOptionalDouble) extends AnyVal {
extension (optional: JOptionalDouble) {
def toOption: Option[Double] =
if (optional.isPresent) Some(optional.getAsDouble) else None

def toOpt: Opt[Double] =
if (optional.isPresent) Opt(optional.getAsDouble) else Opt.Empty

def asScala: Option[Double] = toOption
def asScala: Option[Double] = optional.toOption
}

final class optionalInt2AsScala(private val optional: JOptionalInt) extends AnyVal {
extension (optional: JOptionalInt) {
def toOption: Option[Int] =
if (optional.isPresent) Some(optional.getAsInt) else None

def toOpt: Opt[Int] =
if (optional.isPresent) Opt(optional.getAsInt) else Opt.Empty

def asScala: Option[Int] = toOption
def asScala: Option[Int] = optional.toOption
}

final class optionalLong2AsScala(private val optional: JOptionalLong) extends AnyVal {
extension (optional: JOptionalLong) {
def toOption: Option[Long] =
if (optional.isPresent) Some(optional.getAsLong) else None

def toOpt: Opt[Long] =
if (optional.isPresent) Opt(optional.getAsLong) else Opt.Empty

def asScala: Option[Long] = toOption
def asScala: Option[Long] = optional.toOption
}

final class option2AsJava[T](private val option: Option[T]) extends AnyVal {

/** Note that in scala Some(null) is valid value. It will throw an exception in such case, because java Optional is
* not able to hold null
*/
def toJOptional: JOptional[T] =
if (option.isDefined) ju.Optional.of(option.get) else ju.Optional.empty()
extension (option: Option[Double]) {
def toJOptionalDouble: JOptionalDouble =
if (option.isDefined) ju.OptionalDouble.of(option.get) else ju.OptionalDouble.empty()

def asJava: JOptional[T] = toJOptional
def asJavaDouble: JOptionalDouble = option.toJOptionalDouble
}

final class opt2AsJava[T](private val rawOrNull: Any) extends AnyVal {
private def opt: Opt[T] = Opt(rawOrNull.asInstanceOf[T])

/** Note that in scala Some(null) is valid value. It will throw an exception in such case, because java Optional is
* not able to hold null
*/
def toJOptional: JOptional[T] =
if (opt.isDefined) ju.Optional.of(opt.get) else ju.Optional.empty()
extension (option: Option[Int]) {
def toJOptionalInt: JOptionalInt =
if (option.isDefined) ju.OptionalInt.of(option.get) else ju.OptionalInt.empty()

def asJava: JOptional[T] = toJOptional
def asJavaInt: JOptionalInt = option.toJOptionalInt
}

// note: we have lost information about NOpt.Empty vs NOpt(null) but it doesn't matter because we only convert
// to JOptional which doesn't distinguish between them
final class nopt2AsJava[T](private val rawOrNull: Any) extends AnyVal {
private def opt: NOpt[T] = NOpt(rawOrNull.asInstanceOf[T])

/** Note that in scala Some(null) is valid value. It will throw an exception in such case, because java Optional is
* not able to hold null
*/
def toJOptional: JOptional[T] =
if (opt.isDefined) ju.Optional.of(opt.get) else ju.Optional.empty()
extension (option: Option[Long]) {
def toJOptionalLong: JOptionalLong =
if (option.isDefined) ju.OptionalLong.of(option.get) else ju.OptionalLong.empty()

def asJava: JOptional[T] = toJOptional
def asJavaLong: JOptionalLong = option.toJOptionalLong
}

final class optArg2AsJava[T](private val rawOrNull: Any) extends AnyVal {
private def opt: OptArg[T] = OptArg(rawOrNull.asInstanceOf[T])
// Single generic extension for any option-like wrapper (Option, Opt, NOpt, OptArg).
// Carries `using OptionLike.Aux[O[T], T]` so that resolution only kicks in for true
// option-like receivers, avoiding clash with `scala.collection.convert.AsJavaExtensions.asJava`
// on `Seq`/`Iterable`.
extension [O[_], T](opt: O[T])(using optionLike: OptionLike.Aux[O[T], T]) {

/** Note that in scala Some(null) is valid value. It will throw an exception in such case, because java Optional is
* not able to hold null
*/
def toJOptional: JOptional[T] =
if (opt.isDefined) ju.Optional.of(opt.get) else ju.Optional.empty()
if (optionLike.isDefined(opt)) ju.Optional.of(optionLike.get(opt)) else ju.Optional.empty()
def asJava: JOptional[T] = opt.toJOptional
}

def asJava: JOptional[T] = toJOptional
object JOptional {
def apply[T](nullable: T): JOptional[T] = ju.Optional.ofNullable(nullable)

def empty[T]: JOptional[T] = ju.Optional.empty[T]()
}

final class option2AsJavaDouble(private val option: Option[Double]) extends AnyVal {
def toJOptionalDouble: JOptionalDouble =
if (option.isDefined) ju.OptionalDouble.of(option.get) else ju.OptionalDouble.empty()
object JOptionalDouble {
def apply(value: Double): JOptionalDouble = ju.OptionalDouble.of(value)

def asJavaDouble: JOptionalDouble = toJOptionalDouble
def empty: JOptionalDouble = ju.OptionalDouble.empty()
}

final class option2AsJavaInt(private val option: Option[Int]) extends AnyVal {
def toJOptionalInt: JOptionalInt =
if (option.isDefined) ju.OptionalInt.of(option.get) else ju.OptionalInt.empty()
object JOptionalInt {
def apply(value: Int): JOptionalInt = ju.OptionalInt.of(value)

def asJavaInt: JOptionalInt = toJOptionalInt
def empty: JOptionalInt = ju.OptionalInt.empty()
}

final class option2AsJavaLong(private val option: Option[Long]) extends AnyVal {
def toJOptionalLong: JOptionalLong =
if (option.isDefined) ju.OptionalLong.of(option.get) else ju.OptionalLong.empty()
object JOptionalLong {
def apply(value: Long): JOptionalLong = ju.OptionalLong.of(value)

def asJavaLong: JOptionalLong = toJOptionalLong
def empty: JOptionalLong = ju.OptionalLong.empty()
}

}

object JOptionalUtils extends JOptionalUtils
Loading
Loading