Skip to content

Commit

Permalink
Merge pull request #312 from AnthonyGrod/core-refactor
Browse files Browse the repository at this point in the history
Core refactor
  • Loading branch information
pk044 authored Mar 14, 2024
2 parents 097f281 + 97df748 commit c9d4f4f
Show file tree
Hide file tree
Showing 276 changed files with 2,281 additions and 1,842 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [2.12.18, 2.13.11, 3.3.0]
scala: [2.13.11, 3.3.0]
java: [temurin@17]
runs-on: ${{ matrix.os }}
steps:
Expand Down
1 change: 0 additions & 1 deletion .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ pull_request_rules:
conditions:
- base=master
- author=scala-steward
- status-success=Build and Test (ubuntu-latest, 2.12.18, temurin@17)
- status-success=Build and Test (ubuntu-latest, 2.13.11, temurin@17)
- status-success=Build and Test (ubuntu-latest, 3.3.0, temurin@17)
actions:
Expand Down
66 changes: 49 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ A library maintained by [Iterators](https://www.iteratorshq.com).
* [JsonSchema support](#jsonschema-support)
* [Scalacheck support](#scalacheck-support)
* [Kebs for IntelliJ](#kebs-for-intellij)
* [Kebs 2.0 migration guide](#kebs-20-migration-guide)

### Why?

Expand Down Expand Up @@ -171,7 +172,7 @@ class People(tag: Tag) extends Table[Person](tag, "people") {
If you prefer to **mix in trait** instead of import (for example you're using a custom driver like `slick-pg`), you can do it as well:

```scala
import pl.iterators.kebs.Kebs
import pl.iterators.kebs.slick.Kebs
object MyPostgresProfile extends ExPostgresDriver with PgArraySupport {
override val api: API = new API {}
trait API extends super.API with ArrayImplicits with Kebs
Expand Down Expand Up @@ -338,8 +339,8 @@ class People(tag: Tag) extends Table[Person](tag, "people") {

```scala

import pl.iterators.kebs._
import enums._
import pl.iterators.kebs.slick._
import pl.iterators.kebs.slick.enums._

class People(tag: Tag) extends Table[Person](tag, "people") {

Expand All @@ -361,8 +362,8 @@ If you import `enums.lowercase._` or `enums.uppercase._` then it'll save enum na
Of course, enums also work with traits:

```scala
import pl.iterators.kebs.Kebs
import pl.iterators.kebs.enums.KebsEnums
import pl.iterators.kebs.slick.Kebs
import pl.iterators.kebs.slick.enums.KebsEnums

object MyPostgresProfile extends ExPostgresDriver {
override val api: API = new API {}
Expand All @@ -389,12 +390,12 @@ import MyPostgresProfile.api._

kebs-doobie works similarly to [kebs-slick](#--kebs-generates-slick-mappers-for-your-case-class-wrappers-kebs-slick). It provides doobie's `Meta` instances for:

* Instances of `CaseClass1Rep` (value classes, tagged types, opaque types)
* Instances of `ValueClassLike` (value classes, tagged types, opaque types)
* Instances of `InstanceConverter`
* Enumeratum for Scala 2
* Native enums for Scala 3

To make the magic happen, do `import pl.iterators.kebs._` and `import pl.iterators.kebs.enums._` (or `import pl.iterators.kebs.enums.uppercase._` or `import pl.iterators.kebs.enums.lowercase._`).
To make the magic happen, do `import pl.iterators.doobie.kebs._` and `import pl.iterators.kebs.doobie.enums._` (or `import pl.iterators.kebs.doobie.enums.uppercase._` or `import pl.iterators.kebs.doobie.enums.lowercase._`).

#### - kebs eliminates spray-json induced boilerplate (kebs-spray-json)

Expand Down Expand Up @@ -722,8 +723,8 @@ case class Limit(value: Int) extends AnyVal

case class PaginationQuery(sortBy: Column, sortOrder: SortOrder, offset: Offset, limit: Limit)

import pl.iterators.kebs.unmarshallers._
import enums._
import pl.iterators.kebs.akkahttp.unmarshallers._
import pl.iterators.kebs.akkahttp.unmarshallers.enums._

val route = get {
parameters('sortBy.as[Column], 'order.as[SortOrder] ? (SortOrder.Desc: SortOrder), 'offset.as[Offset] ? Offset(0), 'limit.as[Limit])
Expand Down Expand Up @@ -901,13 +902,13 @@ object Tags {
}

object PositiveIntTag {
implicit val PositiveIntCaseClass1Rep = new CaseClass1Rep[PositiveInt, Int](PositiveInt.apply(_), identity)
implicit val PositiveIntValueClassLike = new ValueClassLike[PositiveInt, Int](PositiveInt.apply(_), identity)
}
object IdTag {
implicit def IdCaseClass1Rep[A] = new CaseClass1Rep[Id[A], Int](Id.apply(_), identity)
implicit def IdValueClassLike[A] = new ValueClassLike[Id[A], Int](Id.apply(_), identity)
}
object NameTag {
implicit val NameCaseClass1Rep = new CaseClass1Rep[Name, String](Name.apply(_), identity)
implicit val NameValueClassLike = new ValueClassLike[Name, String](Name.apply(_), identity)
}
}
```
Expand Down Expand Up @@ -946,15 +947,15 @@ There are some conventions that are assumed during generation.
* take a single argument
* return Either (this is not enforced though - you'll have a compilation error later)

Also, `CaseClass1Rep` is generated for each tag meaning you will get a lot of `kebs` machinery for free eg. spray formats etc.
Also, `ValueClassLike` is generated for each tag meaning you will get a lot of `kebs` machinery for free eg. spray formats etc.

### Opaque types

As an alternative to tagged types, Scala 3 provides [opaque types](https://docs.scala-lang.org/scala3/reference/other-new-features/opaques.html).
The principles of opaque types are similar to tagged type. The basic usage of opaque types requires the
same amount of boilerplate as tagged types - e.g. you have to write smart constructors, validations and unwrapping
mechanisms all by hand. `kebs-opaque` is meant to help with that by generating a handful of methods and providing a
`CaseClass1Rep` for an easy typclass derivation.
`ValueClassLike` for an easy typclass derivation.

```scala
import pl.iterators.kebs.opaque._
Expand All @@ -966,10 +967,10 @@ object MyDomain {
```

That's the basic usage. Inside the companion object you will get methods like `from`, `apply`, `unsafe` and extension
method `unwrap` plus an instance of `CaseClass1Rep[ISBN, String]`. A more complete example below.
method `unwrap` plus an instance of `ValueClassLike[ISBN, String]`. A more complete example below.

```scala
import pl.iterators.kebs.macros.CaseClass1Rep
import pl.iterators.kebs.core.macros.ValueClassLike
import pl.iterators.kebs.opaque._

object MyDomain {
Expand Down Expand Up @@ -1000,7 +1001,7 @@ trait Showable[A] {
def show(a: A): String
}
given Showable[String] = (a: String) => a
given[S, A](using showable: Showable[S], cc1Rep: CaseClass1Rep[A, S]): Showable[A] = (a: A) => showable.show(cc1Rep.unapply(a))
given[S, A](using showable: Showable[S], vcLike: ValueClassLike[A, S]): Showable[A] = (a: A) => showable.show(vcLike.unapply(a))
implicitly[Showable[ISBN]].show(ISBN("1234567890")) // "1234567890"
```

Expand Down Expand Up @@ -1105,3 +1106,34 @@ The code generated by macros in `kebs-tagged-meta` is not visible to IntelliJ ID
plugin that enhances experience with the library by adding support for generated code. You can install it from the IntelliJ Marketplace.
In the Settings/Preferences dialog, select "Plugins" and type "Kebs" into search input (see https://www.jetbrains.com/help/idea/managing-plugins.html for detailed instructions).
You can also use this web page: https://plugins.jetbrains.com/plugin/16069-kebs.

### Kebs 2.0 migration guide

Please be aware that recent changes in the source code might require some changes in your codebase. Follow the guide below to migrate your code to Kebs 2.0:
* If you are using value classes instead of tagged/opaque types, please mix in the `CaseClass1ToValueClass` trait.
* Extend your value-enums with `pl.iterators.kebs.enums.ValueEnumLikeEntry` parameterized with the type of the value.
* Native Scala 3 value-enums:
```scala
enum ColorButRGB(val value: Int) extends ValueEnumLikeEntry[Int] {slick
case Red extends ColorButRGB(0xFF0000)
case Green extends ColorButRGB(0x00FF00)
case Blue extends ColorButRGB(0x0000FF)
}
```
* enumeratum value-enums for Scala 2 and Scala 3:
```scala
sealed abstract class LibraryItem(val value: Int) extends IntEnumEntry with ValueEnumLikeEntry[Int]
object LibraryItem extends IntEnum[LibraryItem] {
case object Book extends LibraryItem(value = 1)
case object Movie extends LibraryItem(value = 2)
case object Magazine extends LibraryItem(3)
case object CD extends LibraryItem(4)
val values = findValues
}
```
* Extend your traits/classes/objects, if inside of one an implicit enum (or value-enum) conversion for `kebs` library's needs should occur, with one of the following traits:
* For Scala 2 and Scala 3 enums from `enumeratum` library: `pl.iterators.kebs.enumeratum.KebsEnumeratum`
* For Scala 2 and Scala 3 value-enums from `enumeratum` library: `pl.iterators.kebs.enumeratum.KebsValueEnumeratum`
* For Scala 3 native value-enums: `pl.iterators.kebs.enums.KebsValueEnum`
* For Scala 2 `scala.Enumeration` enums or Scala 3 native enums: `pl.iterators.kebs.enums.KebsEnum`

Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package pl.iterators.kebs.matchers
package pl.iterators.kebs.akkahttp.matchers

import akka.http.scaladsl.server.{PathMatcher1, PathMatchers}
import enumeratum.{Enum, EnumEntry}
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.macros.CaseClass1Rep
import pl.iterators.kebs.core.instances.InstanceConverter
import pl.iterators.kebs.core.macros.ValueClassLike

import scala.language.implicitConversions

trait KebsMatchers extends PathMatchers {

implicit class SegmentIsomorphism[U](segment: PathMatcher1[U]) {
def as[T](implicit rep: CaseClass1Rep[T, U]): PathMatcher1[T] = segment.map(rep.apply)
def as[T](implicit rep: ValueClassLike[T, U]): PathMatcher1[T] = segment.map(rep.apply)
}

implicit class SegmentConversion[Source](segment: PathMatcher1[Source]) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package pl.iterators.kebs
package pl.iterators.kebs.akkahttp

package object matchers extends KebsMatchers
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package pl.iterators.kebs.unmarshallers
package pl.iterators.kebs.akkahttp.unmarshallers

import akka.http.scaladsl.unmarshalling.{FromStringUnmarshaller, Unmarshaller}
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.macros.CaseClass1Rep
import pl.iterators.kebs.core.instances.InstanceConverter
import pl.iterators.kebs.core.macros.{CaseClass1ToValueClass, ValueClassLike}

trait KebsUnmarshallers {
implicit def kebsUnmarshaller[A, B](implicit rep: CaseClass1Rep[B, A]): Unmarshaller[A, B] =
trait KebsUnmarshallers extends CaseClass1ToValueClass {
implicit def kebsUnmarshaller[A, B](implicit rep: ValueClassLike[B, A]): Unmarshaller[A, B] =
Unmarshaller.strict[A, B](rep.apply)
@inline
implicit def kebsFromStringUnmarshaller[A, B](implicit rep: CaseClass1Rep[B, A],
implicit def kebsFromStringUnmarshaller[A, B](implicit rep: ValueClassLike[B, A],
fsu: FromStringUnmarshaller[A]): FromStringUnmarshaller[B] =
fsu andThen kebsUnmarshaller(rep)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pl.iterators.kebs.akkahttp.unmarshallers.enums

import akka.http.scaladsl.unmarshalling.PredefinedFromStringUnmarshallers._
import akka.http.scaladsl.unmarshalling.{FromStringUnmarshaller, Unmarshaller}
import akka.http.scaladsl.util.FastFuture
import pl.iterators.kebs.core.enums.{EnumLike, ValueEnumLike, ValueEnumLikeEntry}

trait EnumUnmarshallers {
final def enumUnmarshaller[E](`enum`: EnumLike[E]): FromStringUnmarshaller[E] = Unmarshaller { _ =>name =>
`enum`.withNameInsensitiveOption(name) match {
case Some(enumEntry) => FastFuture.successful(enumEntry)
case None =>
FastFuture.failed(new IllegalArgumentException(s"""Invalid value '$name'. Expected one of: ${`enum`.getNamesToValuesMap.keysIterator
.mkString(", ")}"""))
}
}

implicit def kebsEnumUnmarshaller[E](implicit ev: EnumLike[E]): FromStringUnmarshaller[E] =
enumUnmarshaller(ev)
}

trait ValueEnumUnmarshallers {
final def valueEnumUnmarshaller[V, E <: ValueEnumLikeEntry[V]](`enum`: ValueEnumLike[V, E]): Unmarshaller[V, E] = Unmarshaller { _ =>v =>
`enum`.withValueOption(v) match {
case Some(enumEntry) => FastFuture.successful(enumEntry)
case None =>
FastFuture.failed(new IllegalArgumentException(s"""Invalid value '$v'. Expected one of: ${`enum`.getValuesToEntriesMap.keysIterator
.mkString(", ")}"""))
}
}

implicit def kebsValueEnumUnmarshaller[V, E <: ValueEnumLikeEntry[V]](implicit ev: ValueEnumLike[V, E]): Unmarshaller[V, E] =
valueEnumUnmarshaller(ev)

implicit def kebsIntValueEnumFromStringUnmarshaller[E <: ValueEnumLikeEntry[Int]](implicit ev: ValueEnumLike[Int, E]): FromStringUnmarshaller[E] =
intFromStringUnmarshaller andThen valueEnumUnmarshaller(ev)
implicit def kebsLongValueEnumFromStringUnmarshaller[E <: ValueEnumLikeEntry[Long]](implicit ev: ValueEnumLike[Long, E]): FromStringUnmarshaller[E] =
longFromStringUnmarshaller andThen valueEnumUnmarshaller(ev)
implicit def kebsShortValueEnumFromStringUnmarshaller[E <: ValueEnumLikeEntry[Short]](
implicit ev: ValueEnumLike[Short, E]): FromStringUnmarshaller[E] =
shortFromStringUnmarshaller andThen valueEnumUnmarshaller(ev)
implicit def kebsByteValueEnumFromStringUnmarshaller[E <: ValueEnumLikeEntry[Byte]](implicit ev: ValueEnumLike[Byte, E]): FromStringUnmarshaller[E] =
byteFromStringUnmarshaller andThen valueEnumUnmarshaller(ev)
}

trait KebsEnumUnmarshallers extends EnumUnmarshallers with ValueEnumUnmarshallers {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package pl.iterators.kebs.unmarshallers
package pl.iterators.kebs.akkahttp.unmarshallers

package object enums extends KebsEnumUnmarshallers
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package pl.iterators.kebs
package pl.iterators.kebs.akkahttp

package object unmarshallers extends KebsUnmarshallers

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package pl.iterators.kebs
package pl.iterators.kebs.akkahttp.domain

import enumeratum.values.{IntEnum, IntEnumEntry, StringEnum, StringEnumEntry}
import enumeratum.{Enum, EnumEntry}
import pl.iterators.kebs.tag.meta.tagged
import pl.iterators.kebs.tagged._
import pl.iterators.kebs.core.enums.ValueEnumLikeEntry

import java.net.URI
import java.util.UUID
Expand Down Expand Up @@ -47,7 +48,7 @@ object Domain extends Tags {
val values = findValues
}

sealed abstract class LibraryItem(val value: Int) extends IntEnumEntry
sealed abstract class LibraryItem(val value: Int) extends IntEnumEntry with ValueEnumLikeEntry[Int]

object LibraryItem extends IntEnum[LibraryItem] {
case object Book extends LibraryItem(1)
Expand All @@ -63,7 +64,7 @@ object Domain extends Tags {
case class Blue(value: Int)
case class Color(red: Red, green: Green, blue: Blue)

sealed abstract class ShirtSize(val value: String) extends StringEnumEntry
sealed abstract class ShirtSize(val value: String) extends StringEnumEntry with ValueEnumLikeEntry[String]
object ShirtSize extends StringEnum[ShirtSize] {
case object Small extends ShirtSize("S")
case object Medium extends ShirtSize("M")
Expand Down
Loading

0 comments on commit c9d4f4f

Please sign in to comment.