extras Scala Libraries

website·Indexed 32 days ago

https://extras.kevinly.dev

A collection of utility libraries for Scala and Scala.js, including extras-core, extras-cats, extras-circe, extras-hedgehog, extras-fs2, extras-string, extras-render, and extras-scala-io. These libraries provide extension methods and tools for handling JSON codecs, functional programming patterns with Cats, property-based testing with Hedgehog, and string/color manipulation.

Tokens
21.8K
Snippets
195
Records
196
Agent score
58%

What's inside extras

  1. Manually create a TestContext for async simulation

    When not using withIO or runIO, you can manually create a TestContext (from cats-effect-kernel-testkit) to simulate async boundaries and time passage. The Ticker class in extras-hedgehog-ce3 acts as a value class for TestContext.
    import cats.effect.kernel.testkit.TestContext
    import extras.hedgehog.ce3.Ticker
    
    // Option 1: Using the helper method
    implicit val ticker: Ticker = Ticker.withNewTestContext()
    
    // Option 2: Wrapping a TestContext instance
    implicit val ticker: Ticker = Ticker(TestContext())
  2. Manually create a TestContext using Ticker

    When not using withIO or runIO, you can manually create a TestContext (from cats-effect-kernel-testkit) using the Ticker value class. This allows for simulating async boundaries and time passage in tests.
    import cats.effect.kernel.testkit.TestContext
    import extras.hedgehog.ce3.Ticker
    
    // Option 1: Using the helper method
    implicit val ticker: Ticker = Ticker.withNewTestContext()
    
    // Option 2: Wrapping a TestContext instance
    implicit val ticker: Ticker = Ticker(TestContext())
  3. Create test stubs using StubToolsCats.stub

    StubToolsCats.stub is a utility for creating simple test stubs without relying on heavy mock frameworks. It allows you to define optional behavior for a service method; if the behavior is provided as Some(f), it is executed. If it is None, the tool raises a MissingStubException containing the exact line number where the stub was missing, avoiding the ambiguity of NullPointerException common in mock frameworks.
    import cats.MonadThrow
    import cats.syntax.all._
    import extras.testing.StubToolsCats
    
    // Example 1: Simple value return
    def fooStub[F[_]: MonadThrow](f: => Option[A]): FooStub[F] = new FooStub[F] {
      def foo: F[A] = StubToolsCats.stub(f)
    }
    
    // Example 2: Function returning a value
    def fooStub[F[_]: MonadThrow](f: => Option[A => B]): FooStub[F] = new FooStub[F] {
      def foo(a: A): F[B] = StubToolsCats.stub(f).map(_(a))
    }
    
    // Example 3: Function returning an effect F[B]
    def fooStub[F[_]: MonadThrow](f: => Option[A => F[B]]): FooStub[F] = new FooStub[F] {
      def foo(a: A): F[B] = StubToolsCats.stub(f).flatMap(_(a))
    }
  4. Concatenate NonEmptyStrings using the ++ operator

    The extras.refinement.syntax.string package provides the ++ operator to concatenate two or more NonEmptyString values. Unlike standard string concatenation (which returns a plain String), the ++ operator ensures the resulting value remains a NonEmptyString, eliminating the need to manually call NonEmptyString.unsafeFrom after concatenation.
    import eu.timepit.refined.types.string.NonEmptyString
    import extras.refinement.syntax.string._
    
    val hello = NonEmptyString("Hello")
    val space = NonEmptyString(" ")
    val world = NonEmptyString("world")
    
    // Result is NonEmptyString, not String
    val greeting = hello ++ space ++ world
    // greeting: NonEmptyString = Hello world
    
    val givenName = NonEmptyString("Kevin")
    val surname = NonEmptyString("Lee")
    val fullName = givenName ++ NonEmptyString(" ") ++ surname
    // fullName: NonEmptyString = Kevin Lee
  5. Derive a Render instance using contramap

    If you have an existing Render[A] and a function to convert B to A (B => A), you can create a Render[B] using the contramap method. This is useful for creating renderers for wrapper types or domain objects by leveraging renderers for their underlying primitive types.
    import extras.render.Render
    import extras.render.syntax._
    
    final case class Id(value: Int) extends AnyVal
    
    /*
     * Get `Render[Id]` from `Render[Int]` using the function `Id => Int` (Id.value)
     */
    implicit val renderId: Render[Id] = 
      Render[Int].contramap(_.value)
    
    // Usage
    Id(123).render
    // res: String = "123"
  6. Create test stubs using StubToolsFx.stub

    StubToolsFx.stub is a utility for creating simple function-based stubs for testing, removing the need for heavy mock frameworks. It accepts an Option containing the stub implementation. If the Option is None, the tool raises a MissingStubException which includes the exact line number where the stub was missing, providing better traceability than a NullPointerException.

    Depending on the required return type and the presence of cats typeclasses (Functor, Monad), StubToolsFx.stub can be used to handle simple values, functions, or effectful computations.

    import cats.{Functor, Monad}
    import cats.syntax.all._
    import effectie.core.Fx
    import extras.testing.StubToolsFx
    
    // 1. Simple value stub
    def fooStub[F[_]: Fx](f: => Option[A]): FooStub[F] = new FooStub[F] {
      def foo: F[A] = StubToolsFx.stub(f)
    }
    
    // 2. Stub returning a function (requires Functor)
    def fooStub[F[_]: Fx: Functor](f: => Option[() => A]): FooStub[F] = new FooStub[F] {
      def foo(): F[A] = StubToolsFx.stub(f).map(_())
    }
    
    // 3. Stub returning a function with arguments (requires Functor)
    def fooStub[F[_]: Fx: Functor](f: => Option[A => B]): FooStub[F] = new FooStub[F] {
      def foo(a: A): F[B] = StubToolsFx.stub(f).map(_(a))
    }
    
    // 4. Stub returning an effectful function (requires Monad)
    def fooStub[F[_]: Fx: Monad](f: => Option[A => F[B]]): FooStub[F] = new FooStub[F] {
      def foo(a: A): F[B] = StubToolsFx.stub(f).flatMap(_(a))
    }
  7. Import extras-refinement syntax extensions

    To use the additional syntax for newtype and refined types, you can import the specific syntax modules or use the all shortcut to import both refinement and string-specific syntaxes.
    // Import all syntaxes (refinement and string)
    import extras.refinement.syntax.all._
    
    // Or import them individually
    import extras.refinement.syntax.refinement._
    import extras.refinement.syntax.string._
  8. Test Circe JSON Encoder and Decoder symmetry with RoundTripTester

    The RoundTripTester utility verifies that a type A can be encoded into a JSON string using an Encoder[A] and successfully decoded back into the original type using a Decoder[A]. It returns a hedgehog.Result indicating whether the round-trip was successful.

    You can optionally call .indent(n) to specify the indentation level of the JSON output used in failure reports.

    import extras.hedgehog.circe.RoundTripTester
    
    // Basic usage
    RoundTripTester(someValue).test()
    
    // Usage with custom indentation for failure output
    RoundTripTester(someValue).indent(8).test()
  9. Derive a Render instance using Cats Contravariant

    For projects using the Cats library, Contravariant[Render] is provided out of the box. This allows you to derive a Render[B] from a Render[A] given a function B => A using the Cats Contravariant typeclass.
    import cats._
    import extras.render.Render
    import extras.render.syntax._
    
    final case class Id(value: Int) extends AnyVal
    
    /*
     * Use Contravariant[Render] to get `Render[Id]` from `Render[Int]` 
     * with the function `Id => Int` (Id.value)
     */
    implicit val renderId: Render[Id] = 
      Contravariant[Render].contramap(Render[Int])(_.value)
    
    // Usage
    Id(123).render
    // res: String = "123"
  10. Execute a block within a temporary directory

    Use extras.scala.io.file.TempFiles.runWithTempDir to create a temporary directory that is automatically deleted after the provided block of code finishes executing. This is particularly useful for file-based unit tests. The method takes a prefix for the directory name and provides a TempDir value class containing the java.io.File instance.
    import extras.scala.io.file.TempFiles
    import java.io.File
    
    TempFiles.runWithTempDir("temporary-dir-prefix") {
      tempDir =>
        val tmpDir = tempDir.value
        val someFile = new File(tmpDir, "myfile.txt")
        someFile.createNewFile()
        "Done"
    }
    // The temporary directory and its contents are deleted after the block returns.
  11. Add extras-hedgehog-ce3 dependency for JVM and Scala.js

    The extras-hedgehog-ce3 library provides helper tools for writing tests using hedgehog and cats-effect 3. Depending on your target platform, add the following dependency to your build configuration.
    // For JVM
    "io.kevinlee" %% "extras-hedgehog-ce3" % "0.56.0" % Test
    
    // For Scala.js
    "io.kevinlee" %%% "extras-hedgehog-ce3" % "0.56.0" % Test