lorandszakacs / sprout   0.0.5

Apache License 2.0 GitHub

Opaque type encoding for convenient new types, and boilerplate minimization of integration with 3rd party libraries like circe, http4s, doobie, skunk, etc.

Scala versions: 3.x 2.13 2.12
Scala.js versions: 1.x

sprout

⚠️ sprout is no longer maintained. Use monix-newtypes instead. The encodings and rationale behind the two libraries are nearly identical. Most transitions from sprout to monix-newtypes are search+replaces 🌱

getting started

This library is published for Scala 3.0.0, 2.13, 2.12, both on the JVM, and JS platforms.

sprout

libraryDependencies += "com.lorandszakacs" %% "sprout" % "0.0.5"

Depends on:

snapshots

To fetch snapshots of the library add the following to your build:

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

motivation

An opinionated "newtype" encoding for Scala 3 using opaque types. It exists because in real world projects we keep seeing how it's extremely useful to never have any primitive type show up anywhere in your domain model, we're talking about literally hundreds of "new types", for every little thing. Oftimes, they don't need much more extra validation than their underlying type, so we need an encoding that allows us to easily continue using semi-auto derivation of various other codecs for encodings (circe JSON codecs, doobie Put/Get, http4s query param codecs, config readers, etc.).

what's different?

Why use sprout, and not refined, newtypes, scala-newtype, or just plain old opaque type? Well, nothing revolurionary really. Mostly because it is written and maintained with the intent to provide easy integration with 3rd party libraries via the NewType[OldType, NewType] typeclass. And it allows you to trivially write such integrations in your own projects. Thus giving you "newtypes" that work seemlessly in your own gynormous code base!

TL;DR, if your project needs seemless refinement of types whose values are coming somewhere from the outside (e.g. user input) and compile time awesomeness like that found in refined doesn't help anymore then sprout might just be the lightweigh alternative for you!

example

WIP: section WIP as library evolves. Check tests for full set of examples and features of the library.

Define a new type as simply as this:

import sprout.*
type TestSprout = TestSprout.Type
object TestSprout extends Sprout[String]

val s: TestSprout = TestSprout("a plain string")

If you wish to add cats.Show and cats.Eq typeclasses, then you can extend the appropriate types:

import sprout.*

type TestSprout = TestSprout.Type
object TestSprout extends Sprout[String]
  with SproutEq[String]
  with SproutShow[String]
  with SproutOrder[String]

subtypes

There are also subtyping alternatives where you create a "subtype" of your base type by extending SproutSub[T] e.g:

import sprout.*

type TestSprout = TestSprout.Type
object TestSprout extends SproutSub[String]
  with SproutEq[String]
  with SproutShow[String]
  with SproutOrder[String]
val s: TestSprout = TestSprout("a plain string")
val s2: String = s //this compiles now, as opposed to when extending Sprout[String]

refined types

There is also a trait to add more checks:

private type TestSprout = TestSprout.Type

private object TestSprout
  extends SproutRefinedThrow[String]
  with SproutEq[String]
  with SproutShow[String]
  with SproutOrder[String] {

  override def refine[F[_]: MonadThrow](o: String): F[String] =
    if (o.contains("sprout")) o.pure[F] else new RuntimeException("Invalid sprout string").raiseError[F, String]
}

val errored: IO[TestSprout] = TestSprout[IO]("1")
val succces: IO[TestSprout] = TestSprout[IO]("1-sprout")

scala 2 support

There exists a scala 2 implementation of roughly the same functionality using shapeless.

⚠️ Source compatability is a goal, but not a guarantee. ⚠️

The Scala 2 version exists mostly to use in projects where migration to Scala 3 is on the horizon, but not quite there yet.