diff --git a/README.md b/README.md index ad133fdcd..6577d5f41 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ * Lightweight alternatives for Scala `Option` - [`Opt`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/Opt.html) - guarantees no `null`s, [`OptArg`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/OptArg.html), + [`ImplicitOptArg`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/ImplicitOptArg.html), [`NOpt`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/NOpt.html), [`OptRef`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/OptRef.html) (implemented as value classes) 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 f1ea0036b..88e0db4ae 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,11 +1,11 @@ package com.avsystem.commons package jiop -import java.{util => ju} +import java.util as ju trait JOptionalUtils { - import JOptionalUtils._ + import JOptionalUtils.* type JOptional[T] = ju.Optional[T] type JOptionalDouble = ju.OptionalDouble @@ -37,6 +37,9 @@ trait JOptionalUtils { implicit def optArg2AsJava[T](opt: OptArg[T]): optArg2AsJava[T] = new optArg2AsJava((opt: OptArg[Any]).orNull) + implicit def implicitOptArg2AsJava[T](opt: ImplicitOptArg[T]): implicitOptArg2AsJava[T] = + new implicitOptArg2AsJava((opt: ImplicitOptArg[Any]).orNull) + implicit def option2AsJavaDouble(option: Option[Double]): option2AsJavaDouble = new option2AsJavaDouble(option) @@ -166,6 +169,18 @@ object JOptionalUtils { def asJava: JOptional[T] = toJOptional } + final class implicitOptArg2AsJava[T](private val rawOrNull: Any) extends AnyVal { + private def opt: ImplicitOptArg[T] = ImplicitOptArg(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() + + def asJava: JOptional[T] = toJOptional + } final class option2AsJavaDouble(private val option: Option[Double]) extends AnyVal { def toJOptionalDouble: JOptionalDouble = diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index a22d1056a..a6c043be0 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -1,14 +1,14 @@ package com.avsystem.commons import com.avsystem.commons.concurrent.RunNowEC -import com.avsystem.commons.misc._ +import com.avsystem.commons.misc.* import scala.annotation.{nowarn, tailrec} import scala.collection.{AbstractIterator, BuildFrom, Factory, mutable} trait SharedExtensions { - import com.avsystem.commons.SharedExtensionsUtils._ + import com.avsystem.commons.SharedExtensionsUtils.* implicit def universalOps[A](a: A): UniversalOps[A] = new UniversalOps(a) @@ -416,6 +416,11 @@ object SharedExtensionsUtils extends SharedExtensions { */ def toOptArg: OptArg[A] = if (option.isEmpty) OptArg.Empty else OptArg(option.get) + /** + * Converts this `Option` into `ImplicitOptArg`. Because `ImplicitOptArg` cannot hold `null`, `Some(null)` is translated to `OptArg.Empty`. + */ + def toImplicitOptArg: ImplicitOptArg[A] = + if (option.isEmpty) ImplicitOptArg.Empty else ImplicitOptArg(option.get) /** * Apply side effect only if Option is empty. It's a bit like foreach for None @@ -502,7 +507,7 @@ object SharedExtensionsUtils extends SharedExtensions { class PartialFunctionOps[A, B](private val pf: PartialFunction[A, B]) extends AnyVal { - import PartialFunctionOps._ + import PartialFunctionOps.* /** * The same thing as `orElse` but with arguments flipped. @@ -638,7 +643,7 @@ object SharedExtensionsUtils extends SharedExtensions { class MapOps[M[X, Y] <: BMap[X, Y], K, V](private val map: M[K, V]) extends AnyVal { - import MapOps._ + import MapOps.* def getOpt(key: K): Opt[V] = map.get(key).toOpt diff --git a/core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala b/core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala index c4c9063e5..a2bef214d 100644 --- a/core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala +++ b/core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala @@ -66,6 +66,9 @@ object OptionLike { implicit def optArgOptionLike[A]: BaseOptionLike[OptArg[A], A] = new OptionLikeImpl(OptArg.Empty, OptArg.some, _.isDefined, _.get, ignoreNulls = true) + implicit def implicitOptArgOptionLike[A]: BaseOptionLike[ImplicitOptArg[A], A] = + new OptionLikeImpl(ImplicitOptArg.Empty, ImplicitOptArg.some, _.isDefined, _.get, ignoreNulls = true) + implicit def nOptOptionLike[A]: BaseOptionLike[NOpt[A], A] = new OptionLikeImpl(NOpt.Empty, NOpt.some, _.isDefined, _.get, ignoreNulls = false) } diff --git a/core/src/main/scala/com/avsystem/commons/misc/ImplicitOptArg.scala b/core/src/main/scala/com/avsystem/commons/misc/ImplicitOptArg.scala new file mode 100644 index 000000000..02a3ca3f0 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/misc/ImplicitOptArg.scala @@ -0,0 +1,132 @@ +package com.avsystem.commons.misc + +object ImplicitOptArg { + /** + * This implicit conversion allows you to pass unwrapped values where `OptArg` is required. + */ + implicit def argToImplicitOptArg[A](value: A): ImplicitOptArg[A] = ImplicitOptArg(value) + implicit def implicitArgToImplicitOptArg[A](implicit value: A): ImplicitOptArg[A] = ImplicitOptArg(value) + + // additional implicits to cover most common, safe numeric promotions + implicit def intToOptArgLong(int: Int): ImplicitOptArg[Long] = ImplicitOptArg(int) + implicit def intToOptArgDouble(int: Int): ImplicitOptArg[Double] = ImplicitOptArg(int) + + private object EmptyMarker extends Serializable + + def apply[A](value: A): ImplicitOptArg[A] = new ImplicitOptArg[A](if (value != null) value else EmptyMarker) + def unapply[A](opt: ImplicitOptArg[A]): ImplicitOptArg[A] = opt //name-based extractor + + def some[A](value: A): ImplicitOptArg[A] = + if (value != null) new ImplicitOptArg[A](value) + else throw new NullPointerException + + val Empty: ImplicitOptArg[Nothing] = new ImplicitOptArg(EmptyMarker) + def empty[A]: ImplicitOptArg[A] = Empty +} + +/** + * [[ImplicitOptArg]] is like [[OptArg]] except it's intended to be used to type-safely express optional implicit method/constructor + * parameters while at the same time avoiding having to explicitly wrap arguments when passing them + * (thanks to the implicit conversion from `implicit A` to `ImplicitOptArg[A]`). For example: + * + * {{{ + * def takesMaybeString(implicit str: ImplicitOptArg[String]) = ??? + * + * implicit val str: String = "string" + * takesMaybeString() // str is used + * takesMaybeString(using str) // str is used explicitly + * takesMaybeString(using ImplicitOptArg.Empty) // Empty is used explicitly + * }}} + * + * Note that like [[Opt]], [[ImplicitOptArg]] assumes its underlying value to be non-null and `null` is translated into `ImplicitOptArg.Empty`. + *
+ * It is strongly recommended that [[ImplicitOptArg]] type is used without default argument. + * You should not use [[ImplicitOptArg]] as a general-purpose "optional value" type - other types like + * [[Opt]], [[NOpt]] and `Option` serve that purpose. For this reason [[ImplicitOptArg]] deliberately does not have any "transforming" + * methods like `map`, `flatMap`, `orElse`, etc. Instead it's recommended that [[ImplicitOptArg]] is converted to [[Opt]], + * [[NOpt]] or `Option` as soon as possible (using `toOpt`, `toNOpt` and `toOption` methods). + */ +final class ImplicitOptArg[+A] private(private val rawValue: Any) extends AnyVal with OptBase[A] with Serializable { + + import ImplicitOptArg.* + + private def value: A = rawValue.asInstanceOf[A] + + @inline def get: A = if (isEmpty) throw new NoSuchElementException("empty ImplicitOptArg") else value + + @inline def isEmpty: Boolean = rawValue.asInstanceOf[AnyRef] eq EmptyMarker + @inline def isDefined: Boolean = !isEmpty + @inline def nonEmpty: Boolean = isDefined + + @inline def boxedOrNull[B >: Null](implicit boxing: Boxing[A, B]): B = + if (isEmpty) null else boxing.fun(value) + + @inline def toOpt: Opt[A] = + if (isEmpty) Opt.Empty else Opt(value) + + @inline def toOption: Option[A] = + if (isEmpty) None else Some(value) + + @inline def toNOpt: NOpt[A] = + if (isEmpty) NOpt.Empty else NOpt.some(value) + + @inline def toOptRef[B >: Null](implicit boxing: Boxing[A, B]): OptRef[B] = + if (isEmpty) OptRef.Empty else OptRef(boxing.fun(value)) + + @inline def getOrElse[B >: A](default: => B): B = + if (isEmpty) default else value + + @inline def orNull[B >: A](implicit ev: Null <:< B): B = + if (isEmpty) ev(null) else value + + @inline def fold[B](ifEmpty: => B)(f: A => B): B = + if (isEmpty) ifEmpty else f(value) + + /** + * The same as [[fold]] but takes arguments in a single parameter list for better type inference. + */ + @inline def mapOr[B](ifEmpty: => B, f: A => B): B = + if (isEmpty) ifEmpty else f(value) + + @inline def contains[A1 >: A](elem: A1): Boolean = + !isEmpty && value == elem + + @inline def exists(p: A => Boolean): Boolean = + !isEmpty && p(value) + + @inline def forall(p: A => Boolean): Boolean = + isEmpty || p(value) + + @inline def foreach[U](f: A => U): Unit = { + if (!isEmpty) f(value) + } + + @inline def iterator: Iterator[A] = + if (isEmpty) Iterator.empty else Iterator.single(value) + + @inline def toList: List[A] = + if (isEmpty) List() else value :: Nil + + @inline def toRight[X](left: => X): Either[X, A] = + if (isEmpty) Left(left) else Right(value) + + @inline def toLeft[X](right: => X): Either[A, X] = + if (isEmpty) Right(right) else Left(value) + + /** + * Apply side effect only if ImplicitOptArg is empty. It's a bit like foreach for ImplicitOptArg.Empty + * + * @param sideEffect - code to be executed if optArg is empty + * @return the same ImplicitOptArg + * @example {{{captionOptArg.forEmpty(logger.warn("caption is empty")).foreach(setCaption)}}} + */ + @inline def forEmpty(sideEffect: => Unit): ImplicitOptArg[A] = { + if (isEmpty) { + sideEffect + } + this + } + + override def toString: String = + if (isEmpty) "ImplicitOptArg.Empty" else s"ImplicitOptArg($value)" +} diff --git a/core/src/main/scala/com/avsystem/commons/misc/MiscAliases.scala b/core/src/main/scala/com/avsystem/commons/misc/MiscAliases.scala index c51c87c99..37031a3b4 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/MiscAliases.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/MiscAliases.scala @@ -6,6 +6,8 @@ trait MiscAliases { final val Opt = com.avsystem.commons.misc.Opt type OptArg[+A] = com.avsystem.commons.misc.OptArg[A] final val OptArg = com.avsystem.commons.misc.OptArg + type ImplicitOptArg[+A] = com.avsystem.commons.misc.ImplicitOptArg[A] + final val ImplicitOptArg = com.avsystem.commons.misc.ImplicitOptArg type NOpt[+A] = com.avsystem.commons.misc.NOpt[A] final val NOpt = com.avsystem.commons.misc.NOpt type OptRef[+A >: Null] = com.avsystem.commons.misc.OptRef[A] diff --git a/core/src/main/scala/com/avsystem/commons/misc/NOpt.scala b/core/src/main/scala/com/avsystem/commons/misc/NOpt.scala index f66a5b642..caad59886 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/NOpt.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/NOpt.scala @@ -43,7 +43,7 @@ object NOpt { */ final class NOpt[+A] private(private val rawValue: Any) extends AnyVal with OptBase[A] with Serializable { - import NOpt._ + import NOpt.* private def value: A = (if (rawValue.asInstanceOf[AnyRef] eq NullMarker) null else rawValue).asInstanceOf[A] @@ -83,6 +83,9 @@ final class NOpt[+A] private(private val rawValue: Any) extends AnyVal with OptB @inline def toOptArg: OptArg[A] = if (isEmpty) OptArg.Empty else OptArg(value) + @inline def toImplicitOptArg: ImplicitOptArg[A] = + if (isEmpty) ImplicitOptArg.Empty else ImplicitOptArg(value) + @inline def getOrElse[B >: A](default: => B): B = if (isEmpty) default else value @@ -142,7 +145,7 @@ final class NOpt[+A] private(private val rawValue: Any) extends AnyVal with OptB if (isEmpty) Iterator.empty else Iterator.single(value) @inline def toList: List[A] = - if (isEmpty) List() else new ::(value, Nil) + if (isEmpty) List() else new::(value, Nil) @inline def toRight[X](left: => X): Either[X, A] = if (isEmpty) Left(left) else Right(value) 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 99e346a82..ef2e18c7b 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/Opt.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/Opt.scala @@ -88,6 +88,9 @@ final class Opt[+A] private(private val rawValue: Any) extends AnyVal with OptBa @inline def toOptArg: OptArg[A] = if (isEmpty) OptArg.Empty else OptArg(value) + @inline def toImplicitOptArg: ImplicitOptArg[A] = + if (isEmpty) ImplicitOptArg.Empty else ImplicitOptArg(value) + @inline def getOrElse[B >: A](default: => B): B = if (isEmpty) default else value diff --git a/core/src/main/scala/com/avsystem/commons/misc/OptArg.scala b/core/src/main/scala/com/avsystem/commons/misc/OptArg.scala index 7eab8c96b..38bb9f9dc 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/OptArg.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/OptArg.scala @@ -44,6 +44,7 @@ object OptArg { * [[NOpt]] or `Option` as soon as possible (using `toOpt`, `toNOpt` and `toOption` methods). */ final class OptArg[+A] private(private val rawValue: Any) extends AnyVal with OptBase[A] with Serializable { + import OptArg._ private def value: A = rawValue.asInstanceOf[A] @@ -69,6 +70,9 @@ final class OptArg[+A] private(private val rawValue: Any) extends AnyVal with Op @inline def toOptRef[B >: Null](implicit boxing: Boxing[A, B]): OptRef[B] = if (isEmpty) OptRef.Empty else OptRef(boxing.fun(value)) + @inline def toImplicitOptArg: ImplicitOptArg[A] = + if (isEmpty) ImplicitOptArg.Empty else ImplicitOptArg(value) + @inline def getOrElse[B >: A](default: => B): B = if (isEmpty) default else value @@ -101,7 +105,7 @@ final class OptArg[+A] private(private val rawValue: Any) extends AnyVal with Op if (isEmpty) Iterator.empty else Iterator.single(value) @inline def toList: List[A] = - if (isEmpty) List() else new ::(value, Nil) + if (isEmpty) List() else new::(value, Nil) @inline def toRight[X](left: => X): Either[X, A] = if (isEmpty) Left(left) else Right(value) diff --git a/core/src/main/scala/com/avsystem/commons/misc/OptRef.scala b/core/src/main/scala/com/avsystem/commons/misc/OptRef.scala index 14902d22f..b57a02b54 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/OptRef.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/OptRef.scala @@ -67,6 +67,9 @@ final class OptRef[+A >: Null] private(private val value: A) extends AnyVal with @inline def toOptArg: OptArg[A] = if (isEmpty) OptArg.Empty else OptArg(value) + @inline def toImplicitOptArg: ImplicitOptArg[A] = + if (isEmpty) ImplicitOptArg.Empty else ImplicitOptArg(value) + @inline def getOrElse[B >: A](default: => B): B = if (isEmpty) default else value 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 b35f06563..697fb62ed 100644 --- a/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala +++ b/core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala @@ -439,7 +439,7 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { private implicit class IterableOps[A](private val coll: BIterable[A]) extends AnyVal { def writeToList(lo: ListOutput)(implicit writer: GenCodec[A]): Unit = { lo.declareSizeOf(coll) - coll.foreach(new (A => Unit) { + coll.foreach(new(A => Unit) { private var idx = 0 def apply(a: A): Unit = { try writer.write(lo.writeElement(), a) catch { @@ -599,6 +599,9 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { implicit def optArgCodec[T: GenCodec]: GenCodec[OptArg[T]] = new Transformed[OptArg[T], Opt[T]](optCodec[T], _.toOpt, _.toOptArg) + implicit def implicitOptArgCodec[T: GenCodec]: GenCodec[ImplicitOptArg[T]] = + new Transformed[ImplicitOptArg[T], Opt[T]](optCodec[T], _.toOpt, _.toImplicitOptArg) + implicit def optRefCodec[T >: Null : GenCodec]: GenCodec[OptRef[T]] = new Transformed[OptRef[T], Opt[T]](optCodec[T], _.toOpt, _.toOptRef) diff --git a/core/src/test/scala/com/avsystem/commons/misc/ImplicitOptArgTest.scala b/core/src/test/scala/com/avsystem/commons/misc/ImplicitOptArgTest.scala new file mode 100644 index 000000000..4e3502dbd --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/misc/ImplicitOptArgTest.scala @@ -0,0 +1,47 @@ +package com.avsystem.commons.misc + +import com.avsystem.commons.SharedExtensions.* +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +final class ImplicitOptArgTest extends AnyFunSuite with Matchers { + test("nonempty") { + val opt = ImplicitOptArg(23) + opt match { + case ImplicitOptArg(num) => assert(num == 23) + } + } + + test("empty") { + val str: String = null + val opt = ImplicitOptArg(str) + opt match { + case ImplicitOptArg.Empty => + } + } + + test("null some") { + intercept[NullPointerException](ImplicitOptArg.some[String](null)) + } + + def takeMaybeString(implicit str: ImplicitOptArg[String] = ImplicitOptArg.Empty): Opt[String] = str.toOpt + def takeMaybeInt(implicit str: ImplicitOptArg[Int]): Opt[Int] = str.toOpt + + implicit def int: Int = 42 + + + test("argument passing") { + takeMaybeString shouldEqual Opt.Empty + takeMaybeString(using ImplicitOptArg.Empty) shouldEqual Opt.Empty + takeMaybeString(using "stringzor") shouldEqual "stringzor".opt + + takeMaybeInt shouldEqual 42.opt + takeMaybeInt(ImplicitOptArg.Empty) shouldEqual Opt.Empty + + "stefan" |> { implicit st => + takeMaybeString shouldEqual "stefan".opt + takeMaybeString(using "mietek") shouldEqual "mietek".opt + takeMaybeString(using ImplicitOptArg.Empty) shouldEqual Opt.Empty + } + } +} diff --git a/docs/GenCodec.md b/docs/GenCodec.md index 325b7da90..15bc568f1 100644 --- a/docs/GenCodec.md +++ b/docs/GenCodec.md @@ -7,31 +7,31 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [`GenCodec`](#gencodec) - - [The `GenCodec` typeclass](#the-gencodec-typeclass) - - [Formats supported by default](#formats-supported-by-default) - - [`GenKeyCodec`](#genkeycodec) - - [`GenObjectCodec`](#genobjectcodec) - - [Writing and reading](#writing-and-reading) - - [`GenCodec` instances available by default](#gencodec-instances-available-by-default) - - [Deriving codecs](#deriving-codecs) - - [Deriving codecs for generic types](#deriving-codecs-for-generic-types) - - [Depending on external implicits](#depending-on-external-implicits) - - [Serializing case classes](#serializing-case-classes) - - [Field name customization](#field-name-customization) - - [Default field values](#default-field-values) - - [Transient default field values](#transient-default-field-values) - - [Optional and nullable fields](#optional-and-nullable-fields) - - [Case class like types](#case-class-like-types) - - [Serializing sealed hierarchies](#serializing-sealed-hierarchies) - - [Nested sealed hierarchy format](#nested-sealed-hierarchy-format) - - [Flat sealed hierarchy format](#flat-sealed-hierarchy-format) - - [Sealed hierarchy default case](#sealed-hierarchy-default-case) - - [Case name customization](#case-name-customization) - - [Transparent wrappers](#transparent-wrappers) - - [Writing codecs for third party types](#writing-codecs-for-third-party-types) - - [Derive the codec from a "fake companion"](#derive-the-codec-from-a-fake-companion) - - [Transform the codec of another type](#transform-the-codec-of-another-type) - - [Implement the codec manually](#implement-the-codec-manually) + - [The `GenCodec` typeclass](#the-gencodec-typeclass) + - [Formats supported by default](#formats-supported-by-default) + - [`GenKeyCodec`](#genkeycodec) + - [`GenObjectCodec`](#genobjectcodec) + - [Writing and reading](#writing-and-reading) + - [`GenCodec` instances available by default](#gencodec-instances-available-by-default) + - [Deriving codecs](#deriving-codecs) + - [Deriving codecs for generic types](#deriving-codecs-for-generic-types) + - [Depending on external implicits](#depending-on-external-implicits) + - [Serializing case classes](#serializing-case-classes) + - [Field name customization](#field-name-customization) + - [Default field values](#default-field-values) + - [Transient default field values](#transient-default-field-values) + - [Optional and nullable fields](#optional-and-nullable-fields) + - [Case class like types](#case-class-like-types) + - [Serializing sealed hierarchies](#serializing-sealed-hierarchies) + - [Nested sealed hierarchy format](#nested-sealed-hierarchy-format) + - [Flat sealed hierarchy format](#flat-sealed-hierarchy-format) + - [Sealed hierarchy default case](#sealed-hierarchy-default-case) + - [Case name customization](#case-name-customization) + - [Transparent wrappers](#transparent-wrappers) + - [Writing codecs for third party types](#writing-codecs-for-third-party-types) + - [Derive the codec from a "fake companion"](#derive-the-codec-from-a-fake-companion) + - [Transform the codec of another type](#transform-the-codec-of-another-type) + - [Implement the codec manually](#implement-the-codec-manually) @@ -89,10 +89,11 @@ Within `scala-commons` you can find `Input` and `Output` implementations for the * using Java intermediate `BsonValue` representation - `BsonValueInput` & `BsonValueOutput` * using Java stream-like `BsonReader` and `BsonWriter` - `BsonReaderInput` & `BsonWriterOutput` * [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) using - [Lightbend Config](https://github.com/lightbend/config) representation, available in `commons-hocon` module - + [Lightbend Config](https://github.com/lightbend/config) representation, available in `commons-hocon` module - `HoconInput` & `HoconOutput` -Also, in principle it should be relatively easy to implement `Input` and `Output` for various intermediate representations +Also, in principle it should be relatively easy to implement `Input` and `Output` for various intermediate +representations found in third party libraries, e.g. JSON ASTs implemented by all the JSON serialization libraries. ### `GenKeyCodec` @@ -141,10 +142,10 @@ val ints: List[Int] = JsonStringInput.read[List[Int]]("[1, 2, 3]") * Scala enums (extending `NamedEnum` with companion extending `NamedEnumCompanion`) - they serialize as strings equal to their names * Java enums - they serialize as strings equal to their names -* `Option[T]`, `Opt[T]`, `OptArg[T]`, `NOpt[T]` - assuming availability of `GenCodec[T]` +* `Option[T]`, `Opt[T]`, `OptArg[T]`, `ImplicitOptArg[T]`, `NOpt[T]` - assuming availability of `GenCodec[T]` * empty values (`None`, `Opt.Empty`, etc) serialize as `null` while non-empty values serialize as-is - therefore it is not possible to unambiguously serialize `Some(null)` and `NOpt(null)` - they will collapse to `None` - and `NOpt.Empty` upon deserialization (note that `Opt(null)` and `OptArg(null)` already collapse to empty values + and `NOpt.Empty` upon deserialization (note that `Opt(null)`, `OptArg(null)` and `ImplicitOptArg(null)` already collapse to empty values in runtime, independent of serialization). * `Either[A, B]` - assuming availability of `GenCodec[A]` and `GenCodec[B]` @@ -312,7 +313,8 @@ deserialization (i.e. you don't want a language-level default value): case class Data(int: Int, @whenAbsent("default") string: String) ``` -Default field values allow you to evolve your case classes by adding fields, without breaking serialization compatibility. +Default field values allow you to evolve your case classes by adding fields, without breaking serialization +compatibility. #### Transient default field values @@ -362,7 +364,8 @@ case class Data(int: Int, @transientDefault str: Option[String] = None) However, `@optionalParam` is the recommended, more "native" way to do this. -When using `@optionalParam` with `Option`/`Opt`/`OptArg`, `null`-valued fields are treated equivalently to missing fields. If you +When using `@optionalParam` with `Option`/`Opt`/`OptArg`/`ImplicitOptArg`, `null`-valued fields are treated equivalently to missing +fields. If you need to distinguish between missing fields and `null`-valued fields, this can be achieved with the help of `NOpt` (a nullable `Opt`): @@ -375,8 +378,8 @@ object Data extends HasGenCodec[Data] def printJson(value: Data): Unit = println(JsonStringOutput.write(value)) -printJson(Data(42, NOpt.Empty)) // {"int":42} -printJson(Data(42, NOpt(None))) // {"int":42,"str":null} +printJson(Data(42, NOpt.Empty)) // {"int":42} +printJson(Data(42, NOpt(None))) // {"int":42,"str":null} printJson(Data(42, NOpt(Some("foo")))) // {"int":42,"str":"foo"} ``` @@ -384,7 +387,7 @@ printJson(Data(42, NOpt(Some("foo")))) // {"int":42,"str":"foo"} The macro that materializes codecs for case classes does not strictly require a `case class`. It is enough if a class or trait _looks sufficiently like_ a case class. Strictly speaking, the class or trait must have a companion -object with `apply` and `unapply` methods defined as if it were a case class (`unapplySeq` if repeated parameters are +object with `apply` and `unapply` methods defined as if it were a case class (`unapplySeq` if repeated parameters are in play). For a real `case class`, these methods are automatically synthesized by the compiler. ```scala @@ -400,7 +403,7 @@ object Stuff extends HasGenCodec[Stuff] { def intValue = int def strValue = str } - + def unapply(stuff: Stuff): Some[(Int, String)] = Some((stuff.intValue, stuff.strValue)) } @@ -430,9 +433,9 @@ object Expr extends HasGenCodec[Expr] def printJson(value: Expr): Unit = println(JsonStringOutput.write[Expr](value)) -printJson(IntExpr(42)) // {"IntExpr":{"value":42}} +printJson(IntExpr(42)) // {"IntExpr":{"value":42}} printJson(StrExpr("foo")) // {"StrExpr":{"value":"foo"}} -printJson(NullExpr) // {"NullExpr":{}} +printJson(NullExpr) // {"NullExpr":{}} ``` The advantage of this format is that case classes and objects don't need to serialize into objects. @@ -451,9 +454,9 @@ object Expr extends HasGenCodec[Expr] def printJson(value: Expr): Unit = println(JsonStringOutput.write[Expr](value)) -printJson(IntExpr(42)) // {"IntExpr":42} +printJson(IntExpr(42)) // {"IntExpr":42} printJson(StrExpr("foo")) // {"StrExpr":"foo"} -printJson(NullExpr) // {"NullExpr":{}} +printJson(NullExpr) // {"NullExpr":{}} ``` ### Flat sealed hierarchy format @@ -474,9 +477,9 @@ object Expr extends HasGenCodec[Expr] def printJson(value: Expr): Unit = println(JsonStringOutput.write[Expr](value)) -printJson(IntExpr(42)) // {"type":"IntExpr","value":42} +printJson(IntExpr(42)) // {"type":"IntExpr","value":42} printJson(StrExpr("foo")) // {"type":"StrExpr","value":"foo"} -printJson(NullExpr) // {"type":"NullExpr"} +printJson(NullExpr) // {"type":"NullExpr"} ``` Flat sealed hierarchy format is cleaner but requires that all case classes and objects serialize into objects. @@ -518,9 +521,9 @@ import com.avsystem.commons.serialization._ @name("null") case object NullExpr extends Expr object Expr extends HasGenCodec[Expr] -printJson(IntExpr(42)) // {"type":"int","value":42} +printJson(IntExpr(42)) // {"type":"int","value":42} printJson(StrExpr("foo")) // {"type":"str","value":"foo"} -printJson(NullExpr) // {"type":"null"} +printJson(NullExpr) // {"type":"null"} ``` ## Transparent wrappers @@ -565,17 +568,18 @@ import java.time.Duration object ThirdPartyCodecs { object JavaDurationAU { - def apply(seconds: Long, nanos: Int): Duration = + def apply(seconds: Long, nanos: Int): Duration = Duration.ofSeconds(seconds).withNanos(nanos) - def unapply(duration: Duration): Some[(Long, Int)] = + def unapply(duration: Duration): Some[(Long, Int)] = Some((duration.getSeconds, duration.getNano)) } - + implicit val durationCodec: GenCodec[Duration] = GenCodec.fromApplyUnapplyProvider[Duration](JavaDurationAU) } import ThirdPartyCodecs._ + println(JsonStringOutput.write(Duration.ofSeconds(5).withNanos(500))) // {"seconds":5,"nanos":500} ``` @@ -593,15 +597,16 @@ import java.time.Duration object ThirdPartyCodecs { case class DurationRepr(seconds: Long, nanos: Int) object DurationRepr extends HasGenCodec[Duration] - + implicit val durationCodec: GenCodec[Duration] = DurationRepr.codec.transform[Duration]( - d => DurationRepr(d.getSeconds, d.getNamo), + d => DurationRepr(d.getSeconds, d.getNamo), dr => Duration.ofSeconds(dr.seconds).withNanos(dr.nanos) ) } import ThirdPartyCodecs._ + println(JsonStringOutput.write(Duration.ofSeconds(5).withNanos(500))) // {"seconds":5,"nanos":500} ``` @@ -626,7 +631,7 @@ object ThirdPartyCodecs { val nanos = input.getNextNamedField("nanos").readSimple().readInt() Duration.ofSeconds(seconds).withNanos(nanos) } - + def write(output: ObjectOutput, value: Duration): Unit = { output.writeField("seconds").writeSimple().writeLong(value.getSeconds) output.writeField("nanos").writeSimple().writeInt(value.getNano) @@ -635,6 +640,7 @@ object ThirdPartyCodecs { } import ThirdPartyCodecs._ + println(JsonStringOutput.write(Duration.ofSeconds(5).withNanos(500))) // {"seconds":5,"nanos":500} ``` diff --git a/docs/GenCodecOld.md b/docs/GenCodecOld.md index 6c5fca8de..726d5194a 100644 --- a/docs/GenCodecOld.md +++ b/docs/GenCodecOld.md @@ -238,7 +238,7 @@ The library by default provides codecs for common Scala and Java types: so you only have to worry about it when dealing with custom map implementations. * Any `java.util.Map[K,V]`, with the same restrictions as for Scala maps (there must be `GenCodec` for `V` and `GenKeyCodec` for `K`) -* `Option[T]`, `Opt[T]`, `OptArg[T]`, `NOpt[T]`, `OptRef[T]`, provided that `T` can be serialized. +* `Option[T]`, `Opt[T]`, `OptArg[T]`, `ImplicitOptArg[T]`, `NOpt[T]`, `OptRef[T]`, provided that `T` can be serialized. * `Either[A,B]`, provided that `A` and `B` can be serialized. * [`NamedEnum`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/NamedEnum.html)s whose companion object extends [`NamedEnumCompanion`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/NamedEnumCompanion.html). diff --git a/docs/TypedMongo.md b/docs/TypedMongo.md index 71f105231..469cc237d 100644 --- a/docs/TypedMongo.md +++ b/docs/TypedMongo.md @@ -172,7 +172,7 @@ additionally aware of internal structure of some types, including: * [embedded documents](#embedded-document-types) - serialized into BSON documents * collections, i.e. any subtype of `scala.collection.Seq` or `scala.collection.Set` - serialized into BSON arrays * maps, i.e. any subtype of `scala.collection.Map` - serialized into BSON documents -* option-like types, i.e. `Option`, `Opt`, `OptArg`, etc. - serialized into nullable values +* option-like types, i.e. `Option`, `Opt`, `OptArg`, `ImplicitOptArg[T]` etc. - serialized into nullable values Being aware of internal structure makes it possible to build queries, updates, projections, etc. that reach inside these data types. For example, you can refer to a specific map entry in a query. @@ -201,7 +201,7 @@ fields. In other words, the type would be valid but _opaque_. ### Optional fields -A field typed as an `Option`, `Opt`, `OptArg` or similar will serialize just like its wrapped value except that `null` +A field typed as an `Option`, `Opt`, `OptArg`, `ImplicitOptArg[T]` or similar will serialize just like its wrapped value except that `null` will be used to represent the absence of value. If you want to omit that `null`, effectively making the field optional on BSON level, use one of the following: diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenRefMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenRefMacros.scala index 460e38313..3a121cb98 100644 --- a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenRefMacros.scala +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenRefMacros.scala @@ -17,7 +17,8 @@ class GenRefMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) { val TransparentGets: Set[Symbol] = Set( staticType(tq"$CommonsPkg.Opt[_]"), staticType(tq"$CommonsPkg.OptArg[_]"), - staticType(tq"$CommonsPkg.OptRef[_]") + staticType(tq"$CommonsPkg.OptRef[_]"), + staticType(tq"$CommonsPkg.ImplicitOptArg[_]"), ).map(_.member(TermName("get"))) object MapApplyOrGet {