From 833331488617d14530eeb998219a80f49c3bc5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:15:34 +0200 Subject: [PATCH 01/25] =?UTF-8?q?refactor(scala-3,core):=20GenCodec=20impl?= =?UTF-8?q?icit=20class=20=E2=86=92=20extension=20(4=20private=20wrappers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts the 4 internal value-class wrappers (IterableOps, PairIterableOps, ListInputOps, ObjectInputOps) used inside GenCodec from Scala 2-era `private implicit class … extends AnyVal` to Scala 3 `extension` blocks. The wrappers are package-private internal helpers; conversion is call-site transparent (no downstream API impact). Method bodies preserved byte-identical including their `(implicit …)` parameter lists (slice 3.3 territory). Translated from origin/master shape; fork's scala-3 overlay of GenCodec.scala diverges substantially from current single-source layout, so the mechanical `implicit class XOps[A](private val a: A) extends AnyVal { … }` → `extension [A](a: A) { … }` transform was applied per CONTEXT.md's "read fork's intent, apply the syntax change only" guidance. --- .../commons/serialization/GenCodec.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala b/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala index 97c3eef3b..5f16abf32 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala @@ -397,8 +397,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { implicit lazy val BytesCodec: GenCodec[Bytes] = GenCodec.nullableSimple(i => Bytes(i.readBinary()), (o, b) => o.writeBinary(b.bytes)) - private implicit class IterableOps[A](private val coll: BIterable[A]) extends AnyVal { - def writeToList(lo: ListOutput)(implicit writer: GenCodec[A]): Unit = { + extension [A](coll: BIterable[A]) { + private def writeToList(lo: ListOutput)(implicit writer: GenCodec[A]): Unit = { lo.declareSizeOf(coll) coll.foreach(new (A => Unit) { private var idx = 0 @@ -413,8 +413,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { } } - private implicit class PairIterableOps[A, B](private val coll: BIterable[(A, B)]) extends AnyVal { - def writeToObject(oo: ObjectOutput)(implicit keyWriter: GenKeyCodec[A], writer: GenCodec[B]): Unit = { + extension [A, B](coll: BIterable[(A, B)]) { + private def writeToObject(oo: ObjectOutput)(implicit keyWriter: GenKeyCodec[A], writer: GenCodec[B]): Unit = { oo.declareSizeOf(coll) coll.foreach { case (key, value) => val fieldName = keyWriter.write(key) @@ -426,8 +426,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { } } - private implicit class ListInputOps(private val li: ListInput) extends AnyVal { - def collectTo[A: GenCodec, C](implicit fac: Factory[A, C]): C = { + extension (li: ListInput) { + private def collectTo[A: GenCodec, C](implicit fac: Factory[A, C]): C = { val b = fac.newBuilder li.knownSize match { case -1 => @@ -447,8 +447,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { } } - private implicit class ObjectInputOps(private val oi: ObjectInput) extends AnyVal { - def collectTo[K: GenKeyCodec, V: GenCodec, C](implicit fac: Factory[(K, V), C]): C = { + extension (oi: ObjectInput) { + private def collectTo[K: GenKeyCodec, V: GenCodec, C](implicit fac: Factory[(K, V), C]): C = { val b = fac.newBuilder oi.knownSize match { case -1 => From 4467a1d8b6223d911d6df9f540f4b19dc8043900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:16:34 +0200 Subject: [PATCH 02/25] =?UTF-8?q?refactor(scala-3,mongo):=20UpdateOperator?= =?UTF-8?q?sDsl=20implicit=20class=20=E2=86=92=20given=20Conversion=20(HKT?= =?UTF-8?q?=20receiver)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ForCollection[C[X] <: Iterable[X], T, R]` has a higher-kinded receiver `UpdateOperatorsDsl[C[T], R]`. Plain `extension` cannot infer `C`/`T` from named-argument call sites like `push(sort = ...)`, so the conversion is expressed as a `given Conversion[…]` instead (fork eef0edce shape). Hoisted `import MongoUpdateOperator._` to the object level (extension/given body cannot contain imports per Pitfall 6). Added `scala.language.implicitConversions` import to enable the implicit conversion. Translated from origin/master@eef0edce. --- .../commons/mongo/typed/UpdateOperatorsDsl.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala index e841a4d2f..e98454efa 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package mongo.typed +import scala.language.implicitConversions + trait UpdateOperatorsDsl[T, R] { import MongoUpdateOperator._ @@ -23,9 +25,15 @@ trait UpdateOperatorsDsl[T, R] { def unset: R = wrapUpdateOperator(Unset()) } object UpdateOperatorsDsl { - implicit class ForCollection[C[X] <: Iterable[X], T, R](private val dsl: UpdateOperatorsDsl[C[T], R]) extends AnyVal { + import MongoUpdateOperator._ + + // A `given Conversion` (not an `extension`) is used here so the higher-kinded `C[T]` is unified once, + // at conversion time, against the receiver's `UpdateOperatorsDsl[C[T], R]` base type. Plain extension + // methods fail to infer `C`/`T` from the receiver for named-argument calls such as `push(sort = ...)`. + given [C[X] <: Iterable[X], T, R] => Conversion[UpdateOperatorsDsl[C[T], R], ForCollection[C, T, R]] = + ForCollection(_) - import MongoUpdateOperator._ + class ForCollection[C[X] <: Iterable[X], T, R](dsl: UpdateOperatorsDsl[C[T], R]) { private def format: MongoFormat[T] = dsl.format.assumeCollection.elementFormat From d1e0195b3879e0f85e3b07bdb0fd7623e6d863fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:17:35 +0200 Subject: [PATCH 03/25] =?UTF-8?q?refactor(scala-3,mongo):=20QueryOperators?= =?UTF-8?q?Dsl=20implicit=20class=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two `implicit class ForCollection` blocks (in `VanillaQueryOperatorsDsl` and `QueryOperatorsDsl` companions) converted to plain `extension`. Unlike `UpdateOperatorsDsl`, these methods do not use named-argument defaults, so plain `extension` infers `C`/`T` successfully — matching fork eef0edce shape. Hoisted `import MongoQueryOperator._` to the object level (Pitfall 6). Renamed inner helper `format` → `elemFormat` to match fork's naming and avoid ambiguity with the dsl's own `format` member. Translated from origin/master@eef0edce. --- .../commons/mongo/typed/QueryOperatorsDsl.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/QueryOperatorsDsl.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/QueryOperatorsDsl.scala index ef7f2ae15..5d6e87a2a 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/QueryOperatorsDsl.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/QueryOperatorsDsl.scala @@ -59,20 +59,18 @@ trait VanillaQueryOperatorsDsl[T, R] { wrapQueryOperators(Raw(rawOperator, bson)) } object VanillaQueryOperatorsDsl { - implicit class ForCollection[C[X] <: Iterable[X], T, R](private val dsl: VanillaQueryOperatorsDsl[C[T], R]) - extends AnyVal { - - import MongoQueryOperator._ + import MongoQueryOperator._ - private def format: MongoFormat[T] = dsl.format.assumeCollection.elementFormat + extension [C[X] <: Iterable[X], T, R](dsl: VanillaQueryOperatorsDsl[C[T], R]) { + private def elemFormat: MongoFormat[T] = dsl.format.assumeCollection.elementFormat def size(size: Int): R = dsl.wrapQueryOperators(Size(size)) def elemMatch(filter: MongoFilter.Creator[T] => MongoFilter[T]): R = - dsl.wrapQueryOperators(ElemMatch(filter(new MongoFilter.Creator(format)))) + dsl.wrapQueryOperators(ElemMatch(filter(new MongoFilter.Creator(elemFormat)))) def all(values: T*): R = all(values) - def all(values: Iterable[T]): R = dsl.wrapQueryOperators(All(values, format)) + def all(values: Iterable[T]): R = dsl.wrapQueryOperators(All(values, elemFormat)) } } @@ -92,7 +90,7 @@ trait QueryOperatorsDsl[T, R] extends VanillaQueryOperatorsDsl[T, R] { regex(SRegex.quote(infix), if (caseInsensitive) OptArg("i") else OptArg.Empty) } object QueryOperatorsDsl { - implicit class ForCollection[C[X] <: Iterable[X], T, R](private val dsl: QueryOperatorsDsl[C[T], R]) extends AnyVal { + extension [C[X] <: Iterable[X], T, R](dsl: QueryOperatorsDsl[C[T], R]) { def isEmpty: R = dsl.size(0) def contains(value: T): R = dsl.elemMatch(_.is(value)) def containsAny(values: T*): R = containsAny(values) From 92a8eb28def5c7131f008988c476a09d892929f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:19:56 +0200 Subject: [PATCH 04/25] =?UTF-8?q?refactor(scala-3,mongo):=20MongoEntityCom?= =?UTF-8?q?panion=20macroDslExtensions=20implicit=20class=20=E2=86=92=20ex?= =?UTF-8?q?tension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts the `implicit class macroDslExtensions(value: T)` block inside `BaseMongoCompanion` to a Scala 3 `extension` block. Receiver is concrete (`T`), no HKT inference issue — plain `extension` shape. Call-site transparent. Other `implicit def/val` declarations in this file (codec/format/isMongoAdtOrSubtype) are out of scope for slice 3.1 and deferred to slice 3.3 (`implicit def/val → given`). Translated from origin/master@eef0edce. --- .../com/avsystem/commons/mongo/typed/MongoEntityCompanion.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoEntityCompanion.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoEntityCompanion.scala index f59d9f430..761388a61 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoEntityCompanion.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoEntityCompanion.scala @@ -29,7 +29,7 @@ sealed abstract class BaseMongoCompanion[T] extends DataTypeDsl[T] { implicit def isMongoAdtOrSubtype[C <: T]: IsMongoAdtOrSubtype[C] = null - implicit class macroDslExtensions(value: T) { + extension (value: T) { @explicitGenerics @compileTimeOnly("the .as[Subtype] construct can only be used inside lambda passed to .ref(...) macro") def as[C <: T]: C = sys.error("stub") From 2e382f36bb5eed0312a398fbce2fe4eb9b61cba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:20:00 +0200 Subject: [PATCH 05/25] =?UTF-8?q?refactor(scala-3,mongo):=20MongoPolyDataC?= =?UTF-8?q?ompanion=20macroDslExtensions=20implicit=20class=20=E2=86=92=20?= =?UTF-8?q?extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts the polymorphic `implicit class macroDslExtensions[T](value: D[T])` block inside `AbstractMongoPolyDataCompanion` to `extension [T](value: D[T])`. Same mechanical transform as MongoEntityCompanion's sibling; receiver is `D[T]` where `D[_]` is a kind parameter of the enclosing class — kind-parameter declaration preserved as-is per Pitfall 3. Translated from origin/master@eef0edce. --- .../avsystem/commons/mongo/typed/MongoPolyDataCompanion.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoPolyDataCompanion.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoPolyDataCompanion.scala index 594ec7835..1a42a84b1 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoPolyDataCompanion.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoPolyDataCompanion.scala @@ -33,7 +33,7 @@ abstract class AbstractMongoPolyDataCompanion[Implicits, D[_]]( // TODO[scala3-port]: D[_] → D[Any] workaround for Scala 3 wildcard-as-type-arg restriction (S) implicit def isMongoAdtOrSubtype[C <: D[Any]]: IsMongoAdtOrSubtype[C] = null - implicit class macroDslExtensions[T](value: D[T]) { + extension [T](value: D[T]) { @explicitGenerics @compileTimeOnly("the .as[Subtype] construct can only be used inside lambda passed to .ref(...) macro") def as[C <: D[T]]: C = sys.error("stub") From 88de8c774ea8b3ce1e01f27e6f6b8b9d41fde70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:20:03 +0200 Subject: [PATCH 06/25] =?UTF-8?q?refactor(scala-3,mongo):=20MongoFormat=20?= =?UTF-8?q?assume*=20implicit=20class=20=E2=86=92=20extension=20(3=20ops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three companion `implicit class … extends AnyVal` blocks (`collectionFormatOps`, `dictionaryFormatOps`, `typedMapFormatOps`) converted to plain `extension`. Each exposes a single `assume*` cast helper; no named-argument inference issue, plain `extension` shape suffices. Matches fork eef0edce shape. Translated from origin/master@eef0edce. --- .../com/avsystem/commons/mongo/typed/MongoFormat.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoFormat.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoFormat.scala index 97b9c499f..f6c45e33f 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoFormat.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoFormat.scala @@ -123,7 +123,7 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri wrappedFormat: MongoFormat[R], ): MongoFormat[T] = TransparentFormat(codec, wrapping, wrappedFormat) - implicit class collectionFormatOps[C[X] <: Iterable[X], T](private val format: MongoFormat[C[T]]) extends AnyVal { + extension [C[X] <: Iterable[X], T](format: MongoFormat[C[T]]) { def assumeCollection: CollectionFormat[C, T] = format match { case coll: CollectionFormat[C @unchecked, T @unchecked] => coll case _ => @@ -134,8 +134,7 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri } } - implicit class dictionaryFormatOps[M[X, Y] <: BMap[X, Y], K, V](private val format: MongoFormat[M[K, V]]) - extends AnyVal { + extension [M[X, Y] <: BMap[X, Y], K, V](format: MongoFormat[M[K, V]]) { def assumeDictionary: DictionaryFormat[M, K, V] = format match { case dict: DictionaryFormat[M @unchecked, K @unchecked, V @unchecked] => dict case _ => @@ -146,7 +145,7 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri } } - implicit class typedMapFormatOps[K[_]](private val format: MongoFormat[TypedMap[K]]) extends AnyVal { + extension [K[_]](format: MongoFormat[TypedMap[K]]) { def assumeTypedMap: TypedMapFormat[K] = format match { case typedMap: TypedMapFormat[K] => typedMap case _ => From c7c423c5495d28210405e1fe3388d0f1ccc9c0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:20:09 +0200 Subject: [PATCH 07/25] =?UTF-8?q?refactor(scala-3,mongo):=20MongoPropertyR?= =?UTF-8?q?ef=20CollectionRefOps/DictionaryRefOps/TypedMapRefOps=20implici?= =?UTF-8?q?t=20class=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three companion `implicit class … extends AnyVal` blocks on `MongoPropertyRef`-typed receivers converted to plain `extension`: - `CollectionRefOps` → `extension [E, C[X] <: Iterable[X], T]` - `DictionaryRefOps` → `extension [E, M[X, Y] <: BMap[X, Y], K, V]` - `TypedMapRefOps` → `extension [E, K[_]]` `TypedMapRefOps.apply[T](key: K[T])` and `DictionaryRefOps.apply(key: K)` both erase to `apply(Object)`, so `@scala.annotation.targetName("typedMapApply")` was added to the typed-map variant (Rule 1 - Bug auto-fix; required since `extension` methods share the companion's namespace where the value-class wrapper types previously kept them separate). Translated from origin/master@eef0edce. --- .../com/avsystem/commons/mongo/typed/MongoRef.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala index 59926bf5d..a4fb64a84 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala @@ -250,23 +250,22 @@ sealed trait MongoPropertyRef[E, T] object MongoPropertyRef { final val Separator = "." - implicit class CollectionRefOps[E, C[X] <: Iterable[X], T](private val ref: MongoPropertyRef[E, C[T]]) - extends AnyVal { + extension [E, C[X] <: Iterable[X], T](ref: MongoPropertyRef[E, C[T]]) { def head: MongoPropertyRef[E, T] = apply(0) def apply(index: Int): MongoPropertyRef[E, T] = MongoRef.ArrayIndexRef(ref, index, ref.format.assumeCollection.elementFormat) } - implicit class DictionaryRefOps[E, M[X, Y] <: BMap[X, Y], K, V](private val ref: MongoPropertyRef[E, M[K, V]]) - extends AnyVal { + extension [E, M[X, Y] <: BMap[X, Y], K, V](ref: MongoPropertyRef[E, M[K, V]]) { def apply(key: K): MongoPropertyRef[E, V] = { val dictFormat = ref.format.assumeDictionary MongoRef.FieldRef(ref, dictFormat.keyCodec.write(key), dictFormat.valueFormat, Opt.Empty) } } - implicit class TypedMapRefOps[E, K[_]](private val ref: MongoPropertyRef[E, TypedMap[K]]) extends AnyVal { + extension [E, K[_]](ref: MongoPropertyRef[E, TypedMap[K]]) { + @scala.annotation.targetName("typedMapApply") def apply[T](key: K[T]): MongoPropertyRef[E, T] = { val tmFormat = ref.format.assumeTypedMap // TODO[scala3-port]: cast follows K[_] → K[Any] workaround in TypedMapFormat (S) From c3a3fc1bc7bd02bfba3db76355a0b0e664c53587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:22:37 +0200 Subject: [PATCH 08/25] =?UTF-8?q?docs(migration):=20record=20implicit=20cl?= =?UTF-8?q?ass=20=E2=86=92=20extension=20source-compat=20impact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds §3 entries documenting Scala 3 `extension` migrations landed in slice 3.1: - core/GenCodec.scala (4 private internal wrappers — transparent) - mongo/typed/{MongoEntityCompanion, MongoPolyDataCompanion}.macroDslExtensions - mongo/typed/MongoFormat.{collection,dictionary,typedMap}FormatOps (3 assume* helpers) - mongo/typed/MongoPropertyRef.{Collection,Dictionary,TypedMap}RefOps (+ @targetName) - mongo/typed/QueryOperatorsDsl (Vanilla + non-Vanilla ForCollection — plain extension) - mongo/typed/UpdateOperatorsDsl.ForCollection — given Conversion (HKT receiver Pitfall 7) Each entry notes call-site impact (mostly transparent for extension-method resolution; named-type reference to the old wrapper classes breaks). --- MIGRATION.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index b99395701..a7084e589 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -54,6 +54,9 @@ 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). ### mongo @@ -63,6 +66,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 From df194afb9d30c617a0e2457c0758f338ac12844c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:29:47 +0200 Subject: [PATCH 09/25] style(scala-3,mongo): import targetName, drop fully-qualified annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @scala.annotation.targetName → @targetName via scala.annotation.{tailrec, targetName} import. Co-Authored-By: Claude Opus 4.7 --- .../scala/com/avsystem/commons/mongo/typed/MongoRef.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala index a4fb64a84..f377c6723 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala @@ -9,7 +9,7 @@ import com.avsystem.commons.mongo.{BsonValueInput, KeyEscaper} import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.TransparentWrapping import org.bson.{BsonDocument, BsonValue} -import scala.annotation.tailrec +import scala.annotation.{tailrec, targetName} /** Represents a reference to a particular "place" in a MongoDB document. The "place" may be an actual path inside the * document ([[MongoPropertyRef]]) or the whole document _itself_ (you can think of it as an empty path). @@ -265,7 +265,7 @@ object MongoPropertyRef { } extension [E, K[_]](ref: MongoPropertyRef[E, TypedMap[K]]) { - @scala.annotation.targetName("typedMapApply") + @targetName("typedMapApply") def apply[T](key: K[T]): MongoPropertyRef[E, T] = { val tmFormat = ref.format.assumeTypedMap // TODO[scala3-port]: cast follows K[_] → K[Any] workaround in TypedMapFormat (S) From 820ff26c78eedca1183c47d590771d6c5ca2540e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 18:30:38 +0200 Subject: [PATCH 10/25] =?UTF-8?q?docs(scala-3,mongo):=20TODO=20note=20for?= =?UTF-8?q?=20ForCollection=20conversion=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tag the given Conversion workaround with a TODO[scala3-port] marker so the follow-up to convert ForCollection into a proper extension block is tracked once the HKT receiver-inference issue is addressed. Co-Authored-By: Claude Opus 4.7 --- .../avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala index e98454efa..96841d58d 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala @@ -27,9 +27,11 @@ trait UpdateOperatorsDsl[T, R] { object UpdateOperatorsDsl { import MongoUpdateOperator._ - // A `given Conversion` (not an `extension`) is used here so the higher-kinded `C[T]` is unified once, - // at conversion time, against the receiver's `UpdateOperatorsDsl[C[T], R]` base type. Plain extension - // methods fail to infer `C`/`T` from the receiver for named-argument calls such as `push(sort = ...)`. + // TODO[scala3-port]: convert ForCollection to an `extension` block once the HKT receiver-inference + // regression is fixed (dotty#XXXXX). A `given Conversion` (not an `extension`) is used here so + // the higher-kinded `C[T]` is unified once, at conversion time, against the receiver's + // `UpdateOperatorsDsl[C[T], R]` base type. Plain extension methods fail to infer `C`/`T` from the + // receiver for named-argument calls such as `push(sort = ...)`. given [C[X] <: Iterable[X], T, R] => Conversion[UpdateOperatorsDsl[C[T], R], ForCollection[C, T, R]] = ForCollection(_) From f8ca7e6d1fc9777d26a01e4d24483d9eebe3680c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:04:40 +0200 Subject: [PATCH 11/25] =?UTF-8?q?refactor(scala-3,mongo):=20ReactiveMongoE?= =?UTF-8?q?xtensions=20implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern (`implicit def publisherOps + final class PublisherOps extends AnyVal`) in `ReactiveMongoExtensions` to a single `extension [T](publisher: Publisher[T])` block. Translated from origin/master:mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala. --- .../commons/mongo/reactive/ReactiveMongoExtensions.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala index a807d5b17..10a558296 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/reactive/ReactiveMongoExtensions.scala @@ -6,15 +6,10 @@ import monix.reactive.Observable import org.reactivestreams.Publisher trait ReactiveMongoExtensions { - import ReactiveMongoExtensions._ - - implicit final def publisherOps[T](publisher: Publisher[T]): PublisherOps[T] = new PublisherOps(publisher) -} -object ReactiveMongoExtensions extends ReactiveMongoExtensions { /** Extensions for converting [[Publisher]] to [[Task]]/[[Observable]] Monix types */ - final class PublisherOps[T](private val publisher: Publisher[T]) extends AnyVal { + extension [T](publisher: Publisher[T]) { def asMonix: Observable[T] = Observable.fromReactivePublisher(publisher) // prefer using the family of methods below for observables which are intended to only return a single document, @@ -29,3 +24,4 @@ object ReactiveMongoExtensions extends ReactiveMongoExtensions { def completedL: Task[Unit] = headOptionL.void } } +object ReactiveMongoExtensions extends ReactiveMongoExtensions From 72e7bd5a76d3f1f599353ade97f39d1f4d88b7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:05:15 +0200 Subject: [PATCH 12/25] =?UTF-8?q?refactor(scala-3,mongo):=20MongoOps=20DBO?= =?UTF-8?q?ps/FindIterableOps=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern in `MongoOps` (sync) — `implicit def dbOps + final class DBOps extends AnyVal`, `implicit def findIterableOps + final class FindIterableOps extends AnyVal` — to two `extension` blocks on `MongoDatabase` and `FindIterable[T]`. --- .../avsystem/commons/mongo/sync/MongoOps.scala | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala index 67e101429..4b90a6905 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala @@ -12,14 +12,7 @@ import org.bson.conversions.Bson */ trait MongoOps { - import MongoOps._ - - implicit def dbOps(db: MongoDatabase): DBOps = new DBOps(db) - implicit def findIterableOps[T](find: FindIterable[T]): FindIterableOps[T] = new FindIterableOps(find) -} - -object MongoOps { - final class DBOps(private val db: MongoDatabase) extends AnyVal { + extension (db: MongoDatabase) { def getCollection[A](name: String, codec: BsonCodec[A, BsonDocument])(implicit ct: ClassTag[A]) : MongoCollection[A] = { val mongoCodec = new MongoCodec[A, BsonDocument](codec, db.getCodecRegistry) @@ -31,7 +24,7 @@ object MongoOps { } } - final class FindIterableOps[T](private val find: FindIterable[T]) extends AnyVal { + extension [T](find: FindIterable[T]) { def firstOpt: Option[T] = Option(find.first) def page(sort: Bson, offset: Int, maxItems: Int): Vector[T] = { @@ -40,10 +33,10 @@ object MongoOps { .sort(sort) .skip(offset) .limit(maxItems) - .forEach(new JConsumer[T] { - def accept(t: T) = b += t - }) + .forEach(b += _) b.result() } } } + +object MongoOps extends MongoOps From 71c5ecd2819bd3cd055d497d45027d6c99f19d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:06:57 +0200 Subject: [PATCH 13/25] =?UTF-8?q?refactor(scala-3,core):=20JavaTimeInterop?= =?UTF-8?q?=20InstantOps=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern (`implicit def instantOps + class InstantOps extends AnyVal`) to a single `extension (instant: Instant)` block. --- .../scala/com/avsystem/commons/jiop/JavaTimeInterop.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JavaTimeInterop.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JavaTimeInterop.scala index c38a56c58..bb92dcb73 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JavaTimeInterop.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JavaTimeInterop.scala @@ -3,15 +3,12 @@ package jiop import java.time.Instant -import com.avsystem.commons.jiop.JavaTimeInterop.InstantOps import com.avsystem.commons.misc.Timestamp trait JavaTimeInterop { - implicit def instantOps(instant: Instant): InstantOps = new InstantOps(instant) -} -object JavaTimeInterop { - class InstantOps(private val instant: Instant) extends AnyVal { + extension (instant: Instant) { def truncateToTimestamp: Timestamp = Timestamp(instant.toEpochMilli) def truncateToJDate: JDate = new JDate(instant.toEpochMilli) } } +object JavaTimeInterop extends JavaTimeInterop From fe9645587170e96cbd52a732aeb4bcb19a135bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:06:57 +0200 Subject: [PATCH 14/25] =?UTF-8?q?refactor(scala-3,core):=20Java8Collection?= =?UTF-8?q?Utils=20implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps 7 de-facto-implicit-class pairs (jIteratorOps, jIterableOps, jCollectionOps, intJCollectionOps, longJCollectionOps, doubleJCollectionOps, jMapOps) to `extension` blocks. --- .../commons/jiop/Java8CollectionUtils.scala | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/Java8CollectionUtils.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/Java8CollectionUtils.scala index 6320b2d41..7f69eef9b 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/Java8CollectionUtils.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/Java8CollectionUtils.scala @@ -3,29 +3,17 @@ package jiop trait Java8CollectionUtils { - import Java8CollectionUtils._ - - implicit def jIteratorOps[A](it: JIterator[A]): jIteratorOps[A] = new jIteratorOps(it) - implicit def jIterableOps[A](it: JIterable[A]): jIterableOps[A] = new jIterableOps(it) - implicit def jCollectionOps[A](it: JCollection[A]): jCollectionOps[A] = new jCollectionOps(it) - implicit def intJCollectionOps(it: JCollection[Int]): intJCollectionOps = new intJCollectionOps(it) - implicit def longJCollectionOps(it: JCollection[Long]): longJCollectionOps = new longJCollectionOps(it) - implicit def doubleJCollectionOps(it: JCollection[Double]): doubleJCollectionOps = new doubleJCollectionOps(it) - implicit def jMapOps[K, V](map: JMap[K, V]): jMapOps[K, V] = new jMapOps(map) -} - -object Java8CollectionUtils { - class jIteratorOps[A](private val it: JIterator[A]) extends AnyVal { + extension [A](it: JIterator[A]) { def forEachRemaining(code: A => Any): Unit = it.forEachRemaining(jConsumer(code)) } - class jIterableOps[A](private val it: JIterable[A]) extends AnyVal { + extension [A](it: JIterable[A]) { def forEach(code: A => Any): Unit = it.forEach(jConsumer(code)) } - class jCollectionOps[A](private val coll: JCollection[A]) extends AnyVal { + extension [A](coll: JCollection[A]) { def removeIf(pred: A => Boolean): Unit = coll.removeIf(jPredicate(pred)) @@ -33,22 +21,22 @@ object Java8CollectionUtils { coll.stream.asScala } - class intJCollectionOps(private val coll: JCollection[Int]) extends AnyVal { + extension (coll: JCollection[Int]) { def scalaIntStream: ScalaJIntStream = coll.stream.asScalaIntStream } - class longJCollectionOps(private val coll: JCollection[Long]) extends AnyVal { + extension (coll: JCollection[Long]) { def scalaLongStream: ScalaJLongStream = coll.stream.asScalaLongStream } - class doubleJCollectionOps(private val coll: JCollection[Double]) extends AnyVal { + extension (coll: JCollection[Double]) { def scalaDoubleStream: ScalaJDoubleStream = coll.stream.asScalaDoubleStream } - class jMapOps[K, V](private val map: JMap[K, V]) extends AnyVal { + extension [K, V](map: JMap[K, V]) { def compute(key: K, remappingFunction: (K, V) => V): V = map.compute(key, jBiFunction(remappingFunction)) @@ -68,3 +56,5 @@ object Java8CollectionUtils { map.replaceAll(jBiFunction(function)) } } + +object Java8CollectionUtils extends Java8CollectionUtils From 66ada4ef173fd38f061b079f7718265cd56fcec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:06:58 +0200 Subject: [PATCH 15/25] =?UTF-8?q?refactor(scala-3,core):=20GuavaInterop=20?= =?UTF-8?q?implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps 3 de-facto-implicit-class pairs (DecorateFutureAsScala, DecorateSettableFutureAsScala, DecorateFutureAsGuava) to `extension` blocks. Inner self-reference `asScala.toUnit` → `gfut.asScala.toUnit` because Scala 3 extension bodies do not implicitly resolve own siblings without naming the receiver. --- .../avsystem/commons/jiop/GuavaInterop.scala | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/GuavaInterop.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/GuavaInterop.scala index 655078b61..ef96473a1 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/GuavaInterop.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/GuavaInterop.scala @@ -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) @@ -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 From 16666fbcc93269e232d695b5fe3621c9347f0a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:18:44 +0200 Subject: [PATCH 16/25] =?UTF-8?q?refactor(scala-3,core):=20JOptionalUtils?= =?UTF-8?q?=20implicit=20def=20+=20AnyVal=20=E2=86=92=20extension=20(Optio?= =?UTF-8?q?nLike=20unified)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern in JOptionalUtils — 11 pairs (optional2AsScala, optionalDouble2AsScala, optionalInt2AsScala, optionalLong2AsScala, option2AsJava, opt2AsJava, nopt2AsJava, optArg2AsJava, option2AsJavaDouble, option2AsJavaInt, option2AsJavaLong) — to Scala 3 `extension` blocks. Per fork shape (origin/master scala-3 overlay), the 4 `O[T] → JOptional[T]` wrappers (Option, Opt, NOpt, OptArg) consolidate into ONE generic extension parameterized by `OptionLike.Aux[O[T], T]`. This avoids the erasure clash between value-class wrappers (Opt, NOpt, OptArg all erase to Object) AND avoids the source-level clash where a per-receiver `extension (option: Option[T]) { def asJava }` was being eagerly picked over `scala.collection.convert.AsJavaExtensions.asJava` for `Seq`/`Iterable` receivers. Added `import JavaInterop._` in JavaInteropTest.scala because Scala 3 extension resolution prefers imported extensions (GuavaInterop.asScala for ListenableFuture, already imported in test) over package-object-mixed-in extensions (JOptionalUtils.asScala for JOptional, available via the package object). Without the explicit JavaInterop import, `JOptional(x).asScala` resolved against GuavaInterop.asScala first and short-circuited before trying JOptionalUtils.asScala. Mirrors fork test's import shape. --- .../commons/jiop/JOptionalUtils.scala | 162 +++++------------- .../commons/jiop/JavaInteropTest.scala | 1 + 2 files changed, 46 insertions(+), 117 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala index 3af2b200c..8f197d03b 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala @@ -1,187 +1,115 @@ 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 diff --git a/core/jvm/src/test/scala/com/avsystem/commons/jiop/JavaInteropTest.scala b/core/jvm/src/test/scala/com/avsystem/commons/jiop/JavaInteropTest.scala index 08f2d9f6f..c3bb24799 100644 --- a/core/jvm/src/test/scala/com/avsystem/commons/jiop/JavaInteropTest.scala +++ b/core/jvm/src/test/scala/com/avsystem/commons/jiop/JavaInteropTest.scala @@ -2,6 +2,7 @@ package com.avsystem.commons package jiop import com.avsystem.commons.jiop.GuavaInterop._ +import com.avsystem.commons.jiop.JavaInterop._ import com.google.common.util.concurrent.{MoreExecutors, SettableFuture} import org.scalatest.funsuite.AnyFunSuite From 7ce1d00f39db88eb5aa701ed4c17a25ad8bb3377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:20:19 +0200 Subject: [PATCH 17/25] =?UTF-8?q?refactor(scala-3,core):=20JStreamUtils=20?= =?UTF-8?q?implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps 7 de-facto-implicit-class pairs (JStream2AsScala, JStream2AsScalaIntStream, JStream2AsScalaLongStream, JStream2AsScalaDoubleStream, JDoubleStream2AsScala, JIntStream2AsScala, JLongStream2AsScala) to `extension` blocks. --- .../avsystem/commons/jiop/JStreamUtils.scala | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JStreamUtils.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JStreamUtils.scala index 542ce2fd6..0bc1a03b1 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JStreamUtils.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JStreamUtils.scala @@ -11,58 +11,33 @@ trait JStreamUtils { type JLongStream = ju.stream.LongStream type JCollector[T, A, R] = ju.stream.Collector[T, A, R] - import JStreamUtils._ - - implicit def jStream2AsScala[T](jStream: JStream[T]): JStream2AsScala[T] = - new JStream2AsScala(jStream) - - implicit def jStream2AsScalaIntStream(jStream: JStream[Int]): JStream2AsScalaIntStream = - new JStream2AsScalaIntStream(jStream) - - implicit def jStream2AsScalaLongStream(jStream: JStream[Long]): JStream2AsScalaLongStream = - new JStream2AsScalaLongStream(jStream) - - implicit def jStream2AsScalaDoubleStream(jStream: JStream[Double]): JStream2AsScalaDoubleStream = - new JStream2AsScalaDoubleStream(jStream) - - implicit def jDoubleStream2AsScala(jStream: JDoubleStream): JDoubleStream2AsScala = - new JDoubleStream2AsScala(jStream) - - implicit def jIntStream2AsScala(jStream: JIntStream): JIntStream2AsScala = - new JIntStream2AsScala(jStream) - - implicit def jLongStream2AsScala(jStream: JLongStream): JLongStream2AsScala = - new JLongStream2AsScala(jStream) -} - -object JStreamUtils { - - final class JStream2AsScala[T](private val jStream: JStream[T]) extends AnyVal { + extension [T](jStream: JStream[T]) { def asScala: ScalaJStream[T] = new ScalaJStream(jStream) } - final class JStream2AsScalaIntStream(private val jStream: JStream[Int]) extends AnyVal { + extension (jStream: JStream[Int]) { def asScalaIntStream: ScalaJIntStream = jStream.asScala.asIntStream } - final class JStream2AsScalaLongStream(private val jStream: JStream[Long]) extends AnyVal { + extension (jStream: JStream[Long]) { def asScalaLongStream: ScalaJLongStream = jStream.asScala.asLongStream } - final class JStream2AsScalaDoubleStream(private val jStream: JStream[Double]) extends AnyVal { + extension (jStream: JStream[Double]) { def asScalaDoubleStream: ScalaJDoubleStream = jStream.asScala.asDoubleStream } - final class JDoubleStream2AsScala(private val jStream: JDoubleStream) extends AnyVal { + extension (jStream: JDoubleStream) { def asScala: ScalaJDoubleStream = new ScalaJDoubleStream(jStream) } - final class JIntStream2AsScala(private val jStream: JIntStream) extends AnyVal { + extension (jStream: JIntStream) { def asScala: ScalaJIntStream = new ScalaJIntStream(jStream) } - final class JLongStream2AsScala(private val jStream: JLongStream) extends AnyVal { + extension (jStream: JLongStream) { def asScala: ScalaJLongStream = new ScalaJLongStream(jStream) } - } + +object JStreamUtils extends JStreamUtils From d40eaeb4b87b05d548273ac780575d57a19a9c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:20:19 +0200 Subject: [PATCH 18/25] =?UTF-8?q?refactor(scala-3,core):=20TaskExtensions?= =?UTF-8?q?=20TaskOps/TaskCompanionOps=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern in TaskExtensions to `extension` blocks: `TaskOps` (local AnyVal) becomes `extension [T](task: Task[T])`; `TaskCompanionOps` (singleton object) becomes `extension (task: Task.type)`. Hoisted `import ObservableExtensions.observableOps` above the trait (Pitfall 6: extension body cannot contain imports). Internal cross-references rewritten (`traverseMap`, `currentTimestamp`) to use the receiver name `task`. --- .../commons/concurrent/TaskExtensions.scala | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/concurrent/TaskExtensions.scala b/core/src/main/scala/com/avsystem/commons/concurrent/TaskExtensions.scala index 7e32cd105..3aaa1d004 100644 --- a/core/src/main/scala/com/avsystem/commons/concurrent/TaskExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/concurrent/TaskExtensions.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package concurrent -import com.avsystem.commons.concurrent.TaskExtensions.{TaskCompanionOps, TaskOps} +import com.avsystem.commons.concurrent.ObservableExtensions.observableOps import com.avsystem.commons.misc.Timestamp import monix.eval.Task import monix.reactive.Observable @@ -11,13 +11,8 @@ import scala.concurrent.TimeoutException import scala.concurrent.duration.FiniteDuration trait TaskExtensions { - implicit def taskOps[T](task: Task[T]): TaskOps[T] = new TaskOps(task) - implicit def taskCompanionOps(task: Task.type): TaskCompanionOps.type = TaskCompanionOps -} - -object TaskExtensions extends TaskExtensions { - final class TaskOps[T](private val task: Task[T]) extends AnyVal { + extension [T](task: Task[T]) { /** Similar to [[Task.timeoutWith]] but exception instance is created lazily (for performance) */ @@ -35,8 +30,7 @@ object TaskExtensions extends TaskExtensions { task.tapError(t => Task(f.applyOpt(t))) } - object TaskCompanionOps { - import com.avsystem.commons.concurrent.ObservableExtensions.observableOps + extension (task: Task.type) { /** A [[Task]] of [[Opt.Empty]] */ def optEmpty[A]: Task[Opt[A]] = Task.pure(Opt.Empty) @@ -45,7 +39,7 @@ object TaskExtensions extends TaskExtensions { opt.fold(Task.optEmpty[B])(a => f(a).map(_.opt)) def fromOpt[A](maybeTask: Opt[Task[A]]): Task[Opt[A]] = maybeTask match { - case Opt(task) => task.map(_.opt) + case Opt(t) => t.map(_.opt) case Opt.Empty => Task.optEmpty } @@ -53,12 +47,14 @@ object TaskExtensions extends TaskExtensions { Observable.fromIterable(map).mapEval { case (key, value) => f(key, value) }.toL(Map) def traverseMapValues[K, A, B](map: Map[K, A])(f: (K, A) => Task[B]): Task[Map[K, B]] = - traverseMap(map) { case (key, value) => f(key, value).map(key -> _) } + task.traverseMap(map) { case (key, value) => f(key, value).map(key -> _) } def currentTimestamp: Task[Timestamp] = Task.clock.realTime(TimeUnit.MILLISECONDS).map(Timestamp(_)) def usingNow[T](useNow: Timestamp => Task[T]): Task[T] = - currentTimestamp.flatMap(useNow) + task.currentTimestamp.flatMap(useNow) } } + +object TaskExtensions extends TaskExtensions From a4f30ed26770ca8b99f1b9e3eb111a2c5e62960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:21:10 +0200 Subject: [PATCH 19/25] =?UTF-8?q?refactor(scala-3,core):=20JCollectionUtil?= =?UTF-8?q?s=20pairIterableOps=20implicit=20def=20+=20AnyVal=20=E2=86=92?= =?UTF-8?q?=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/avsystem/commons/jiop/JCollectionUtils.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/jiop/JCollectionUtils.scala b/core/src/main/scala/com/avsystem/commons/jiop/JCollectionUtils.scala index 381b5e167..c928a962c 100644 --- a/core/src/main/scala/com/avsystem/commons/jiop/JCollectionUtils.scala +++ b/core/src/main/scala/com/avsystem/commons/jiop/JCollectionUtils.scala @@ -213,13 +213,7 @@ trait JCollectionUtils extends JFactories { } } - import JCollectionUtils._ - - implicit def pairIterableOps[A, B](coll: IterableOnce[(A, B)]): pairIterableOps[A, B] = new pairIterableOps(coll) -} - -object JCollectionUtils { - class pairIterableOps[A, B](private val coll: IterableOnce[(A, B)]) extends AnyVal { + extension [A, B](coll: IterableOnce[(A, B)]) { def toJMap[M[K, V] <: JMap[K, V]](implicit fac: Factory[(A, B), M[A, B]]): M[A, B] = { val b = fac.newBuilder coll.iterator.foreach(b += _) From a30ff9ef42c89ae214d4b8e9a2b03e3aa7e07a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:21:59 +0200 Subject: [PATCH 20/25] =?UTF-8?q?refactor(scala-3,core):=20Opt.LazyOptOps?= =?UTF-8?q?=20implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern in `Opt.LazyOptOps` to `extension [A](opt: => Opt[A])`. The by-name receiver replaces the thunked `() => Opt[A]` value-class workaround; method bodies reference `opt` directly (Scala 3 by-name extension receiver is evaluated per access). --- core/src/main/scala/com/avsystem/commons/misc/Opt.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/misc/Opt.scala b/core/src/main/scala/com/avsystem/commons/misc/Opt.scala index c2ebc9129..f1d5831b8 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/Opt.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/Opt.scala @@ -29,20 +29,18 @@ object Opt { def withFilter(q: A => Boolean): WithFilter[A] = new WithFilter[A](self, x => p(x) && q(x)) } - final class LazyOptOps[A](private val opt: () => Opt[A]) extends AnyVal { + extension [A](opt: => Opt[A]) { /** When a given condition is true, evaluates the `opt` argument and returns it. When the condition is false, `opt` * is not evaluated and [[Opt.Empty]] is returned. */ - def when(cond: Boolean): Opt[A] = if (cond) opt() else Opt.Empty + def when(cond: Boolean): Opt[A] = if (cond) opt else Opt.Empty /** Unless a given condition is true, this will evaluate the `opt` argument and return it. Otherwise, `opt` is not * evaluated and [[Opt.Empty]] is returned. */ @inline def unless(cond: Boolean): Opt[A] = when(!cond) } - - implicit def lazyOptOps[A](opt: => Opt[A]): LazyOptOps[A] = new LazyOptOps(() => opt) } /** Like [[Option]] but implemented as value class (avoids boxing) and treats `null` as no value. Therefore, there is no From 06aed30a2fa2b1e372ab280c3d14637a8f0ad6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:31:16 +0200 Subject: [PATCH 21/25] =?UTF-8?q?refactor(scala-3,core):=20SharedExtension?= =?UTF-8?q?s=20implicit=20def=20+=20AnyVal=20=E2=86=92=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps 18 de-facto-implicit-class pairs in SharedExtensions (UniversalOps, LazyUniversalOps, NullableOps, StringOps, IntOps, FutureOps, LazyFutureOps, FutureCompanionOps, OptionOps, TryOps, LazyTryOps, TryCompanionOps, PartialFunctionOps, SetOps, IterableOnceOps, IterableOps, PairIterableOnceOps, MapOps, IteratorOps, IteratorCompanionOps) to Scala 3 `extension` blocks. Structural notes vs prior single-source layout: - `SharedExtensionsUtils` companion object eliminated (no value-class wrappers needed). - Helper singletons promoted to `object SharedExtensions` companion: `FutureCompanionOps`, `TryCompanionOps`, `IteratorCompanionOps` (real impls), `PartialFunctionOps.{NoValueMarker, NoValueMarkerFunc}` (the marker carrier), `MapOps.Entry`. - `extension (companion: Future.type) { def eval, def traverseCompleted, def sequenceCompleted }` etc. delegate to the matching helper-object methods. - Method bodies preserved verbatim (no `inline`/`using` adoption — that is slice 3.4/3.3 territory). - Dropped orphan `OrderingOps` per existing MiMa exclusion `SharedExtensionsUtils.orderingOps` (was already unreachable via `implicit def`). - Dropped dead `IteratorOps.distinct/distinctBy` per existing MiMa exclusion (stdlib-covered, removed in fork commit ade8d4a8). `SharedExtensions.MapOps.Entry` and `SharedExtensions.PartialFunctionOps.{NoValueMarker, NoValueMarkerFunc}` import paths preserved for external callers. --- .../avsystem/commons/SharedExtensions.scala | 369 +++++++++--------- 1 file changed, 174 insertions(+), 195 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index 70c670615..8ba3826cd 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -1,5 +1,8 @@ package com.avsystem.commons +import com.avsystem.commons.SharedExtensions.MapOps.Entry +import com.avsystem.commons.SharedExtensions.PartialFunctionOps.{NoValueMarker, NoValueMarkerFunc} +import com.avsystem.commons.SharedExtensions.{FutureCompanionOps, IteratorCompanionOps, TryCompanionOps} import com.avsystem.commons.concurrent.RunNowEC import com.avsystem.commons.misc.* @@ -8,56 +11,7 @@ import scala.collection.{mutable, AbstractIterator, BuildFrom, Factory} trait SharedExtensions { - import com.avsystem.commons.SharedExtensionsUtils.* - - implicit def universalOps[A](a: A): UniversalOps[A] = new UniversalOps(a) - - implicit def lazyUniversalOps[A](a: => A): LazyUniversalOps[A] = new LazyUniversalOps(() => a) - - implicit def nullableOps[A >: Null](a: A): NullableOps[A] = new NullableOps(a) - - implicit def stringOps(str: String): StringOps = new StringOps(str) - - implicit def intOps(int: Int): IntOps = new IntOps(int) - - implicit def futureOps[A](fut: Future[A]): FutureOps[A] = new FutureOps(fut) - - implicit def lazyFutureOps[A](fut: => Future[A]): LazyFutureOps[A] = new LazyFutureOps(() => fut) - - implicit def futureCompanionOps(fut: Future.type): FutureCompanionOps.type = FutureCompanionOps - - implicit def optionOps[A](option: Option[A]): OptionOps[A] = new OptionOps(option) - - implicit def tryOps[A](tr: Try[A]): TryOps[A] = new TryOps(tr) - - implicit def lazyTryOps[A](tr: => Try[A]): LazyTryOps[A] = new LazyTryOps(() => tr) - - implicit def tryCompanionOps(trc: Try.type): TryCompanionOps.type = TryCompanionOps - - implicit def partialFunctionOps[A, B](pf: PartialFunction[A, B]): PartialFunctionOps[A, B] = - new PartialFunctionOps(pf) - - implicit def setOps[A](set: BSet[A]): SetOps[A] = new SetOps(set) - - implicit def iterableOnceOps[C[X] <: IterableOnce[X], A](coll: C[A]): IterableOnceOps[C, A] = - new IterableOnceOps(coll) - - implicit def iterableOps[C[X] <: BIterable[X], A](coll: C[A]): IterableOps[C, A] = new IterableOps(coll) - - implicit def pairIterableOnceOps[C[X] <: IterableOnce[X], K, V](coll: C[(K, V)]): PairIterableOnceOps[C, K, V] = - new PairIterableOnceOps(coll) - - implicit def mapOps[M[X, Y] <: BMap[X, Y], K, V](map: M[K, V]): MapOps[M, K, V] = new MapOps(map) - - implicit def iteratorOps[A](it: Iterator[A]): IteratorOps[A] = new IteratorOps(it) - - implicit def iteratorCompanionOps(it: Iterator.type): IteratorCompanionOps.type = IteratorCompanionOps - -} -object SharedExtensions extends SharedExtensions - -object SharedExtensionsUtils extends SharedExtensions { - class UniversalOps[A](private val a: A) extends AnyVal { + extension [A](a: A) { /** The "pipe" operator. Alternative syntax to apply a function on an argument. Useful for fluent expressions and * avoiding intermediate variables. @@ -86,7 +40,7 @@ object SharedExtensionsUtils extends SharedExtensions { * `Opt.Empty`. For example, calling `.unboxedOpt` on a `java.lang.Integer` will convert it to `Opt[Int]`. */ def unboxedOpt[B](implicit unboxing: Unboxing[B, A]): Opt[B] = - opt.map(unboxing.fun) + a.opt.map(unboxing.fun) def checkNotNull(msg: String): A = if (a != null) a else throw new NullPointerException(msg) @@ -150,37 +104,37 @@ object SharedExtensionsUtils extends SharedExtensions { def debugMacro: A = a } - class LazyUniversalOps[A](private val a: () => A) extends AnyVal { - def evalFuture: Future[A] = FutureCompanionOps.eval(a()) + extension [A](a: => A) { + def evalFuture: Future[A] = FutureCompanionOps.eval(a) - def evalTry: Try[A] = Try(a()) + def evalTry: Try[A] = Try(a) def optIf(condition: Boolean): Opt[A] = - if (condition) Opt(a()) else Opt.Empty + if (condition) Opt(a) else Opt.Empty def optionIf(condition: Boolean): Option[A] = - if (condition) Some(a()) else None + if (condition) Some(a) else None def recoverFrom[T <: Throwable: ClassTag](fallbackValue: => A): A = - try a() + try a catch { case _: T => fallbackValue } def recoverToOpt[T <: Throwable: ClassTag]: Opt[A] = - try Opt(a()) + try Opt(a) catch { case _: T => Opt.Empty } } - class NullableOps[A >: Null](private val a: A) extends AnyVal { + extension [A >: Null](a: A) { def optRef: OptRef[A] = OptRef(a) } private val RemovableLineBreak = "\\n+".r - class StringOps(private val str: String) extends AnyVal { + extension (str: String) { /** Makes sure that `String` value is not `null` by replacing `null` with empty string. */ @@ -232,7 +186,7 @@ object SharedExtensionsUtils extends SharedExtensions { } } - class IntOps(private val int: Int) extends AnyVal { + extension (int: Int) { def times(code: => Any): Unit = { var i = 0 while (i < int) { @@ -242,7 +196,7 @@ object SharedExtensionsUtils extends SharedExtensions { } } - class FutureOps[A](private val fut: Future[A]) extends AnyVal { + extension [A](fut: Future[A]) { def onCompleteNow[U](f: Try[A] => U): Unit = fut.onComplete(f)(RunNowEC) @@ -290,10 +244,10 @@ object SharedExtensionsUtils extends SharedExtensions { fut.zipWith(that)(f)(RunNowEC) def toUnit: Future[Unit] = - mapNow(_ => ()) + fut.mapNow(_ => ()) def toVoid: Future[Void] = - mapNow(_ => null: Void) + fut.mapNow(_ => null: Void) /** Returns a `Future` that completes with the specified `result`, but only after this future completes. */ @@ -306,10 +260,10 @@ object SharedExtensionsUtils extends SharedExtensions { /** Returns a `Future` that completes successfully, but only after this future completes. */ def ignoreFailures: Future[Unit] = - thenReturn(Future.successful {}) + fut.thenReturn(Future.successful {}) } - class LazyFutureOps[A](private val fut: () => Future[A]) extends AnyVal { + extension [A](fut: => Future[A]) { /** Evaluates a left-hand-side expression that returns a `Future` and ensures that all exceptions thrown by that * expression are converted to a failed `Future`. Also, if left-hand-side expression returns `null`, it's converted @@ -317,7 +271,7 @@ object SharedExtensionsUtils extends SharedExtensions { */ def catchFailures: Future[A] = { val result = - try fut() + try fut catch { case NonFatal(t) => Future.failed(t) } @@ -325,69 +279,34 @@ object SharedExtensionsUtils extends SharedExtensions { } } - object FutureCompanionOps { + extension (companion: Future.type) { /** Evaluates an expression and wraps its value into a `Future`. Failed `Future` is returned if expression * evaluation throws an exception. This is very similar to `Future.apply` but evaluates the argument immediately, * without dispatching it to some `ExecutionContext`. */ - def eval[T](expr: => T): Future[T] = - try Future.successful(expr) - catch { - case NonFatal(cause) => Future.failed(cause) - } + def eval[T](expr: => T): Future[T] = FutureCompanionOps.eval(expr) /** Different version of `Future.traverse`. Transforms a `IterableOnce[A]` into a `Future[IterableOnce[B]]`, which - * only completes after all `in` `Future`s are completed, using the provided function `A => Future[B]`. This is - * useful for performing a parallel map. For example, to apply a function to all items of a list - * - * @tparam A - * the type of the value inside the Futures in the `IterableOnce` - * @tparam B - * the type of the value of the returned `Future` - * @tparam M - * the type of the `IterableOnce` of Futures - * @param in - * the `IterableOnce` of Futures which will be sequenced - * @param fn - * the function to apply to the `IterableOnce` of Futures to produce the results - * @return - * the `Future` of the `IterableOnce` of results + * only completes after all `in` `Future`s are completed, using the provided function `A => Future[B]`. */ def traverseCompleted[A, B, M[X] <: IterableOnce[X]]( in: M[A] )( fn: A => Future[B] )(implicit bf: BuildFrom[M[A], B, M[B]] - ): Future[M[B]] = { - val (barrier, i) = in.iterator.foldLeft((Future.unit, Future.successful(bf.newBuilder(in)))) { - case ((priorFinished, fr), a) => - val transformed = fn(a) - (transformed.thenReturn(priorFinished), fr.zipWithNow(transformed)(_ += _)) - } - barrier.thenReturn(i.mapNow(_.result())) - } + ): Future[M[B]] = FutureCompanionOps.traverseCompleted(in)(fn) /** Different version of `Future.sequence`. Transforms a `IterableOnce[Future[A]]` into a `Future[IterableOnce[A]`, * which only completes after all `in` `Future`s are completed. - * - * @tparam A - * the type of the value inside the Futures - * @tparam M - * the type of the `IterableOnce` of Futures - * @param in - * the `IterableOnce` of Futures which will be sequenced - * @return - * the `Future` of the `IterableOnce` of results */ def sequenceCompleted[A, M[X] <: IterableOnce[X]]( in: M[Future[A]] )(implicit bf: BuildFrom[M[Future[A]], A, M[A]] - ): Future[M[A]] = - traverseCompleted(in)(identity) + ): Future[M[A]] = FutureCompanionOps.sequenceCompleted(in) } - class OptionOps[A](private val option: Option[A]) extends AnyVal { + extension [A](option: Option[A]) { /** Converts this `Option` into `Opt`. Because `Opt` cannot hold `null`, `Some(null)` is translated to `Opt.Empty`. */ @@ -429,7 +348,7 @@ object SharedExtensionsUtils extends SharedExtensions { option.fold(ifEmpty)(f) } - class TryOps[A](private val tr: Try[A]) extends AnyVal { + extension [A](tr: Try[A]) { /** Converts this `Try` into `Opt`. Because `Opt` cannot hold `null`, `Success(null)` is translated to `Opt.Empty`. */ @@ -473,60 +392,32 @@ object SharedExtensionsUtils extends SharedExtensions { } } - class LazyTryOps[A](private val tr: () => Try[A]) extends AnyVal { + extension [A](tr: => Try[A]) { /** Evaluates a left-hand side expression that return `Try`, catches all exceptions and converts them into a * `Failure`. */ - def catchFailures: Try[A] = try tr() + def catchFailures: Try[A] = try tr catch { case NonFatal(t) => Failure(t) } } - object TryCompanionOps { + extension (companion: Try.type) { /** Simple version of `TryOps.traverse`. Transforms a `IterableOnce[Try[A]]` into a `Try[IterableOnce[A]]`. Useful * for reducing many `Try`s into a single `Try`. */ def sequence[A, M[X] <: IterableOnce[X]](in: M[Try[A]])(implicit bf: BuildFrom[M[Try[A]], A, M[A]]): Try[M[A]] = - in.iterator - .foldLeft(Try(bf.newBuilder(in))) { - case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f - case (tr, tb) => - for { - r <- tr - a <- tb - } yield r += a - } - .map(_.result()) + TryCompanionOps.sequence(in) - /** Transforms a `IterableOnce[A]` into a `Try[IterableOnce[B]]` using the provided function `A => Try[B]`. For - * example, to apply a function to all items of a list: - * - * {{{ - * val myTryList = TryOps.traverse(myList)(x => Try(myFunc(x))) - * }}} + /** Transforms a `IterableOnce[A]` into a `Try[IterableOnce[B]]` using the provided function `A => Try[B]`. */ def traverse[A, B, M[X] <: IterableOnce[X]](in: M[A])(fn: A => Try[B])(implicit bf: BuildFrom[M[A], B, M[B]]) - : Try[M[B]] = - in.iterator - .map(fn) - .foldLeft(Try(bf.newBuilder(in))) { - case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f - case (tr, tb) => - for { - r <- tr - b <- tb - } yield r += b - } - .map(_.result()) - + : Try[M[B]] = TryCompanionOps.traverse(in)(fn) } - class PartialFunctionOps[A, B](private val pf: PartialFunction[A, B]) extends AnyVal { - - import PartialFunctionOps.* + extension [A, B](pf: PartialFunction[A, B]) { /** The same thing as `orElse` but with arguments flipped. Useful in situations where `orElse` would have to be * called on a partial function literal, which does not work well with type inference. @@ -548,12 +439,14 @@ object SharedExtensionsUtils extends SharedExtensions { case rawValue => forNonEmpty(rawValue.asInstanceOf[B]) } } - object PartialFunctionOps { - private object NoValueMarker - private final val NoValueMarkerFunc = (_: Any) => NoValueMarker + + extension [A](set: BSet[A]) { + def containsAny(other: BIterable[A]): Boolean = other.exists(set.contains) + + def containsAll(other: BIterable[A]): Boolean = other.forall(set.contains) } - class IterableOnceOps[C[X] <: IterableOnce[X], A](private val coll: C[A]) extends AnyVal { + extension [C[X] <: IterableOnce[X], A](coll: C[A]) { private def it: Iterator[A] = coll.iterator def toSized[To](fac: Factory[A, To], sizeHint: Int): To = { @@ -564,11 +457,11 @@ object SharedExtensionsUtils extends SharedExtensions { } def toMapBy[K](keyFun: A => K): Map[K, A] = - mkMap(keyFun, identity) + coll.mkMap(keyFun, identity) def mkMap[K, V](keyFun: A => K, valueFun: A => V): Map[K, V] = { val res = Map.newBuilder[K, V] - it.foreach { a => + coll.iterator.foreach { a => res += ((keyFun(a), valueFun(a))) } res.result() @@ -576,54 +469,79 @@ object SharedExtensionsUtils extends SharedExtensions { def groupToMap[K, V, To](keyFun: A => K, valueFun: A => V)(implicit bf: BuildFrom[C[A], V, To]): Map[K, To] = { val builders = mutable.Map[K, mutable.Builder[V, To]]() - it.foreach { a => + coll.iterator.foreach { a => builders.getOrElseUpdate(keyFun(a), bf.newBuilder(coll)) += valueFun(a) } builders.iterator.map { case (k, v) => (k, v.result()) }.toMap } - def findOpt(p: A => Boolean): Opt[A] = it.find(p).toOpt + def findOpt(p: A => Boolean): Opt[A] = coll.iterator.find(p).toOpt def flatCollect[B](f: PartialFunction[A, IterableOnce[B]])(implicit fac: Factory[B, C[B]]): C[B] = coll.iterator.collect(f).flatten.to(fac) - def collectFirstOpt[B](pf: PartialFunction[A, B]): Opt[B] = it.collectFirst(pf).toOpt + def collectFirstOpt[B](pf: PartialFunction[A, B]): Opt[B] = coll.iterator.collectFirst(pf).toOpt - def reduceOpt[A1 >: A](op: (A1, A1) => A1): Opt[A1] = if (it.isEmpty) Opt.Empty else it.reduce(op).opt + def reduceOpt[A1 >: A](op: (A1, A1) => A1): Opt[A1] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.reduce(op).opt + } - def reduceLeftOpt[B >: A](op: (B, A) => B): Opt[B] = if (it.isEmpty) Opt.Empty else it.reduceLeft(op).opt + def reduceLeftOpt[B >: A](op: (B, A) => B): Opt[B] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.reduceLeft(op).opt + } - def reduceRightOpt[B >: A](op: (A, B) => B): Opt[B] = if (it.isEmpty) Opt.Empty else it.reduceRight(op).opt + def reduceRightOpt[B >: A](op: (A, B) => B): Opt[B] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.reduceRight(op).opt + } - def maxOpt(implicit ord: Ordering[A]): Opt[A] = if (it.isEmpty) Opt.Empty else it.max.opt + def maxOpt(implicit ord: Ordering[A]): Opt[A] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.max.opt + } - def maxOptBy[B: Ordering](f: A => B): Opt[A] = if (it.isEmpty) Opt.Empty else it.maxBy(f).opt + def maxOptBy[B: Ordering](f: A => B): Opt[A] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.maxBy(f).opt + } - def minOpt(implicit ord: Ordering[A]): Opt[A] = if (it.isEmpty) Opt.Empty else it.min.opt + def minOpt(implicit ord: Ordering[A]): Opt[A] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.min.opt + } - def minOptBy[B: Ordering](f: A => B): Opt[A] = if (it.isEmpty) Opt.Empty else it.minBy(f).opt + def minOptBy[B: Ordering](f: A => B): Opt[A] = { + val it = coll.iterator + if (it.isEmpty) Opt.Empty else it.minBy(f).opt + } def indexOfOpt(elem: A): Opt[Int] = coll.iterator.indexOf(elem).opt.filter(_ != -1) def indexWhereOpt(p: A => Boolean): Opt[Int] = coll.iterator.indexWhere(p).opt.filter(_ != -1) - def mkStringOr(start: String, sep: String, end: String, default: String): String = + def mkStringOr(start: String, sep: String, end: String, default: String): String = { + val it = coll.iterator if (it.nonEmpty) it.mkString(start, sep, end) else default + } - def mkStringOr(sep: String, default: String): String = + def mkStringOr(sep: String, default: String): String = { + val it = coll.iterator if (it.nonEmpty) it.mkString(sep) else default + } def mkStringOrEmpty(start: String, sep: String, end: String): String = - mkStringOr(start, sep, end, "") + coll.mkStringOr(start, sep, end, "") def asyncFoldLeft[B](zero: Future[B])(fun: (B, A) => Future[B])(implicit ec: ExecutionContext): Future[B] = - it.foldLeft(zero)((fb, a) => fb.flatMap(b => fun(b, a))) + coll.iterator.foldLeft(zero)((fb, a) => fb.flatMap(b => fun(b, a))) def asyncFoldRight[B](zero: Future[B])(fun: (A, B) => Future[B])(implicit ec: ExecutionContext): Future[B] = - it.foldRight(zero)((a, fb) => fb.flatMap(b => fun(a, b))) + coll.iterator.foldRight(zero)((a, fb) => fb.flatMap(b => fun(a, b))) def asyncForeach(fun: A => Future[Unit])(implicit ec: ExecutionContext): Future[Unit] = - it.foldLeft[Future[Unit]](Future.unit)((fu, a) => fu.flatMap(_ => fun(a))) + coll.iterator.foldLeft[Future[Unit]](Future.unit)((fu, a) => fu.flatMap(_ => fun(a))) def partitionEither[L, R]( fun: A => Either[L, R] @@ -641,7 +559,7 @@ object SharedExtensionsUtils extends SharedExtensions { } } - class PairIterableOnceOps[C[X] <: IterableOnce[X], K, V](private val coll: C[(K, V)]) extends AnyVal { + extension [C[X] <: IterableOnce[X], K, V](coll: C[(K, V)]) { def intoMap[M[X, Y] <: BMap[X, Y]](implicit fac: Factory[(K, V), M[K, V]]): M[K, V] = { val builder = fac.newBuilder coll.iterator.foreach(builder += _) @@ -649,32 +567,21 @@ object SharedExtensionsUtils extends SharedExtensions { } } - class SetOps[A](private val set: BSet[A]) extends AnyVal { - def containsAny(other: BIterable[A]): Boolean = other.exists(set.contains) - - def containsAll(other: BIterable[A]): Boolean = other.forall(set.contains) - } - - class IterableOps[C[X] <: BIterable[X], A](private val coll: C[A]) extends AnyVal { + extension [C[X] <: BIterable[X], A](coll: C[A]) { def headOpt: Opt[A] = if (coll.isEmpty) Opt.Empty else Opt(coll.head) def lastOpt: Opt[A] = if (coll.isEmpty) Opt.Empty else Opt(coll.last) } - class MapOps[M[X, Y] <: BMap[X, Y], K, V](private val map: M[K, V]) extends AnyVal { - - import MapOps.* + extension [M[X, Y] <: BMap[X, Y], K, V](map: M[K, V]) { def getOpt(key: K): Opt[V] = map.get(key).toOpt /** For iterating, filtering, mapping etc without having to use tuples */ def entries: Iterator[Entry[K, V]] = map.iterator.map { case (k, v) => Entry(k, v) } } - object MapOps { - case class Entry[K, V](key: K, value: V) - } - class IteratorOps[A](private val it: Iterator[A]) extends AnyVal { + extension [A](it: Iterator[A]) { def pairs: Iterator[(A, A)] = new AbstractIterator[(A, A)] { private var first: NOpt[A] = NOpt.empty @@ -742,6 +649,92 @@ object SharedExtensionsUtils extends SharedExtensions { } + extension (companion: Iterator.type) { + def untilEmpty[T](elem: => Opt[T]): Iterator[T] = IteratorCompanionOps.untilEmpty(elem) + + def iterateUntilEmpty[T](start: Opt[T])(nextFun: T => Opt[T]): Iterator[T] = + IteratorCompanionOps.iterateUntilEmpty(start)(nextFun) + } + +} + +object SharedExtensions extends SharedExtensions { + + object FutureCompanionOps { + + /** Evaluates an expression and wraps its value into a `Future`. Failed `Future` is returned if expression + * evaluation throws an exception. This is very similar to `Future.apply` but evaluates the argument immediately, + * without dispatching it to some `ExecutionContext`. + */ + def eval[T](expr: => T): Future[T] = + try Future.successful(expr) + catch { + case NonFatal(cause) => Future.failed(cause) + } + + /** Different version of `Future.traverse`. Transforms a `IterableOnce[A]` into a `Future[IterableOnce[B]]`. */ + def traverseCompleted[A, B, M[X] <: IterableOnce[X]]( + in: M[A] + )( + fn: A => Future[B] + )(implicit bf: BuildFrom[M[A], B, M[B]] + ): Future[M[B]] = { + val (barrier, i) = in.iterator.foldLeft((Future.unit, Future.successful(bf.newBuilder(in)))) { + case ((priorFinished, fr), a) => + val transformed = fn(a) + (transformed.thenReturn(priorFinished), fr.zipWithNow(transformed)(_ += _)) + } + barrier.thenReturn(i.mapNow(_.result())) + } + + /** Different version of `Future.sequence`. */ + def sequenceCompleted[A, M[X] <: IterableOnce[X]]( + in: M[Future[A]] + )(implicit bf: BuildFrom[M[Future[A]], A, M[A]] + ): Future[M[A]] = + traverseCompleted(in)(identity) + } + + object TryCompanionOps { + + /** Simple version of `TryOps.traverse`. */ + def sequence[A, M[X] <: IterableOnce[X]](in: M[Try[A]])(implicit bf: BuildFrom[M[Try[A]], A, M[A]]): Try[M[A]] = + in.iterator + .foldLeft(Try(bf.newBuilder(in))) { + case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f + case (tr, tb) => + for { + r <- tr + a <- tb + } yield r += a + } + .map(_.result()) + + /** Transforms a `IterableOnce[A]` into a `Try[IterableOnce[B]]` using the provided function `A => Try[B]`. */ + def traverse[A, B, M[X] <: IterableOnce[X]](in: M[A])(fn: A => Try[B])(implicit bf: BuildFrom[M[A], B, M[B]]) + : Try[M[B]] = + in.iterator + .map(fn) + .foldLeft(Try(bf.newBuilder(in))) { + case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f + case (tr, tb) => + for { + r <- tr + b <- tb + } yield r += b + } + .map(_.result()) + } + + object PartialFunctionOps { + private[commons] object NoValueMarker + private[commons] final val NoValueMarkerFunc = (_: Any) => NoValueMarker + } + + object MapOps { + case class Entry[K, V](key: K, value: V) + } + object IteratorCompanionOps { def untilEmpty[T](elem: => Opt[T]): Iterator[T] = new AbstractIterator[T] { @@ -797,18 +790,4 @@ object SharedExtensionsUtils extends SharedExtensions { } } } - - final class OrderingOps[A](private val ordering: Ordering[A]) extends AnyVal { - @deprecated("Scala 2.13 has native scala.math.Ordering.orElse implementation", "2.27.0") - def orElse(whenEqual: Ordering[A]): Ordering[A] = - (x, y) => - ordering.compare(x, y) match { - case 0 => whenEqual.compare(x, y) - case res => res - } - - @deprecated("Scala 2.13 has native scala.math.Ordering.orElseBy implementation", "2.27.0") - def orElseBy[B: Ordering](f: A => B): Ordering[A] = - orElse(Ordering.by(f)) - } } From dd2e5fb21eb787e8a6be2c1089942d478187264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:32:11 +0200 Subject: [PATCH 22/25] =?UTF-8?q?refactor(scala-3,core):=20JsInterop=20Und?= =?UTF-8?q?efOrOps/JsOptOps=20implicit=20def=20+=20AnyVal=20=E2=86=92=20ex?= =?UTF-8?q?tension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the de-facto-implicit-class pattern in `JsInterop` to `extension` blocks: `extension [A](undefOr: UndefOr[A]) { def toOpt }` and `extension [A](opt: Opt[A]) { def orUndefined }`. This depends on the prior SharedExtensions sweep: with `OptionOps.toOpt` still wrapped as an implicit-class conversion, the new `UndefOr[A].toOpt` extension would shadow it via Scala 3 resolution preference (Opt[Option[Int]] inferred via `Option[Int] <: UndefOr[Option[Int]]` widening). Now that `Option[A].toOpt` is also an `extension`, both compete at equal rank and the compiler picks by receiver-type specificity (`Option[Int]` resolves to `extension [A](option: Option[A])`). `jsDateTimestampConversions` (wrapping shared TimestampConversions) intentionally NOT converted — slice 3.3 territory (`implicit def → given Conversion`). --- .../avsystem/commons/jsiop/JsInterop.scala | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/core/js/src/main/scala/com/avsystem/commons/jsiop/JsInterop.scala b/core/js/src/main/scala/com/avsystem/commons/jsiop/JsInterop.scala index 683ad63b0..fe89f3859 100644 --- a/core/js/src/main/scala/com/avsystem/commons/jsiop/JsInterop.scala +++ b/core/js/src/main/scala/com/avsystem/commons/jsiop/JsInterop.scala @@ -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 @@ -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 From caaa3dfcf7e6229f1fb9738a9721d58aef272aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:33:07 +0200 Subject: [PATCH 23/25] style(scala-3): scalafmt fixups for slice 3.1 extension sweep --- .../scala/com/avsystem/commons/jiop/JOptionalUtils.scala | 1 + .../scala/com/avsystem/commons/mongo/sync/MongoOps.scala | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala index 8f197d03b..b485b163e 100644 --- a/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala +++ b/core/jvm/src/main/scala/com/avsystem/commons/jiop/JOptionalUtils.scala @@ -78,6 +78,7 @@ trait JOptionalUtils { // 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 */ diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala index 4b90a6905..2a56f5e07 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/sync/MongoOps.scala @@ -29,11 +29,7 @@ trait MongoOps { def page(sort: Bson, offset: Int, maxItems: Int): Vector[T] = { val b = Vector.newBuilder[T] - find - .sort(sort) - .skip(offset) - .limit(maxItems) - .forEach(b += _) + find.sort(sort).skip(offset).limit(maxItems).forEach(b += _) b.result() } } From f37b442faccf3c51de058891fcc41669fde8235c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:34:43 +0200 Subject: [PATCH 24/25] =?UTF-8?q?docs(migration):=20record=20de-facto-impl?= =?UTF-8?q?icit-class=20=E2=86=92=20extension=20sweep=20(slice=203.1=20fol?= =?UTF-8?q?low-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index a7084e589..bd587a9c7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -57,6 +57,36 @@ the bottom of this file. Restoration ships incrementally per feature area. - `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 From af4d4548f6d0332dee77b1c165556b748216e5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 19:44:22 +0200 Subject: [PATCH 25/25] refactor(scala-3,mongo): convert UpdateOperatorsDsl.ForCollection to extension --- .../mongo/typed/UpdateOperatorsDsl.scala | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala index 96841d58d..6dee8b4a5 100644 --- a/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala +++ b/mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/UpdateOperatorsDsl.scala @@ -27,17 +27,9 @@ trait UpdateOperatorsDsl[T, R] { object UpdateOperatorsDsl { import MongoUpdateOperator._ - // TODO[scala3-port]: convert ForCollection to an `extension` block once the HKT receiver-inference - // regression is fixed (dotty#XXXXX). A `given Conversion` (not an `extension`) is used here so - // the higher-kinded `C[T]` is unified once, at conversion time, against the receiver's - // `UpdateOperatorsDsl[C[T], R]` base type. Plain extension methods fail to infer `C`/`T` from the - // receiver for named-argument calls such as `push(sort = ...)`. - given [C[X] <: Iterable[X], T, R] => Conversion[UpdateOperatorsDsl[C[T], R], ForCollection[C, T, R]] = - ForCollection(_) + extension [C[X] <: Iterable[X], T, R](dsl: UpdateOperatorsDsl[C[T], R]) { - class ForCollection[C[X] <: Iterable[X], T, R](dsl: UpdateOperatorsDsl[C[T], R]) { - - private def format: MongoFormat[T] = dsl.format.assumeCollection.elementFormat + private def mongoFormat: MongoFormat[T] = dsl.format.assumeCollection.elementFormat def push(values: T*): R = push(values) @@ -47,26 +39,26 @@ object UpdateOperatorsDsl { slice: OptArg[Int] = OptArg.Empty, sort: OptArg[MongoOrder[T]] = OptArg.Empty, ): R = - dsl.wrapUpdateOperator(Push(values, position.toOpt, slice.toOpt, sort.toOpt, format)) + dsl.wrapUpdateOperator(Push(values, position.toOpt, slice.toOpt, sort.toOpt, mongoFormat)) def addToSet(values: T*): R = addToSet(values) - def addToSet(values: Iterable[T]): R = dsl.wrapUpdateOperator(AddToSet(values, format)) + def addToSet(values: Iterable[T]): R = dsl.wrapUpdateOperator(AddToSet(values, mongoFormat)) def popFirst: R = pop(true) def popLast: R = pop(false) def pop(first: Boolean): R = dsl.wrapUpdateOperator(Pop(first)) def pull(filter: MongoFilter.Creator[T] => MongoFilter[T]): R = - dsl.wrapUpdateOperator(Pull(filter(new MongoFilter.Creator(format)))) + dsl.wrapUpdateOperator(Pull(filter(new MongoFilter.Creator(mongoFormat)))) def pullAll(values: T*): R = pullAll(values) - def pullAll(values: Iterable[T]): R = dsl.wrapUpdateOperator(PullAll(values, format)) + def pullAll(values: Iterable[T]): R = dsl.wrapUpdateOperator(PullAll(values, mongoFormat)) /** Uses [[https://docs.mongodb.com/manual/reference/operator/update/positional/ the $$ positional operator]] to * update first element of an array field that matches the query document. The array field must appear as part of * the query document. */ def updateFirstMatching(update: MongoUpdate.Creator[T] => MongoUpdate[T]): R = { - val up = update(new MongoUpdate.Creator(format)) + val up = update(new MongoUpdate.Creator(mongoFormat)) dsl.wrapUpdate(MongoUpdate.UpdateArrayElements(up, MongoUpdate.ArrayElementsQualifier.FirstMatching())) } @@ -75,7 +67,7 @@ object UpdateOperatorsDsl { * to update all elements of an array field. */ def updateAll(update: MongoUpdate.Creator[T] => MongoUpdate[T]): R = { - val up = update(new MongoUpdate.Creator(format)) + val up = update(new MongoUpdate.Creator(mongoFormat)) dsl.wrapUpdate(MongoUpdate.UpdateArrayElements(up, MongoUpdate.ArrayElementsQualifier.Each())) } @@ -86,8 +78,8 @@ object UpdateOperatorsDsl { */ def updateFiltered(filter: MongoFilter.Creator[T] => MongoFilter[T], update: MongoUpdate.Creator[T] => MongoUpdate[T]) : R = { - val fil = filter(new MongoFilter.Creator(format)) - val up = update(new MongoUpdate.Creator(format)) + val fil = filter(new MongoFilter.Creator(mongoFormat)) + val up = update(new MongoUpdate.Creator(mongoFormat)) dsl.wrapUpdate(MongoUpdate.UpdateArrayElements(up, MongoUpdate.ArrayElementsQualifier.Filtered(fil))) } }