Skip to main content

Release Notes

All releases of Extras, newest first.

Showing 60 of 60 releases

2026

Bug Fixed

  • Fix: [extras-hedgehog-circe] NoSuchMethodError on hedgehog 0.15.0 (Syntax.==== gained SourcePos param) (#641)

    RoundTripTester in extras-hedgehog-circe 0.56.0 was compiled against hedgehog 0.13.1. hedgehog 0.15.0 changed the signature of ====, so RoundTripTester threw NoSuchMethodError at runtime with hedgehog 0.15.0. extras-hedgehog-circe is now built against hedgehog 0.15.0.

Internal Housekeeping

  • Update libraries (#642)
    • hedgehog: 0.13.1 => 0.15.0
    • hedgehog-extra: 0.21.0 => 0.25.0
  • [extras-scala-io] Change the return type of Rgb.unapply in Scala 3 from Option[(Red, Green, Blue)] to Some[(Red, Green, Blue)] so pattern matches with the Rgb extractor are exhaustive (#640)
  • Update sbt and sbt plugins (#640)

What's Changed

New Contributors

Internal Housekeeping

  • Update doobie 1 from 1.0.0-RC11 to 1.0.0-RC12 (#625)
  • Update libraries (#630)
    • fs2 3: 3.12.2 => 3.13.0
    • http4s 0.23: 0.23.16 => 0.23.33
    • hedgehog: 0.13.0 => 0.13.1
    • hedgehog-extra: 0.19.0 => 0.21.0
    • effectie: 2.3.0 => 2.4.0

What's Changed

Bug Fixed

  • Fix: [extras-render] Inaccessible given instance in Scala 3.8.x may cause a compilation error (#626)
    • Bug details: kevin-lee/orphan/issues/96

What's Changed

Internal Housekeeping

  • Update doobie 1.0 to 1.0.0-RC11 (#617)
  • Update Scala.js to 1.20.2 (#619)
  • Update Scala Native to 0.5.10 and cats-effect 3 to 3.7.0 (#621)

What's Changed

New Features

  • [extras-scala-io] Add dimming in ANSI Color (#607)

    e.g.)

    import extras.scala.io.syntax.color._

    "Hello".dim
    "Hello".blue.dim
    "Hello".rgbed(0x00eeff).dim
    Screenshot 2026-03-14 at 3 15 57 pm Screenshot 2026-03-14 at 3 16 20 pm Screenshot 2026-03-14 at 3 16 45 pm Screenshot 2026-03-14 at 3 20 16 pm

Bug Fixes

  • [extras-scala-io] Applying multiple colors adds multiple resets at the end (#609)

    • Before

      import extras.scala.io.syntax.color._

      "Hello".blue.bold.dim
      // \^[[2m\^[[1m\^[[34mHello\^[[0m\^[[0m\^[[0m
      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ "\^[[0m" x 3
      import extras.scala.io.syntax.truecolor.all._

      "Hello".rgbed(0x223344).rgbed(0xaa00ee).rgbed(0x00bb33)
      // \^[[38;2;0;187;51m\^[[38;2;170;0;238m\^[[38;2;34;51;68mHello\^[[0m\^[[0m\^[[0m
      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ "\^[[0m" x 3
    • Now

      import extras.scala.io.syntax.color._

      "Hello".blue.bold.dim
      // \^[[2m\^[[1m\^[[34mHello\^[[0m
      // ^^^^^^^^^
      import extras.scala.io.syntax.truecolor.all._

      "Hello".rgbed(0x223344).rgbed(0xaa00ee).rgbed(0x00bb33)
      // \^[[38;2;0;187;51m\^[[38;2;170;0;238m\^[[38;2;34;51;68mHello\^[[0m
      // ^^^^^^^^^

What's Changed

2025

Bug Fixes

  • [extras-core] extras.core.syntax.all is missing predefs in Scala 2 (#599)

What's Changed

New Features

  • [extras-core] Add the Elvis operator (?:) to extras.core.syntax for Scala 3 (#580)

    Elvis Operator

    val a: String | Null = "blah"

    a ?: "Unknown"
    // String = "blah"
    val a: String | Null = null

    a ?: "Unknown"
    // String = "Unknown"
    val a: String | Null = "blah"

    a ?: { println("Hello!"); "Unknown" }
    // String = "blah"
    val a: String | Null = null

    a ?: { println("Hello!"); "Unknown" }
    // Hello!
    // String = "Unknown"

  • [extras-core] Add the Crying Elvis Operator (?:=) to extras.core.syntax for Scala 2 (#594)

    Crying Elvis Operator

    • Unlike Scala 3's extension methods, in Scala 2, extension methods are just regular methods in an implicit value class. As a result, method names ending with : are right-associative, so a ?: b is evaluated as b.?:(a).

      Since we need the possibly-null value (a) to be the receiver, the operator is provided as ?:= (not ending with :), so a ?:= b is evaluated as a.?:=(b).

    • Scala 2 doesn't have union types like A | Null, so this operates on a plain A value that may be null at runtime.

    • Also, for compatibility with Scala 2 code, Scala 3 predefs should have ?:= as an alias for ?:.

    val a = "blah"

    a ?:= "Unknown"
    // String = "blah"
    val a: String = null

    a ?:= "Unknown"
    // String = "Unknown"
    val a = "blah"

    a ?:= { println("Hello!"); "Unknown" }
    // String = "blah"
    val a: String = null

    a ?:= { println("Hello!"); "Unknown" }
    // Hello!
    // String = "Unknown"

Changes

  • Rename extras.core.syntax.string to extras.core.syntax.strings (#578)

    Also, replace the package object for extras.core.syntax with a package named extras.core.syntax.

Internal Housekeeping

  • Upgrade doobie for cats-effect 3 to 1.0.0-RC10 (#587)

  • Update libraries (#591)

    • Update cats to 2.13.0
    • Update cats-effect 3 to 3.6.3
    • Update kittens to 3.5.0
    • Update circe to 0.14.13
    • Update fs2 v3 to 3.12.2
    • Update hedgehog-extra to 0.19.0
    • Update embedded-postgres to 2.2.0
    • Update effectie to 2.3.0

What's Changed

New Features

  • [extras-testing-tools] Support Scala Native (#572)
    • StubTools is not included because Thread.currentThread.getStackTrace.toList returns an empty List.

What's Changed

New Features

  • [extras-testing-tools] Add Scala.js support (#560)

  • [extras-testing-tools] Add compile-time error check for Scala 2 (#562)

    CompileTimeError.from(
    "some Scala code causing a compile-time error"
    )
    // String = "compile-time error message"

What's Changed

Bug Fixed

  • [extras-render] .render extension method doesn't work well when there's a method with the same name exists in the context in Scala 3 (#554)

    • Version: 0.46.0
    • Scala Version: 3
    • Java Version: n/a

    The following code

    import extras.render.Render
    import extras.render.syntax.*

    final case class JdkByCs(
    version: JdkByCs.Version,
    ) derives CanEqual
    object JdkByCs {

    extension (jdkByCs: JdkByCs) {
    def render: String =
    s"JdkByCs(version=${jdkByCs.version.render})" // <= this .render fails!
    }

    type Version = Version.Type
    object Version extends Newtype[SemVer | DecVer | DotSeparatedVersion] {

    extension (version: Version) {
    def major: MajorVersion = version.value match {
    case SemVer(m, n, _, _, _) =>
    MajorVersion(if (m.value == 1) n.value else m.value)

    case DecVer(m, n, _, _) =>
    MajorVersion(if (m.value == 1) n.value else m.value)

    case DotSeparatedVersion(v, vs) =>
    val vNum = v.toInt
    MajorVersion(
    if vNum == 1 then
    vs.take(1)
    .headOption
    .filter(_.forall(_.isDigit))
    .fold(vNum)(_.toInt)
    else vNum
    )
    }

    }

    given renderVersion: Render[Version] with {

    def render(a: Version): String = a.value match {
    case v: SemVer => SemVer.render(v)
    case v: DecVer => DecVer.render(v)
    case DotSeparatedVersion(v, vs) => s"$v.${vs.mkString(".")}"
    }
    }
    }

    }

    fails in compilation with an error like

    [error] -- [E008] Not Found Error: /home/runner/work/jdk-sym-link/jdk-sym-link/modules/jdk-sym-link-core/src/main/scala/jdksymlink/cs/CoursierCmd.scala:90:103
    [error] 90 | s"""JdkByCs(version=${jdkByCs.version.render})
    [error] | ^^^^^^^^^^^^^^^^^^^^^^
    [error] |value render is not a member of jdksymlink.cs.CoursierCmd.JdkByCs.Version.
    [error] |An extension method was tried, but could not be fully constructed:
    [error] |
    [error] | jdksymlink.cs.CoursierCmd.JdkByCs.render(jdkByCs.version)
    [error] |
    [error] | failed with:
    [error] |
    [error] | Found: (jdkByCs.version : jdksymlink.cs.CoursierCmd.JdkByCs.Version)
    [error] | Required: jdksymlink.cs.CoursierCmd.JdkByCs
    • Cause:

      Although it's an extension method with the name render for JdkByCs, it looks like this extension method

      extension (jdkByCs: JdkByCs) {
      def render: String =
      s"""JdkByCs(version=${jdkByCs.version.render})" // <= this .render fails!
      }

      has precedence over the extension method of the same name for JdkByCs.Version.

    • Solution:

      Revert the render extension method for Scala 3 back to an implicit value class.

What's Changed

New Features

  • [extras-render] Add a new implementation of Render for Scala 3 using Scala 3 syntax (#543)

    • Get rid of implicit syntax
      • Use using and summon.
      • Use type-class syntax.
      • Use extension method syntax.
    • Use opaque type.
  • [extras-render] Provide an optional instance of cats.Contravariant[Render] (#545)

    • The instance of cats.Contravariant[Render] is available only if the cats library is included in the project. Without it, attempting to use or access the instance of the cats.Contravariant[Render] type class causes a "compile-time" error.
    • Although Render has a contramap extension method, it’s still a good idea to provide an instance of cats.Contravariant. Since I don’t want extras-render to depend on Cats by default, I want to make it optional using sbt’s Optional dependency feature.

Internal Housekeeping

  • Upgrade hedgehog and hedgehog-extra (#549)
    • hedgehog to 0.10.1
    • hedgehog-extra to 0.11.0

What's Changed

Internal Housekeeping

  • Upgrade effectie to 2.0.0 (#526)
  • Upgrade effectie to 2.0.0-beta14 (#508)

New Features

  • Add Scala.js support (#522)

What's Changed

2023

New Features

  • [extras-string] Add extras.strings.syntax.all to have all strings syntax (#441)
    import extras.strings.syntax.all._
  • [extras-string] Add case conversion - string.toPascalCase (#443)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toPascalCase
    // Abc
    "AbcDefGhi".toPascalCase
    // AbcDefGhi
    "abcDefGhi".toPascalCase
    // AbcDefGhi
    "abc".toPascalCase
    // Abc
    "ABC".toPascalCase
    // Abc
    "abc_def_ghi".toPascalCase
    // AbcDefGhi
    "abc-def-ghi".toPascalCase
    // AbcDefGhi
    "abc def ghi".toPascalCase
    // AbcDefGhi
  • [extras-string] Add case conversion - string.toOnePascalCase (#455)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toOnePascalCase
    // Abc
    "AbcDef".toOnePascalCase
    // Abcdef
    "abcDef".toOnePascalCase
    // Abcdef
    "abc".toOnePascalCase
    // Abc
    "ABC".toOnePascalCase
    // Abc
  • [extras-string] Add case conversion - string.toCamelCase (#444)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toCamelCase
    // abc
    "AbcDefGhi".toCamelCase
    // abcDefGhi
    "abcDefGhi".toCamelCase
    // abcDefGhi
    "abc".toCamelCase
    // abc
    "ABC".toCamelCase
    // abc
    "abc_def_ghi".toCamelCase
    // abcDefGhi
    "abc-def-ghi".toCamelCase
    // abcDefGhi
    "abc def ghi".toCamelCase
    // abcDefGhi
  • [extras-string] Add case conversion - string.toSnakeCase (#445)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toSnakeCase
    // Abc
    "AbcDefGhi".toSnakeCase
    // Abc_Def_Ghi
    "abcDefGhi".toSnakeCase
    // abc_Def_Ghi
    "abc".toSnakeCase
    // abc
    "ABC".toSnakeCase
    // ABC
    "Abc_Def_Ghi".toSnakeCase
    // Abc_Def_Ghi
    "abc_def_ghi".toSnakeCase
    // abc_def_ghi
    "ABC_DEF_GHI".toSnakeCase
    // ABC_DEF_GHI
    "abc-def-ghi".toSnakeCase
    // abc_def_ghi
    "Abc-Def-Ghi".toSnakeCase
    // Abc_Def_Ghi
    "ABC-DEF-GHI".toSnakeCase
    // ABC_DEF_GHI
    "abc def ghi".toSnakeCase
    // abc_def_ghi
    "Abc Def Ghi".toSnakeCase
    // Abc_Def_Ghi
    "ABC DEF GHI".toSnakeCase
    // ABC_DEF_GHI
  • [extras-string] Add case conversion - string.toSnakeUpperCase (#465)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toSnakeUpperCase
    // ABC
    "AbcDefGhi".toSnakeUpperCase
    // ABC_DEF_GHI
    "abcDefGhi".toSnakeUpperCase
    // ABC_DEF_GHI
    "abc".toSnakeUpperCase
    // ABC
    "ABC".toSnakeUpperCase
    // ABC
    "Abc_Def_Ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "abc_def_ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "ABC_DEF_GHI".toSnakeUpperCase
    // ABC_DEF_GHI
    "abc-def-ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "Abc-Def-Ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "ABC-DEF-GHI".toSnakeUpperCase
    // ABC_DEF_GHI
    "abc def ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "Abc Def Ghi".toSnakeUpperCase
    // ABC_DEF_GHI
    "ABC DEF GHI".toSnakeUpperCase
    // ABC_DEF_GHI
  • [extras-string] Add case conversion - string.toSnakeLowerCase (#466)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toSnakeLowerCase
    // abc
    "AbcDefGhi".toSnakeLowerCase
    // abc_def_ghi
    "abcDefGhi".toSnakeLowerCase
    // abc_def_ghi
    "abc".toSnakeLowerCase
    // abc
    "ABC".toSnakeLowerCase
    // abc
    "Abc_Def_Ghi".toSnakeLowerCase
    // abc_def_ghi
    "abc_def_ghi".toSnakeLowerCase
    // abc_def_ghi
    "ABC_DEF_GHI".toSnakeLowerCase
    // abc_def_ghi
    "abc-def-ghi".toSnakeLowerCase
    // abc_def_ghi
    "Abc-Def-Ghi".toSnakeLowerCase
    // abc_def_ghi
    "ABC-DEF-GHI".toSnakeLowerCase
    // abc_def_ghi
    "abc def ghi".toSnakeLowerCase
    // abc_def_ghi
    "Abc Def Ghi".toSnakeLowerCase
    // abc_def_ghi
    "ABC DEF GHI".toSnakeLowerCase
    // abc_def_ghi
  • [extras-string] Add case conversion - string.toKebabCase (#446)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toKebabCase
    // Abc
    "AbcDefGhi".toKebabCase
    // Abc-Def-Ghi
    "abcDefGhi".toKebabCase
    // abc-Def-Ghi
    "abc".toKebabCase
    // abc
    "ABC".toKebabCase
    // ABC
    "Abc_Def_Ghi".toKebabCase
    // Abc-Def-Ghi
    "abc_def_ghi".toKebabCase
    // abc-def-ghi
    "ABC_DEF_GHI".toKebabCase
    // ABC-DEF-GHI
    "abc-def-ghi".toKebabCase
    // abc-def-ghi
    "Abc-Def-Ghi".toKebabCase
    // Abc-Def-Ghi
    "ABC-DEF-GHI".toKebabCase
    // ABC-DEF-GHI
    "abc def ghi".toKebabCase
    // abc-def-ghi
    "Abc Def Ghi".toKebabCase
    // Abc-Def-Ghi
    "ABC DEF GHI".toKebabCase
    // ABC-DEF-GHI
  • [extras-string] Add case conversion - string.toKebabUpperCase (#467)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toKebabUpperCase
    // ABC
    "AbcDefGhi".toKebabUpperCase
    // ABC-DEF-GHI
    "abcDefGhi".toKebabUpperCase
    // ABC-DEF-GHI
    "abc".toKebabUpperCase
    // ABC
    "ABC".toKebabUpperCase
    // ABC
    "Abc_Def_Ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "abc_def_ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "ABC_DEF_GHI".toKebabUpperCase
    // ABC-DEF-GHI
    "abc-def-ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "Abc-Def-Ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "ABC-DEF-GHI".toKebabUpperCase
    // ABC-DEF-GHI
    "abc def ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "Abc Def Ghi".toKebabUpperCase
    // ABC-DEF-GHI
    "ABC DEF GHI".toKebabUpperCase
    // ABC-DEF-GHI
  • [extras-string] Add case conversion - string.toKebabLowerCase (#468)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "Abc".toKebabLowerCase
    // abc
    "AbcDefGhi".toKebabLowerCase
    // abc-def-ghi
    "abcDefGhi".toKebabLowerCase
    // abc-def-ghi
    "abc".toKebabLowerCase
    // abc
    "ABC".toKebabLowerCase
    // abc
    "Abc_Def_Ghi".toKebabLowerCase
    // abc-def-ghi
    "abc_def_ghi".toKebabLowerCase
    // abc-def-ghi
    "ABC_DEF_GHI".toKebabLowerCase
    // abc-def-ghi
    "abc-def-ghi".toKebabLowerCase
    // abc-def-ghi
    "Abc-Def-Ghi".toKebabLowerCase
    // abc-def-ghi
    "ABC-DEF-GHI".toKebabLowerCase
    // abc-def-ghi
    "abc def ghi".toKebabLowerCase
    // abc-def-ghi
    "Abc Def Ghi".toKebabLowerCase
    // abc-def-ghi
    "ABC DEF GHI".toKebabLowerCase
    // abc-def-ghi
  • [extras-string] Add case conversion - Seq[String].mkPascalCaseString (#447)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkPascalCaseString
    // AbcDef
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkPascalCaseString
    // AbcDefGhiJklMnoPqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkPascalCaseString
    // AbcDefGhiJklMnoPqr
    List("abc").mkPascalCaseString
    // Abc
    List("ABC").mkPascalCaseString
    // Abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkPascalCaseString
    // AbcDefGhiJklMnoPqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkPascalCaseString
    // AbcDefGhiJklMnoPqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
    List("Abc Def Ghi", "jkl mno Pqr").mkPascalCaseString
    // AbcDefGhiJklMnoPqr
    List("abc def ghi", "jkl mno pqr", "st u").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkPascalCaseString
    // AbcDefGhiJklMnoPqrStU
  • [extras-string] Add case conversion - Seq[String].mkCamelCaseString (#448)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkCamelCaseString
    // abcDef
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkCamelCaseString
    // abcDefGhiJklMnoPqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkCamelCaseString
    // abcDefGhiJklMnoPqr
    List("abc").mkCamelCaseString
    // abc
    List("ABC").mkCamelCaseString
    // abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkCamelCaseString
    // abcDefGhiJklMnoPqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkCamelCaseString
    // abcDefGhiJklMnoPqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
    List("Abc Def Ghi", "jkl mno Pqr").mkCamelCaseString
    // abcDefGhiJklMnoPqr
    List("abc def ghi", "jkl mno pqr", "st u").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkCamelCaseString
    // abcDefGhiJklMnoPqrStU
  • [extras-string] Add case conversion - Seq[String].mkSnakeCaseString (#449)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkSnakeCaseString
    // Abc_Def
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkSnakeCaseString
    // Abc_Def_Ghi_jkl_Mno_Pqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkSnakeCaseString
    // abc_Def_Ghi_jkl_Mno_Pqr
    List("abc").mkSnakeCaseString
    // Abc
    List("ABC").mkSnakeCaseString
    // Abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkSnakeCaseString
    // Abc_Def_Ghi_jkl_mno_Pqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkSnakeCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkSnakeCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkSnakeCaseString
    // Abc_Def_Ghi_jkl_mno_Pqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkSnakeCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkSnakeCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("Abc Def Ghi", "jkl mno Pqr").mkSnakeCaseString
    // Abc_Def_Ghi_jkl_mno_Pqr
    List("abc def ghi", "jkl mno pqr", "st u").mkSnakeCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkSnakeCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
  • [extras-string] Add case conversion - Seq[String].mkSnakeUpperCaseString (#478)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkSnakeUpperCaseString
    // ABC_DEF
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR
    List("abc").mkSnakeUpperCaseString
    // ABC
    List("ABC").mkSnakeUpperCaseString
    // ABC
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("Abc Def Ghi", "jkl mno Pqr").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR
    List("abc def ghi", "jkl mno pqr", "st u").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkSnakeUpperCaseString
    // ABC_DEF_GHI_JKL_MNO_PQR_ST_U
  • [extras-string] Add case conversion - Seq[String].mkSnakeLowerCaseString (#479)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkSnakeLowerCaseString
    // abc_def
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr
    List("abc").mkSnakeLowerCaseString
    // abc
    List("ABC").mkSnakeLowerCaseString
    // abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("Abc Def Ghi", "jkl mno Pqr").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr
    List("abc def ghi", "jkl mno pqr", "st u").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkSnakeLowerCaseString
    // abc_def_ghi_jkl_mno_pqr_st_u
  • [extras-string] Add case conversion - Seq[String].mkKebabCaseString (#450)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkKebabCaseString
    // Abc-Def
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkKebabCaseString
    // Abc-Def-Ghi-jkl-Mno-Pqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkKebabCaseString
    // abc-Def-Ghi-jkl-Mno-Pqr
    List("abc").mkKebabCaseString
    // Abc
    List("ABC").mkKebabCaseString
    // Abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkKebabCaseString
    // Abc-Def-Ghi-jkl-mno-Pqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkKebabCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkKebabCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkKebabCaseString
    // Abc-Def-Ghi-jkl-mno-Pqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkKebabCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkKebabCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("Abc Def Ghi", "jkl mno Pqr").mkKebabCaseString
    // Abc-Def-Ghi-jkl-mno-Pqr
    List("abc def ghi", "jkl mno pqr", "st u").mkKebabCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkKebabCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
  • [extras-string] Add case conversion - Seq[String].mkKebabUpperCaseString (#480)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkKebabUpperCaseString
    // ABC-DEF
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR
    List("abc").mkKebabUpperCaseString
    // ABC
    List("ABC").mkKebabUpperCaseString
    // ABC
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("Abc Def Ghi", "jkl mno Pqr").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR
    List("abc def ghi", "jkl mno pqr", "st u").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkKebabUpperCaseString
    // ABC-DEF-GHI-JKL-MNO-PQR-ST-U
  • [extras-string] Add case conversion - Seq[String].mkKebabLowerCaseString (#481)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    List("Abc", "Def").mkKebabLowerCaseString
    // abc-def
    List("AbcDef", "Ghi", "jkl", "MnoPqr").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr
    List("abcDef", "Ghi", "jkl", "MnoPqr").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr
    List("abc").mkKebabLowerCaseString
    // abc
    List("ABC").mkKebabLowerCaseString
    // abc
    List("Abc_Def_Ghi", "jkl_mno_Pqr").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr
    List("abc_def_ghi", "jkl_mno_pqr", "st_u").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC_DEF_GHI", "JKL_MNO_PQR", "ST_U").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("Abc-Def-Ghi", "jkl-mno-Pqr").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr
    List("abc-def-ghi", "jkl-mno-pqr", "st-u").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC-DEF-GHI", "JKL-MNO-PQR", "ST-U").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("Abc Def Ghi", "jkl mno Pqr").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr
    List("abc def ghi", "jkl mno pqr", "st u").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
    List("ABC DEF GHI", "JKL MNO PQR", "ST U").mkKebabLowerCaseString
    // abc-def-ghi-jkl-mno-pqr-st-u
  • [extras-string] Add String.splitByCase (#451)
    import extras.strings.syntax.cases._
    // or
    import extras.strings.syntax.all._
    "AbcDefGhijklMnop".splitByCase
    results in
    Vector("Abc", "Def", "Ghijkl", "Mnop")
    "abcDefGhijklMnop".splitByCase
    results in
    Vector("abc", "Def", "Ghijkl", "Mnop")

What's Changed

Internal Housekeeping

  • Upgrade effectie to 2.0.0-beta13 (#438)

What's Changed

Internal Housekeeping

  • Upgrade effectie to 2.0.0-beta12 (#433)

What's Changed

New Features

  • Add extras-string module (#406)

  • [extras-string] Add syntax for Int and add toOrdinal (#408)

    1.toOrdinal
    // String = 1st
    2.toOrdinal
    // String = 2nd
    3.toOrdinal
    // String = 3rd
    4.toOrdinal
    // String = 4th
    ...
    (1 to 30).map(_.toOrdinal).foreach(println(_))
    1st
    2nd
    3rd
    4th
    5th
    6th
    7th
    8th
    9th
    10th
    11th
    12th
    13th
    14th
    15th
    16th
    17th
    18th
    19th
    20th
    21st
    22nd
    23rd
    24th
    25th
    26th
    27th
    28th
    29th
    30th
  • [extras-string] Add syntax for Long and add toOrdinal (#409)

    1L.toOrdinal
    // String = 1st
    2L.toOrdinal
    // String = 2nd
    3L.toOrdinal
    // String = 3rd
    4L.toOrdinal
    // String = 4th
    ...
    (1L to 30L).map(_.toOrdinal).foreach(println(_))
    1st
    2nd
    3rd
    4th
    5th
    6th
    7th
    8th
    9th
    10th
    11th
    12th
    13th
    14th
    15th
    16th
    17th
    18th
    19th
    20th
    21st
    22nd
    23rd
    24th
    25th
    26th
    27th
    28th
    29th
    30th
  • [extras-string] Add commaWith syntax for Seq[String] (#419)

    An extension method for a Seq[String], providing a way to join the elements with a comma and a given conjunction.

    It joins String values with commas and uses the given conjunction before the last element.

    List.empty[String].commaWith("BLAH")
    // String = ""

    List("").commaWith("BLAH")
    // String = ""

    List("aaa").commaWith("BLAH")
    // String = "aaa"

    List("aaa", "bbb").commaWith("BLAH")
    // String = "aaa BLAH bbb"

    List("aaa", "bbb", "ccc").commaWith("BLAH")
    // String = "aaa, bbb BLAH ccc"

    List("aaa", "bbb", "ccc", "ddd").commaWith("BLAH")
    // String = "aaa, bbb, ccc BLAH ddd"
  • [extras-string] Add serialCommaWith syntax for Seq[String] (#420)

    An extension method for a Seq[String], providing a way to join the elements with a serial comma and a given conjunction.

    It joins String values with commas and uses the given conjunction before the last element.

    This method employs the serial comma (also known as the Oxford comma), which means it always inserts a comma before the conjunction unless there are only two elements.

    List.empty[String].serialCommaWith("BLAH")
    // String = ""

    List("").serialCommaWith("BLAH")
    // String = ""

    List("aaa").serialCommaWith("BLAH")
    // String = "aaa"

    List("aaa", "bbb").serialCommaWith("BLAH")
    // String = "aaa BLAH bbb"

    List("aaa", "bbb", "ccc").serialCommaWith("BLAH")
    // String = "aaa, bbb, BLAH ccc"

    List("aaa", "bbb", "ccc", "ddd").serialCommaWith("BLAH")
    // String = "aaa, bbb, ccc, BLAH ddd"
  • [extras-string] Add commaAnd syntax for Seq[String] (#412)

    Format Seq[String] into a human-readable list using comma and the conjunction and. It separates elements by commas and uses the term and before the last element.

    List.empty[String].commaAnd
    // String = ""

    List("").commaAnd
    // String = ""

    List("aaa").commaAnd
    // String = "aaa"

    List("aaa", "bbb").commaAnd
    // String = "aaa and bbb"

    List("aaa", "bbb", "ccc").commaAnd
    // String = "aaa, bbb and ccc"

    List("aaa", "bbb", "ccc", "ddd").commaAnd
    // String = "aaa, bbb, ccc and ddd"
  • [extras-string] Add serialCommaAnd syntax for Seq[String] (#414)

    Format Seq[String] into a human-readable list using comma and the conjunction and.

    It separates elements by commas and uses the term and before the last element following the "Oxford comma" style.

    e.g.) "aaa, bbb, and ccc"

    List.empty[String].serialCommaAnd
    // String = ""

    List("").serialCommaAnd
    // String = ""

    List("aaa").serialCommaAnd
    // String = "aaa"

    List("aaa", "bbb").serialCommaAnd
    // String = "aaa and bbb"

    List("aaa", "bbb", "ccc").serialCommaAnd
    // String = "aaa, bbb, and ccc"

    List("aaa", "bbb", "ccc", "ddd").serialCommaAnd
    // String = "aaa, bbb, ccc, and ddd"
  • [extras-string] Add commaOr syntax for Seq[String] (#416)

    Format Seq[String] into a human-readable list using comma and the conjunction or.

    It separates elements by commas and uses the term or before the last element.

    List.empty[String].commaOr
    // String = ""

    List("").commaOr
    // String = ""

    List("aaa").commaOr
    // String = "aaa"

    List("aaa", "bbb").commaOr
    // String = "aaa or bbb"

    List("aaa", "bbb", "ccc").commaOr
    // String = "aaa, bbb or ccc"

    List("aaa", "bbb", "ccc", "ddd").commaOr
    // String = "aaa, bbb, ccc or ddd"
  • [extras-string] Add serialCommaOr syntax for Seq[String] (#417)

    Format Seq[String] into a human-readable list using comma and the conjunction or.

    It separates elements by commas and uses the term or before the last element following the "Oxford comma" style.

    e.g.) "aaa, bbb, or ccc"

    List.empty[String].serialCommaOr
    // String = ""

    List("").serialCommaOr
    // String = ""

    List("aaa").serialCommaOr
    // String = "aaa"

    List("aaa", "bbb").serialCommaOr
    // String = "aaa or bbb"

    List("aaa", "bbb", "ccc").serialCommaOr
    // String = "aaa, bbb, or ccc"

    List("aaa", "bbb", "ccc", "ddd").serialCommaOr
    // String = "aaa, bbb, ccc, or ddd"

What's Changed

Internal Housekeeping

  • Upgrade sbt to 1.9.3 (#399)
  • Upgrade effectie to 2.0.0-beta11 (#402)

What's Changed

New Feature

  • [extras-cats] Add innerForeach and innerForeachF to OptionSyntax (#384)
  • [extras-cats] Add innerForeach and innerForeachF to EitherSyntax (#386)

What's Changed

New Feature

  • Add newtype support for doobie (#378)

Internal Housekeeping

  • Upgrade effectie to 2.0.0-beta9 (#376)

What's Changed

Changes

  • [extras-cats] Fix innerFold and innerFoldF for F[Either[A, B]] to have the same method signature as Either's (#368)
    • innerFold

      def innerFold[D](ifLeft: => D)(f: B => D)(implicit F: Functor[F]): F[D]

      to

      def innerFold[D](forLeft: A => D)(forRight: B => D)(implicit F: Functor[F]): F[D]
    • innerFoldF

      def innerFoldF[D](ifLeft: => F[D])(f: B => F[D])(implicit F: FlatMap[F]): F[D]

      to

      def innerFoldF[D](forLeft: A => F[D])(forRight: B => F[D])(implicit F: FlatMap[F]): F[D]

What's Changed

New Feature

extras-cats
  • Add innerMap extension method to F[Option[A]] (#348)
    val foa: F[Option[A]] = ...
    foa.innerMap(A => B) // F[Option[B]]
    val foa: IO[Option[Int]] = IO.pure(1)
    // IO[Option[Int]] = IO(Some(1))
    foa.innerMap(_ + 999)
    // IO[Option[Int]] = IO(Some(1000))

  • Add innerFlatMap extension method to F[Option[A]] (#349)
    val foa: F[Option[A]] = ...
    foa.innerFlatMap(A => Option[B]) // F[Option[B]]
    val foa: IO[Option[Int]] = IO.pure(1)
    // IO[Option[Int]] = IO(Some(1))
    foa.innerFlatMap((_ + 999).some)
    // IO[Option[Int]] = IO(Some(1000))

  • Add innerFlatMapF extension method to F[Option[A]] (#352)
    val foa: F[Option[A]] = ...
    foa.innerFlatMapF(A => F[Option[B]]) // F[Option[B]]
    val foa: IO[Option[Int]] = IO.pure(1)
    // IO[Option[Int]] = IO(Some(1))
    foa.innerFlatMapF(a => IO.pure((a + 999).some))
    // IO[Option[Int]] = IO(Some(1000))

  • innerGetOrElse, innerGetOrElseF, innerFold and innerFoldF to extension methods to F[Option[A]] (#354)
    • innerGetOrElse

      val foa: F[Option[A]] = ...
      foa.innerGetOrElse[B >: A](=> B) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(999)
      // IO[Option[Int]] = IO(Some(999))
      foa.innerGetOrElse(0)
      // IO[Option[Int]] = IO(Some(1000))

      val foa2: IO[Option[Int]] = IO.pure(Option.empty[Int])
      // IO[Option[Int]] = None
      foa2.innerGetOrElse(0)
      // IO[Option[Int]] = IO(Some(0))
    • innerGetOrElseF

      val foa: F[Option[A]] = ...
      foa.innerGetOrElseF[B >: A](=> F[B]) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(999)
      // IO[Option[Int]] = IO(Some(999))
      foa.innerGetOrElseF(IO.pure(0))
      // IO[Option[Int]] = IO(Some(1000))

      val foa2: IO[Option[Int]] = IO.pure(Option.empty[Int])
      // IO[Option[Int]] = None
      foa2.innerGetOrElseF(IO.pure(0))
      // IO[Option[Int]] = IO(Some(0))
    • innerFold

      val foa: F[Option[A]] = ...
      foa.innerFold[B](=> B)(A => B) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(1)
      // IO[Option[Int]] = IO(Some(1))
      foa.innerFold(0)(_ + 999)
      // IO[Option[Int]] = IO(Some(1000))

      val foa2: IO[Option[Int]] = IO.pure(Option.empty[Int])
      // IO[Option[Int]] = None
      foa2.innerFold(0)(_ + 999)
      // IO[Option[Int]] = IO(Some(0))
    • innerFoldF

      val foa: F[Option[A]] = ...
      foa.innerFoldF[B >: A](=> F[B]) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(1)
      // IO[Option[Int]] = IO(Some(1))
      foa.innerFoldF(IO.pure(0))(a => IO.pure(a + 999))
      // IO[Option[Int]] = IO(Some(1000))

      val foa2: IO[Option[Int]] = IO.pure(Option.empty[Int])
      // IO[Option[Int]] = None
      foa2.innerFoldF(IO.pure(0))(a => IO.pure(a + 999))
      // IO[Option[Int]] = IO(Some(0))

  • Add innerMap, innerFlatMap, innerFlatMapF, innerGetOrElse, innerGetOrElseF, innerFold and innerFoldF extension methods to F[Either[A, B]] (#356)
    • innerMap

      val feab: F[Either[A, B]] = ...
      feab.innerMap(B => D) // F[Either[A, D]]
      val feab: F[Either[String, Int]] = IO.pure(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerMap(_ + 999)
      // IO[Either[String, Int]] = IO(Right(1000))
    • innerFlatMap

      val feab: F[Either[A, B]] = ...
      feab.innerFlatMap(B => Either[A, D]) // F[Either[A, D]]
      val feab: F[Either[String, Int]] = IO.pure(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFlatMap(b => (b + 999).asRight[String])
      // IO[Either[String, Int]] = IO(Right(1000))
    • innerFlatMapF

      val feab: F[Either[A, B]] = ...
      feab.innerFlatMapF(B => F[Either[A, D]]) // F[Either[A, D]]
      val feab: F[Either[String, Int]] = IO.pure(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFlatMapF(b => IO.pure((b + 999).asRight[String]))
      // IO[Either[String, Int]] = IO(Right(1000))
    • innerGetOrElse

      val feab: F[Either[A, B]] = ...
      feab.innerGetOrElse[D >: B](=> D) // F[D]
      val feab: F[Either[String, Int]] = IO.pure(Right(999))
      // IO[Either[String, Int]] = IO(Right(999))
      feab.innerGetOrElse(0)
      // IO[Int] = IO(999)

      val feab2: F[Either[String, Int]] = IO.pure("Error".asLeft[Int])
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerGetOrElse(0)
      // IO[Int] = IO(0)
    • innerGetOrElseF

      val feab: F[Either[A, B]] = ...
      feab.innerGetOrElseF[D >: B](=> F[D]) // F[D]
      val feab: F[Either[String, Int]] = IO.pure(Right(999))
      // IO[Either[String, Int]] = IO(Right(999))
      feab.innerGetOrElseF(IO.pure(0))
      // IO[Either[String, Int]] = IO(Right(999))

      val feab2: F[Either[String, Int]] = IO.pure("Error".asLeft[Int])
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerGetOrElseF(IO.pure(0))
      // IO[Int] = IO(0)
    • innerFold

      val feab: F[Either[A, B]] = ...
      feab.innerFold[D](=> D)(B => D) // F[D]
      val feab: F[Either[String, Int]] = IO.pure(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFold(0)(_ + 999)
      // IO[Int] = IO(1000)

      val feab: F[Either[String, Int]] = IO.pure("Error".asLeft)
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab.innerFold(0)(_ + 999)
      // IO[Int] = IO(0)
    • innerFoldF

      val feab: F[Either[A, B]] = ...
      feab.innerFoldF[D](=> F[D])(B => F[D]) // F[D]
      val feab: F[Either[String, Int]] = IO.pure(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFoldF(IO.pure(0))(b => IO.pure(b + 999))
      // IO[Int] = IO(1000)

      val feab: F[Either[String, Int]] = IO.pure("Error".asLeft)
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab.innerFoldF(IO.pure(0))(b => IO.pure(b + 999))
      // IO[Int] = IO(0)

  • Add innerLeftMap, innerLeftFlatMap and innerLeftFlatMapF extension methods to F[Either[A, B]] (#358)

    • innerLeftMap

      val feab: F[Either[A, B]] = ...
      feab.innerLeftMap(A => C) // F[Either[C, B]]
      val feab: F[Either[String, Int]] = IO.pure(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab.innerLeftMap("Failed: " + _)
      // IO[Either[String, Int]] = IO(Left("Failed: Error"))
    • innerLeftFlatMap

      val feab: F[Either[A, B]] = ...
      feab.innerFlatMap(A => Either[C, B]) // F[Either[C, B]]
      val feab: F[Either[String, Int]] = IO.pure(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab.innerFlatMap(a => ("Failed: " + a).asLeft[Int])
      // IO[Either[String, Int]] = IO(Left("Failed: Error"))
    • innerLeftFlatMapF

      val feab: F[Either[A, B]] = ...
      feab.innerFlatMapF(A => F[Either[C, B]]) // F[Either[C, B]]
      val feab: F[Either[String, Int]] = IO.pure(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab.innerFlatMapF(a => IO.pure(("Failed: " + a).asLeft[Int]))
      // IO[Either[String, Int]] = IO(Left("Failed: Error"))
  • Add innerFilter, innerExists, innerContains, innerForall, innerCollect, innerOrElse and innerOrElseF extension methods to F[Option[A]] (#360)

    • innerFilter

      val foa: F[Option[A]] = ...
      foa.innerFilter(A => Boolean) // F[Option[A]]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerFilter(_ > 0)
      // IO[Option[Int]] = IO(Some(1))
      foa.innerFilter(_ > 1)
      // IO[Option[Int]] = IO(None)
    • innerExists

      val foa: F[Option[A]] = ...
      foa.innerExists(A => Boolean) // F[Boolean]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerExists(_ > 0)
      // IO[Boolean] = IO(true)
      foa.innerExists(_ > 1)
      // IO[Boolean] = IO(false)
    • innerContains

      val foa: F[Option[A]] = ...
      foa.innerContains(A) // F[Boolean]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerContains(1)
      // IO[Boolean] = IO(true)
      foa.innerContains(0)
      // IO[Boolean] = IO(false)
    • innerForall

      val foa: F[Option[A]] = ...
      foa.innerForall(A) // F[Boolean]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerForall(_ > 0)
      // IO[Boolean] = IO(true)
      foa.innerForall(_ > 1)
      // IO[Boolean] = IO(false)

      val foa2: IO[Option[Int]] = IO.pure(None)
      // IO[Option[Int]] = IO(None)
      foa2.innerForall(_ > 1)
      // IO[Boolean] = IO(true)
    • innerCollect

      val foa: F[Option[A]] = ...
      foa.innerCollect(PartialFunction[A, B]) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerCollect {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(0)

      val foa2: IO[Option[Int]] = IO.pure(Some(2))
      // IO[Option[Int]] = IO(2)
      foa.innerCollect {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(999)

      val foa3: IO[Option[Int]] = IO.pure(Some(3))
      // IO[Option[Int]] = IO(3)
      foa.innerCollect {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(None)
    • innerOrElse

      val foa: F[Option[A]] = ...
      foa.innerOrElse[B >: A](Option[B]) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerOrElse(Some(0))
      // IO[Option[Int]] = IO(Some(1))

      val foa2: IO[Option[Int]] = IO.pure(None)
      // IO[Option[Int]] = IO(None)
      foa2.innerOrElse(Some(0))
      // IO[Option[Int]] = IO(Some(0))
    • innerOrElseF

      val foa: F[Option[A]] = ...
      foa.innerOrElse[B >: A](F[Option[B]]) // F[Option[B]]
      val foa: IO[Option[Int]] = IO.pure(Some(1))
      // IO[Option[Int]] = IO(Some(1))
      foa.innerOrElse(IO.pure(Some(0)))
      // IO[Option[Int]] = IO(Some(1))

      val foa2: IO[Option[Int]] = IO.pure(None)
      // IO[Option[Int]] = IO(None)
      foa2.innerOrElse(IO.pure(Some(0)))
      // IO[Option[Int]] = IO(Some(0))
  • Add innerFind, innerFilterOrElse, innerExists, innerForall, innerContains, innerCollectFirst, innerOrElse and innerOrElseF extension methods to F[Either[A, B]] (#362)

    • innerFind

      val feab: F[Either[A, B]] = ...
      feab.innerFind(B => Boolean): F[Option[B]]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFind(_ > 0)
      // IO[Option[Int]] = IO(Some(1))
      feab.innerFind(_ > 1)
      // IO[Option[Int]] = IO(None)
    • innerFilterOrElse

      val feab: F[Either[A, B]] = ...
      feab.innerFilterOrElse[C >: A](B => Boolean, C): F[Either[C, B]]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFilterOrElse(_ > 0, "Error")
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerFilterOrElse(_ > 1, "Error")
      // IO[Either[String, Int]] = IO(Left("Error"))
    • innerExists

      val feab: F[Either[A, B]] = ...
      feab.innerExists(B => Boolean): F[Boolean]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerExists(_ > 0)
      // IO[Boolean] = IO(true)
      feab.innerExists(_ > 1)
      // IO[Boolean] = IO(false)
    • innerForall

      val feab: F[Either[A, B]] = ...
      feab.innerForall(B => Boolean): F[Boolean]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerForall(_ > 0)
      // IO[Boolean] = IO(true)
      feab.innerForall(_ > 1)
      // IO[Boolean] = IO(false)

      val feab2: IO[Either[String, Int]] = IO(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerForall(_ > 1)
      // IO[Boolean] = IO(true)
    • innerContains

      val feab: F[Either[A, B]] = ...
      feab.innerContains(B): F[Boolean]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerContains(1)
      // IO[Boolean] = IO(true)
      feab.innerContains(0)
      // IO[Boolean] = IO(false)

      val feab2: IO[Either[String, Int]] = IO(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerContains(1)
      // IO[Boolean] = IO(false)
    • innerCollectFirst

      val feab: F[Either[A, B]] = ...
      feab.innerCollectFirst[D](PartialFunction[B, D]): F[Option[D]]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerCollectFirst {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(Some(0))

      val feab2: IO[Either[String, Int]] = IO(Right(2))
      // IO[Either[String, Int]] = IO(Right(2))
      feab2.innerCollectFirst {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(Some(999))

      val feab3: IO[Either[String, Int]] = IO(Right(3))
      // IO[Either[String, Int]] = IO(Right(3))
      feab3.innerCollectFirst {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(None)

      val feab4: IO[Either[String, Int]] = IO(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab4.innerCollectFirst {
      case 1 => 0
      case 2 => 999
      }
      // IO[Option[Int]] = IO(None)
    • innerOrElse

      val feab: F[Either[A, B]] = ...
      feab.innerOrElse[C >: A, D >: B](=> Either[C, D]): F[Either[C, D]]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerOrElse(0.asRight[String])
      // IO[Option[Int]] = IO(Some(1))

      val feab2: IO[Either[String, Int]] = IO(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerOrElse(0.asRight[String])
      // IO[Option[Int]] = IO(Some(0))
    • innerOrElseF

      val feab: F[Either[A, B]] = ...
      feab.innerOrElseF[C >: A, D >: B](=> F[Either[C, D]]): F[Either[C, D]]
      val feab: IO[Either[String, Int]] = IO(Right(1))
      // IO[Either[String, Int]] = IO(Right(1))
      feab.innerOrElseF(IO.pure(0.asRight[String]))
      // IO[Option[Int]] = IO(Some(1))

      val feab2: IO[Either[String, Int]] = IO(Left("Error"))
      // IO[Either[String, Int]] = IO(Left("Error"))
      feab2.innerOrElseF(IO.pure(0.asRight[String]))
      // IO[Option[Int]] = IO(Some(0))
  • [extras-cats] Add tests for all new extension methods in AllSyntax inherited from OptionSyntax and EitherSyntax (#366)

What's Changed

New Feature

  • [extras-render] Add contramap to Render (#344)
    import java.util.UUID
    import extras.render.Render

    final case class Id(value: UUID) extends AnyVal
    object Id {
    implicit val idRender: Render[Id] = Render[UUID].contramap(_.value)
    }

What's Changed

New Features

  • [extras-circe] Add renameFields for Encoder to rename circe Json fields (#331)

    final case class Something(a: Int, b: String, price: BigDecimal, c: Boolean)
    object Something {
    import extras.circe.codecs.encoder._
    implicit val somethingEncoder: Encoder[Something] =
    deriveEncoder[Something].renameFields(
    "a" -> "productNumber",
    "b" -> "name",
    "c" -> "inStock",
    )
    }

    import io.circe.syntax._
    Something(1, "Vibranium Shield", BidDecimal(9999999), false).asJson.spaces2
    {
    "id": 1,
    "name": "Vibranium Shield",
    "price": 9999999,
    "inStock": false
    }
    • [extras-circe] renameFields for Encoder should fail encoding when new name conflicts with the existing name (#335)
      final case class Something(n: Int, s: String, name: String)
      object Something {

      import extras.circe.codecs.encoder._

      implicit val somethingEncoder: Encoder[Something] =
      deriveEncoder[Something].renameFields(
      "n" -> "productNumber",
      "s" -> "name", // Something already has a field named "name"
      )
      }

      io.circe.syntax._
      Something(1, "some name", "another name").asJson
      // This fails with extras.circe.codecs.encoder.NamingConflictError
  • [extras-circe] Add renameFields for Decoder to rename circe Json fields (#333)

    final case class Something(a: Int, b: String, price: BigDecimal, c: Boolean)
    object Something {
    import extras.circe.codecs.decoder._

    implicit val somethingDecoder: Decoder[Something] =
    deriveDecoder[Something].renameFields(
    "a" -> "productNumber",
    "b" -> "name",
    "c" -> "inStock",
    )
    }
    val json = """{
    "productNumber": 1,
    "name": "Vibranium Shield",
    "price": 9999999,
    "inStock": false
    }"""

    import io.circe.parser._
    decode[Something](json)
    // Either[Error, Something] = Right(Something(a = 1, b = "Vibranium Shield", price = 9999999, c = false))
  • [extras-circe] Add renameFields for Codec to rename circe Json fields (#336)

    final case class Something(a: Int, b: String, price: BigDecimal, c: Boolean)
    object Something {
    import extras.circe.codecs.codec._
    implicit val somethingEncoder: Codec[Something] =
    deriveCodec[Something].renameFields(
    "a" -> "productNumber",
    "b" -> "name",
    "c" -> "inStock",
    )
    }

    val something = Something(1, "Vibranium Shield", BidDecimal(9999999), false)

    Encoding:

    import io.circe.syntax._
    something.asJson.spaces2
    {
    "id": 1,
    "name": "Vibranium Shield",
    "price": 9999999,
    "inStock": false
    }

    Decoding:

    val json = """{
    "productNumber": 1,
    "name": "Vibranium Shield",
    "price": 9999999,
    "inStock": false
    }"""

    import io.circe.parser._
    decode[Something](json)
    // Either[Error, Something] = Right(Something(a = 1, b = "Vibranium Shield", price = 9999999, c = false))

Improvement

  • Optimize StubToolsCats and StubToolsFx (#329)

What's Changed

Internal Housekeeping

  • Upgrade effectie to 2.0.0-beta7 (#322)
  • Upgrade sbt-plugins (#324)
    • sbt-scalafmt to 2.5.0
      • scalafmt to 3.7.2
    • sbt-scoverage to 2.0.7
    • sbt-mdoc to 2.3.7
    • sbt-tpolecat to 0.4.2
    • scalafix-rules to 0.2.15
    • sbt-scalajs to 1.13.0

What's Changed

New Feature

  • Add extras-testing-tools-effectie with StubToolsFx (#319)
extras-testing-tools-effectie
import cats._
import effectie.core._

trait TestType[F[*]] {
def foo(n: Int): F[Int]
def bar(n: Int): F[Int]
}

object TestTypeStub {
def apply[F[*]: Fx: Monad](f4Foo: => Option[Int => Int], f4Bar: => Option[Int => F[Int]]): TestType[F] =
new TestType[F] {

override def foo(n: Int): F[Int] =
StubToolsFx.stub(f4Foo).map(_(n))

override def bar(n: Int): F[Int] =
StubToolsFx.stub(f4Bar).flatMap(_(n))
}
}

If the stub function is missing, it will be F containing MissingStubException saying where the missing stub is.

For example, the following code

import effectie.instances.ce2.fx._

val testType = TestTypeStub[IO](none, f.some)

testType.foo(n).catchNonFatalThrowable // IO[Either[Throwable, Int]]

results in IO[Either[Throwable, Int]] where Either is Left(MissingStubException) with the information like the following.

>> Missing Stub implementation at
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:16)
>> ---
>> Details:
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:16)
at cats.ApplicativeError.fromOption(ApplicativeError.scala:318)
at cats.ApplicativeError.fromOption$(ApplicativeError.scala:315)
at cats.effect.IOLowPriorityInstances$IOEffect.fromOption(IO.scala:865)
at extras.testing.StubToolsFx$StubToolsFxPartiallyApplied$.$anonfun$apply$1(StubToolsFx.scala:24)
at cats.effect.internals.IORunLoop$.step(IORunLoop.scala:319)
at cats.effect.IO.unsafeRunTimed(IO.scala:338)
at cats.effect.IO.unsafeRunSync(IO.scala:256)
at extras.testing.StubToolsFxSpec$.$anonfun$testStubIntToIntWithIOFailureCase$1(StubToolsFxSpec.scala:79)
at extras.testing.StubToolsFxSpec$.$anonfun$testStubIntToIntWithIOFailureCase$1$adapted(StubToolsFxSpec.scala:71)
at scala.Option.map(Option.scala:242)
at hedgehog.core.PropertyT.$anonfun$map$1(PropertyT.scala:64)
at scala.Option.map(Option.scala:242)
at hedgehog.core.GenT.$anonfun$map$2(GenT.scala:12)
at hedgehog.core.TreeImplicits1$$anon$1.map(Tree.scala:35)
at hedgehog.core.TreeImplicits1$$anon$1.map(Tree.scala:33)
at hedgehog.core.Tree.map(Tree.scala:16)
at hedgehog.core.GenT.$anonfun$map$1(GenT.scala:12)
at hedgehog.core.PropertyTReporting.loop$1(PropertyT.scala:205)
at hedgehog.core.PropertyTReporting.report(PropertyT.scala:231)
at hedgehog.core.PropertyTReporting.report$(PropertyT.scala:182)
at hedgehog.package$$anon$2.report(package.scala:32)
at hedgehog.PropertyTOps.check(Property.scala:38)
at hedgehog.PropertyTOps.check$(Property.scala:37)
at hedgehog.package$Property$.check(package.scala:19)
at hedgehog.runner.Properties.$anonfun$main$1(Properties.scala:19)
at hedgehog.runner.Properties.$anonfun$main$1$adapted(Properties.scala:18)
at scala.collection.immutable.List.foreach(List.scala:333)
at hedgehog.runner.Properties.main(Properties.scala:18)
at extras.testing.StubToolsFxSpec.main(StubToolsFxSpec.scala)

What's Changed

New Feature

  • Add [extras-testing-tools] and [extras-testing-tools-cats] with tools to use stub (#307)
extras-testing-tools
import extras.testing.StubTools

trait TestType {
def foo(n: Int): Int
}

object TestTypeStub {
def apply(f: => Option[Int => Int]): TestType = new TestType {
override def foo(n: Int): Int =
f.fold[Int](throw StubTools.missing)(_(n))
}
}

If the stub function is missing, it will throw a MissingStubException saying where the missing stub is. For example,

TestTypeStub(None)

results in

>> Missing Stub implementation at
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:11)
>> ---
>> Details:
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:11)
at scala.Option.fold(Option.scala:263)
at extras.testing.TestTypeStub$$anon$1.foo(TestType.scala:11)
at extras.testing.StubToolsSpec$.$anonfun$testMissingMissingCase$2(StubToolsSpec.scala:33)
...
extras-testing-tools-cats
import cats._
import cats.syntax.all._
import extras.testing.StubToolsCats

trait TestType[F[*]] {
def foo(n: Int): F[Int]
def bar(n: Int): F[Int]
}

object TestTypeStub {
def apply[F[*]: MonadThrow](f4Foo: => Option[Int => Int], f4Bar: => Option[Int => F[Int]]): TestType[F] =
new TestType[F] {

override def foo(n: Int): F[Int] =
StubToolsCats.stub(f4Foo).map(_(n))

override def bar(n: Int): F[Int] =
StubToolsCats.stub(f4Bar).flatMap(_(n))
}
}

If the stub function is missing, it will be F containing MissingStubException saying where the missing stub is.

For example, the following code

val testType = TestTypeStub[IO](none, f.some)

testType.foo(n).attempt // Either[Throwable, Int]

results in IO[Either[Throwable, Int]] where Either is Left(MissingStubException) with the information like the following.

>> Missing Stub implementation at
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:16)
>> ---
>> Details:
>> extras.testing.TestTypeStub$$anon$1.$anonfun$foo$1(TestType.scala:16)
at cats.ApplicativeError.fromOption(ApplicativeError.scala:318)
at cats.ApplicativeError.fromOption$(ApplicativeError.scala:315)
at cats.effect.IOLowPriorityInstances$IOEffect.fromOption(IO.scala:865)
at extras.testing.StubToolsCats$StubToolsCatsPartiallyApplied$.$anonfun$apply$1(StubToolsCats.scala:24)
...

Internal Housekeeping

  • Upgrade effectie to 2.0.0-beta5 (#310)

What's Changed

Changes

  • [extras-fs2-v2-text][extras-fs2-v3-text]: Replace the implicit param, Sync, for utf8String from extras.fs2.text.syntax with Compiler (#304)

    So the following method

    def utf8String(implicit sync: Sync[F]): F[String]

    has been changed to

    def utf8String(implicit compiler: Compiler[F, F]): F[String]

    fs2 v2: fs2.Stream.Compiler / fs2 v3: fs2.Compiler

What's Changed

New Features

  • [extras-circe]: Add an extension method to circe Encoder to add more fields (#291)
    import io.circe._
    import io.circe.generic.semiauto._

    final case class Something(n: Int) {
    def name: String = "Blah"
    }
    object Something {
    implicit val somethingEncoder: Encoder[Something] = deriveEncoder
    }

    Something(1).asJson.spaces2
    results in
    {
    "n": 1
    }
    There should be an easy way to add the name field to have JSON like
    {
    "n": 1,
    "name": "Blah"
    }
    For instance,
    import extras.circe.codecs.encoder._

    object Something {
    implicit val somethingEncoder: Encoder[Something] =
    deriveEncoder.withFields { something =>
    List("name" -> something.name.asJson)
    }
    }
  • [extras-fs2-v2-text][extras-fs2-v3-text]: Add fs2 syntax to compile Stream[F, Byte] to F[String] (#295)
    import fs2.Stream
    import extras.fs2.text.syntax._

    val stream: Stream[F, Byte] = ...
    stream.utf8String // F[String]
    import org.http4s.Response
    import extras.fs2.text.syntax._

    val response: Response[F] = ...
    response.body.utf8String // F[String]

What's Changed

2022

New Feature

  • [extras-render-refined] Add newtype + refined support (#277)

    import eu.timepit.refined.types.string.NonEmptyString
    import extras.render.Render
    import io.estatico.newtype.macros.newtype

    import extras.render.refined._

    @newtype case class Name(value: NonEmptyString)
    object Name {
    implicit val nameRender: Render[Name] = deriving
    }

    instead of

    @newtype case class Name(value: NonEmptyString)
    object Name {
    implicit val nameRender: Render[Name] = Render.render(_.value.value)
    }
  • [extras-doobie-tools] Add doobie tools to run SQL query (#283)

    • extras-doobie-tools-ce2
    • extras-doobie-tools-ce3

    This is meant to be used for testing.

    e.g.)

    DbTools.fetchSingleRow[F][Example](
    sql"""
    SELECT id, name, note
    FROM db_tools_test.example
    """
    )(transactor) // F[Option[Example]]

    DbTools.fetchMultipleRows[F][Example](
    sql"""
    SELECT id, name, note
    FROM db_tools_test.example
    """
    )(transactor) // F[List[Example]]

    DbTools.updateSingle[F](
    sql"""
    INSERT INTO db_tools_test.example (id, name, note) VALUES (${example.id}, ${example.name}, ${example.note})
    """
    )(transactor) // F[Int]

    // val examples: List[Example] = ???
    DbTools.updateMultiple[F][Example](
    "INSERT INTO db_tools_test.example (id, name, note) VALUES (?, ?, ?)"
    )(examples)(transactor) // F[Int]

    with

    implicit val writeExample: Write[Example] =
    Write[(Int, String, String)].contramap(example =>
    (example.id.value, example.name.value, example.note.value)
    )

    implicit val readExample: Read[Example] =
    Read[(Int, String, String)].map {
    case (id, name, note) =>
    Example(Example.Id(id), Example.Name(name), Example.Note(note))
    }

    Or in Scala 3,

    given readExample: Read[Example] =
    Read[(Int, String, String)].map {
    case (id, name, note) =>
    Example(Example.Id(id), Example.Name(name), Example.Note(note))
    }

    given writeExample: Write[Example] =
    Write[(Int, String, String)].contramap(example =>
    (example.id.value, example.name.value, example.note.value)
    )

Scala + Library Version Up

  • Bump Scala / bump libraries (#284)
    • Scala 2.12.13 => 2.12.17
    • Scala 2.13.6 => 2.13.10
    • Scala 3.0.0 => 3.1.3
    • Cats 2.6.1 and 2.7.0 => 2.8.0
    • Cats Effect 2.5.4 => 2.5.5
    • Cats Effect 3.2.9 => 3.3.14

What's Changed

New Feature

extras-type-info
  • [extras-type-info] Release extras-type-info (#273)
    sealed trait Aaa
    object Aaa {
    final case class Bbb(n: Int) extends Aaa
    case object Ccc extends Aaa

    def bbb(n: Int): Aaa = Bbb(n)
    def ccc: Aaa = Ccc
    }

    final case class Something[A](a: A)

    Aaa.bbb(123).nestedClassName
    // Aaa.Bbb

    Aaa.ccc.nestedTypeName
    // Aaa.Ccc

It was supposed to be released in 0.24.0 but wasn't included by mistake.

What's Changed

New Features

extras-type-info
  • [extras-type-info] Add extras-type-info module to replace extras-reflects with Scala 3 support (#264)
    sealed trait Aaa
    object Aaa {
    final case class Bbb(n: Int) extends Aaa
    case object Ccc extends Aaa

    def bbb(n: Int): Aaa = Bbb(n)
    def ccc: Aaa = Ccc
    }

    final case class Something[A](a: A)

    Aaa.bbb(123).nestedClassName
    // Aaa.Bbb

    Aaa.ccc.nestedTypeName
    // Aaa.Ccc
extras-hedgehog-circe
  • [extras-hedgehog-circe] Add round-trip test support for circe (#263)
    object SomethingSpec extends Properties {
    override def tests: List[Test] = List(
    property("round-trip test Codec[Something]", roundTripTestCodecSomething)
    )

    def roundTripTestCodecSomething: Property =
    for {
    something <- genSomething.log("something")
    } yield {
    RoundTripTester(something)
    .indent(2)
    .test()
    }

    def genSomething: Gen[Something] =
    for {
    id <- Gen.int(Range.linear(1, 100))
    name <- Gen.string(Gen.unicode, Range.linear(3, 20))
    } yield Something(id, name)

    final case class Something(id: Int, name: String)
    object Something {
    implicit val somethingShow: Show[Something] =
    something => s"Something(id = ${something.id.toString}, name = ${something.name})"

    implicit val somethingCodec: Codec[Something] = io.circe.generic.semiauto.deriveCodec
    }
    }
  • [extras-hedgehog-circe] Use extras-type-info to support Scala 3 (#270)
    • Scala 2 version uses extras-type-info as well, but extras-type-info internally uses WeakTypeTag from scala.reflect.runtime.universe in scala-reflect, an additional reflection API library, to get the type info.
    • Scala 3 version of extras-type-info uses scala.reflect.ClassTag, scala.compiletime and scala.quoted to get the type info.

What's Changed

Package Rename

  • Rename extras.render.syntax.render => extras.render.syntax (#252)

New Features

  • [extras-render] Add String interpolation support for Render (#253)
    import extras.render.Render
    import extras.render.syntax._

    final case class Foo(id: Int, name: String)

    object Foo {
    implicit val fooRender: Render[Foo] =
    foo => s"{ID=${foo.id.toString},Name=${foo.name}}"
    }

    val foo1 = Foo(1, "A")
    val foo2 = Foo(2, "B")
    val foo3 = Foo(3, "C")

    println(render"$foo1 > $foo2 >> $foo3")
    // {ID=1,Name=A} > {ID=2,Name=B} >> {ID=3,Name=C}
  • [extras-render] Add Render.render[A](A => String): Render[A] (#256)
    import extras.render.Render

    final case class Something(value: String)
    object Something {
    implicit val renderSomething: Render[Something] =
    Render.render(s => s"value=${s.value}")
    }

    val foo = Something("Blah")
    Render[Something].render(foo)
    // String = "value=Blah"

What's Changed

Fixed

  • Fix Iterable[A].renderString renders only the first element (#243)

What's Changed

Supported Scala Version

  • Drop support for Scala 2.11 (#236)

New Features

  • [extras-core] Add Render[A] type-class (#234)
  • [extras-core] Add syntax for Render[A] type-class (#238)

Internal Housekeeping

  • Add sbt-dependency-submission GitHub workflow (#231)

What's Changed

Artifact Name Change

  • Rename extras-hedgehog-cats-effect3 to extras-hedgehog-ce3 (#215)

New Features

  • [extras-hedgehog-ce3] Add support for test returning IO[Result] (#216)
  • [extras-hedgehog-ce3] Rename withIO to runIO in CatsEffectRunner (#221)
    def testSomething: Property =
    for {
    n <- Gen.int(Range.linear(Int.MinValue, Int.MaxValue)).log("n")
    } yield runIO { // withIO: (=> IO[Result]) => Result
    // Manual implicit Ticker creation is no longer required.

    val expected = n
    val actual = IO(n)

    actual.map(_ ==== expected) // IO[Result]
    }
  • [extras-hedgehog-ce3] Add withIO with implicit ticker => (#223)
    def testSomething: Property =
    for {
    n <- Gen.int(Range.linear(Int.MinValue, Int.MaxValue)).log("n")
    } yield withIO { implicit ticker =>

    val expected = n
    val actual = IO(n)

    actual.completeAs(expected) // Result
    }

Internal Housekeeping

  • Bump Scala.js (#219)
    • sbt-scalajs to 1.11.0
    • sbt-scalajs-crossproject to 1.2.0

What's Changed

New Features

  • [extras-refinement] Add NonEmptyString concatenation syntax (#208)
    import eu.timepit.refined.types.string.NonEmptyString
    import extras.refinement.syntax.string._

    NonEmptyString("abc") ++ NonEmptyString("def")
    // NonEmptyString("abcdef")

    val givenName = NonEmptyString("Kevin")
    val surname = NonEmptyString("Lee")
    val fullName = givenName ++ NonEmptyString(" ") ++ surname
    // NonEmptyString("Kevin Lee")
  • [extras-refinement] Add all syntax to refinement (#210)
    import extras.refinement.syntax.all._
    is equivalent to
    import extras.refinement.syntax.refinement._
    import extras.refinement.syntax.string._

Internal Housekeeping

  • Upgrade docs site to Docusaurus 2.0.0-rc.1 (#195)
  • Upgrade docs site to Docusaurus 2.0.1 (#206)
  • [docs] Add Fira Code Nerd Font and CaskaydiaCove Nerd Font, and use FiraCode Nerd Font for code blocks (#203)
  • Upgrade hedgehog to 0.9.0 (#199)
  • Replace lunr search with Algolia DocSearch (#193)
  • Remove -XX:+UseJVMCICompiler from .jvmopts (#191)

What's Changed

Done

  • [extras-scala-io] Add Rainbow.rainbows(Seq[String]) (#186)
  • [extras-scala-io] Add syntax: Seq[String].rainbowed (#188)

What's Changed

Done

  • [extras-core] Add syntax to encode String to escaped unicode chars (#172)
  • [extras-scala-io] extras.scala.io.syntax.color "".colored(Color) should return "" (#170)
  • [extras-scala-io] Add color and colored to Color (#175)
  • [extras-scala-io] Rgb.color("") and Rgb.colored("") should return "" (#176)
  • [extras-scala-io] "".rgb and "".rgbed should return "" (#177)
  • [extras-scala-io] Rainbow.rainbow("") and Rainbow.rainbowHtml("") should return "" (#180)
  • [extras-scala-io] "".rainbowed and "".rainbowedHtml should return "" (#181)

What's Changed

Done

  • [extras-scala-io] Add Rgb to have true color support (#162)
    Rgb.fromInt(0x000000) // Right(Rgb(0))
    Rgb.fromInt(0xff0000) // Right(Rgb(16711680))
    Rgb.fromInt(0x00ff00) // Right(Rgb(65280))
    Rgb.fromInt(0x0000ff) // Right(Rgb(255))
    Rgb.fromInt(0xffffff) // Right(Rgb(16777215))
    // and more smart constructors and extension methods to get ASCII escape chars and HTML colors (hex)
  • [extras-scala-io] Add a way to paint String with rainbow 🌈 colors (#161) Screen Shot 2022-06-18 at 7 00 16 pm

What's Changed

Done

  • [extras-scala-io] Add more String.color to color syntax (#158)

What's Changed

Done

  • Add toValue for newtype containing refined type to get the underlying value (#149)

What's Changed

Done

  • [extras-scala-io] Change file.ops to file.syntax (#137)
  • [extras-scala-io] Change getAllFiles to listAllFilesRecursively (#138)
  • [extras-scala-io] Change deleteFileRecursively to deleteAllRecursively (#139)

What's Changed

Done

  • [extras-scala-io] Add a method to delete all files in a directory and its sub-directories (#131)
  • [extras-scala-io] Add: Run with temporary directory - runWithTempDir (#133)

What's Changed

Done

  • [extras-reflects] Add A.nestedTypeName syntax (#125)

What's Changed

Done

  • More syntax for extras-reflects (#119)

What's Changed

Done

  • extras-refinement: Downgrade refined to 0.9.27 (#110)

What's Changed

Done

  • Add syntax to support newtype and refined (#101)
    • extras-reflects and extras-refinement have been added.

What's Changed

Done

  • Set up Codecov in GitHub Actions (#83)
  • Add Scalafix check (#92)
  • Support Scala.js (#95)
  • Move all sub-projects to modules (#97)

What's Changed

2021

Done

  • Add a way to test IO[A] - IO[A].completeThen(A => Result): Result (#71)
  • Add a way to test IO[A] with error - IO[A].errorThen(Throwable => Result): Result (#73)
  • CatsEffectRunner.IoOps.expectError should take Eq and Show for the expected error (#75)
  • Publish to s01.oss.sonatype.org (the new Maven central) (#67)

What's Changed

Done

  • Add extras-hedgehog-cats-effect3 to provide cats-effect3 test kit for hedgehog (#58)
  • Add core project to keep all common stuff (#62)

What's Changed

Done

  • Create the doc site (#23)(#34)
  • Add extras-scala-io (#24)
    • Add typed version of scala.io.AnsiColor to extras-scala-io (#26)
    • ColorSyntax for typed AnsiColor (#27)
    • CanClose[A] type class as an alternative to AutoCloseable (#28)
  • Redesign extras-concurrent-testing APIs (#36)
  • Add a shorter name method for eitherT (#39)
  • Add syntax for testing Future (#41)

What's Changed

Done

  • Add additional syntax for EitherT to extras-cats (#8)
  • Add additional syntax for OptionT to extras-cats (#14)
  • Add all syntax to extras-cats (#19)
  • Set up GitHub Actions build and release (#4)
  • Change project structure (#9)
  • Tools for Java and Scala concurrent (#11)

What's Changed