Skip to content

Commit

Permalink
Kebs 2.0 (#349)
Browse files Browse the repository at this point in the history
* Rename CaseClass1Rep to ValueClassLike

* Create ValueEnumLike and EnumLike interfaces

* Implement ValueEnumLike and EnumLike interfaces

* Minor naming fix

* Rename CaseClass1Rep to ValueClassLike also in tests

* Resolve conflict in changes.md

* Introduce Enumeration to EnumLike implicit macro

* Introduce implicit macro Enumeratum to EnumLike converter

* Remove scala3 enum support for testing reasons

* Fixed scala2 Enumeration support bug

* Scala 2.12.18 support drop

* Added s3 enum support

* Added s3 enumeratum support

* Introduced support for enumeratum value-enum. In progress

* Added value enum for s3 support. Still bugs in value enumeratum s2

* Added debug comments in KebsValueEnumeratum

* Changed debug comments in KebsValueEnumeratum

* Draft for value enumeratum debug

* Structure types value enums.

* Remove redundant wrappers, introduce package files and common tests for  s2 and s3

* Scala 2.12.18 support drop

* Revert "Scala 2.12.18 support drop"

This reverts commit 9790377.

* Fixed cyclic reference bug

* Introduce package objects in enum and enumeratum tests

* Introduce EnumLike and ValueEnumLike in slick and circe support. Tests CirceValueEnumDecoderEncoderTest and SlickMappedValueEnumColumnTypeTests not working, commented them out for now.

* Bring back one test for slick and one test for circe that cause errors

* Replace EnumOf and ValueEnumOf in favour of EnumLike and ValueEnumLike in all s2 modules

* Moved domain files in http4s-stir and in http4s to dedicated domain package

* Pekko http unmarshall test debug

* Bring back kebsValueEnumUnmarshaller

* Solved ambiguity problem. Type safety bug remains

* Throw an exception when generic types in pekko-http unmarshaller macros are conflicting

* Remove redundant import

* Rewrite core and enum module to the state before struct types

* Replace struct types with mixed trait

* Remove redundant enum macros from core

* Fix http4sStirSupport unmarshaller test

* Remove implicits prioritisation

* Update README with a migration guide

* Make conversion from Product to VCL optional

* Use FlatCaseClass1

* Rename FlatCaseClass1 to CaseClass1ToValueClass

* Update README

* Proper package naming convention, works for s2

* Proper packages naming convention for s2 and s3

* Remove minor bug in README

* Remove minor bug regarding instance keyword in README

* Enum interface refactor and minor stylistic fixes

* Remove instances naming bug

* Fix naming

* removing redundant dependencies and code

* doobie enumeratum fix

* remove unnecessary import

* slick - add enumeratum only for tests

* pekko-http - enumeratum in tests only

* remove duplication

* http4s-stir - enumeratum in tests only

* moving model to separate package object

* circe - model refactor

* removing redundancy in scalacheck module

* Bumps pre-Slick 3.5.x

* Basic support for Slick 3.5.x (#378)

* Basic support for Slick 3.5.x

* Buming CI to 3.3.3

* given -> implicit for ValueClassReps

* Removing -Xsource:3 temporarily

* Slick cleanup

* Unused cleanup.

---------

Co-authored-by: agrodowski <agrodowski@iteratorshq.com>
Co-authored-by: Antoni Grodowski <53835371+AnthonyGrod@users.noreply.github.com>
Co-authored-by: Paweł Kiersznowski <pkiersznowski@iterato.rs>
Co-authored-by: pkiersznowski <pkiersznowski@iteratorshq.com>
  • Loading branch information
5 people committed Jul 22, 2024
1 parent 5e8e9b2 commit 1412caa
Show file tree
Hide file tree
Showing 291 changed files with 2,829 additions and 3,237 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.12, 3.3.0]
scala: [2.13.11, 3.3.3]
java: [temurin@17]
runs-on: ${{ matrix.os }}
steps:
Expand Down
3 changes: 1 addition & 2 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ 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.12, 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:
merge:
Expand Down
91 changes: 51 additions & 40 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 @@ -519,18 +520,6 @@ test("work with nested single field objects") {
}
```

* mix-in `KebsSpray.NonFlat` if you want _flat_ format to become globally turned off for a protocol
```scala
object KebsProtocol extends DefaultJsonProtocol with KebsSpray.NoFlat
```

* use `noflat` annotation on selected case-classes (thanks to @dbronecki)
```scala
case class Book(name: String, chapters: List[Chapter])
@noflat case class Chapter(name: String)
```


Often you have to deal with convention to have **`snake-case` fields in JSON**.
That's something `kebs-spray-json` can do for you as well

Expand Down Expand Up @@ -662,10 +651,7 @@ object AfterKebs {
}
}
```
If you want to disable flat formats, you can mix-in `KebsCirce.NoFlat`:
```scala
object KebsProtocol extends KebsCirce with KebsCirce.NoFlat
```

You can also support snake-case fields in JSON:
```scala
object KebsProtocol extends KebsCirce with KebsCirce.Snakified
Expand All @@ -677,8 +663,7 @@ And capitalized:
```

**NOTE for Scala 3 version of kebs-circe**:
1. As of today, there is no support for the @noflat annotation - using it will have no effect.
2. If you're using recursive types - due to [this issue](https://github.com/circe/circe/issues/1980) you'll have to add codecs explicitly in the following way:
If you're using recursive types - due to [this issue](https://github.com/circe/circe/issues/1980) you'll have to add codecs explicitly in the following way:
```scala
case class R(a: Int, rs: Seq[R]) derives Decoder, Encoder.AsObject
```
Expand All @@ -690,11 +675,6 @@ case class R(a: Int, rs: Seq[R]) derives Decoder, Encoder.AsObject
import KebsProtocol.{given, _}
```

as for NoFlat, it should stay the same:
```scala
object KebsProtocol extends KebsCirce with KebsCirce.NoFlat
import KebsProtocol._
```
#### - kebs generates akka-http / pekko-http Unmarshaller (kebs-akka-http / kebs-pekko-http)

It makes it very easy to use 1-element case-classes or `enumeratum` enums/value enums in eg. `parameters` directive:
Expand Down Expand Up @@ -722,8 +702,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 +881,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 +926,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 +946,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 +980,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 +1085,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,14 @@
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 scala.language.implicitConversions
import pl.iterators.kebs.core.instances.InstanceConverter
import pl.iterators.kebs.core.macros.ValueClassLike

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.

Loading

0 comments on commit 1412caa

Please sign in to comment.