All Projects → theiterators → Kebs

theiterators / Kebs

Licence: mit
Scala library to eliminate boilerplate

Programming Languages

scala
5932 projects
hack
652 projects

Projects that are alternatives of or similar to Kebs

Akka Http Microservice
Example of http (micro)service in Scala & akka-http
Stars: ✭ 701 (+520.35%)
Mutual labels:  akka, akka-http, boilerplate
Es Cqrs Shopping Cart
A resilient and scalable shopping cart system designed using Event Sourcing (ES) and Command Query Responsibility Segregation (CQRS)
Stars: ✭ 19 (-83.19%)
Mutual labels:  slick, akka, akka-http
ecommerce
A project for exploring Akka with Scala
Stars: ✭ 24 (-78.76%)
Mutual labels:  akka, akka-http, slick
Lagom Example
Example usage of the Lagom Framework for writing Java-based microservices
Stars: ✭ 20 (-82.3%)
Mutual labels:  akka, play-framework
Akka Http Json
Integrate some of the best JSON libs in Scala with Akka HTTP
Stars: ✭ 530 (+369.03%)
Mutual labels:  akka, akka-http
Scale
Another example of a REST API with Akka HTTP
Stars: ✭ 23 (-79.65%)
Mutual labels:  akka, akka-http
Go Api Boilerplate
Go Server/API boilerplate using best practices DDD CQRS ES gRPC
Stars: ✭ 373 (+230.09%)
Mutual labels:  restful, boilerplate
Play Reactive Slick
This is Play Template with a nice User Interface. If you want to use Play as web framework and Postgres as Database then this demo project can be used as a starting point for your application.
Stars: ✭ 40 (-64.6%)
Mutual labels:  slick, play-framework
Play26 Swagger Reactivemongo
A fully featured CRUD app built with Play 2.6, Swagger and ReactiveMongo
Stars: ✭ 36 (-68.14%)
Mutual labels:  play-framework, play
Spark As Service Using Embedded Server
This application comes as Spark2.1-as-Service-Provider using an embedded, Reactive-Streams-based, fully asynchronous HTTP server
Stars: ✭ 46 (-59.29%)
Mutual labels:  akka, akka-http
Swagger Akka Http Sample
Sample demonstrating use of swagger-akka-http
Stars: ✭ 79 (-30.09%)
Mutual labels:  akka, akka-http
Akka Http Session
Web & mobile client-side akka-http sessions, with optional JWT support
Stars: ✭ 429 (+279.65%)
Mutual labels:  akka, akka-http
Akka Http
The Streaming-first HTTP server/module of Akka
Stars: ✭ 1,163 (+929.2%)
Mutual labels:  akka, akka-http
Scala Ddd Example
🎯 λ Hexagonal Architecture + DDD + CQRS applied in Scala using Akka HTTP
Stars: ✭ 86 (-23.89%)
Mutual labels:  akka, akka-http
Koa Rest Api Boilerplate
💯 Boilerplate for Node.js Koa RESTful API application with Docker, Swagger, Jest, CodeCov and CircleCI
Stars: ✭ 420 (+271.68%)
Mutual labels:  restful, boilerplate
Es6 Express Mongoose Passport Rest Api
Lightweight boilerplate for Node RESTful API, ES6, Express, Mongoose and Passport 🎁
Stars: ✭ 36 (-68.14%)
Mutual labels:  restful, boilerplate
Akka Http Quickstart Scala.g8
Stars: ✭ 103 (-8.85%)
Mutual labels:  akka, akka-http
Boilerplay
Using the latest technology in the Scala ecosystem, Boilerplay is a reactive web application built on Play Framework, ScalaJS, Silhouette, Sangria/GraphQL, and PostgreSQL. It provides a good starting point for whatever you want to build.
Stars: ✭ 279 (+146.9%)
Mutual labels:  akka, play-framework
Akka Grpc
Akka gRPC
Stars: ✭ 361 (+219.47%)
Mutual labels:  akka, akka-http
Play Spark Scala
Stars: ✭ 51 (-54.87%)
Mutual labels:  akka, play-framework

Kebs

Scala library to eliminate boilerplate

Maven Central GitHub license Build Status

logo

A library maintained by Iterators.

Table of contents

Why?

kebs is for eliminating some common sources of Scala boilerplate code that arise when you use Slick (kebs-slick), Spray (kebs-spray-json), Play (kebs-play-json), Circe (kebs-circe), Akka HTTP (kebs-akka-http).

SBT

Support for slick

libraryDependencies += "pl.iterators" %% "kebs-slick" % "1.9.0"

Support for spray-json

libraryDependencies += "pl.iterators" %% "kebs-spray-json" % "1.9.0"

Support for play-json

libraryDependencies += "pl.iterators" %% "kebs-play-json" % "1.9.0"

Support for circe

libraryDependencies += "pl.iterators" %% "kebs-circe" % "1.9.0"

Support for json-schema

libraryDependencies += "pl.iterators" %% "kebs-jsonschema" % "1.9.0"

Support for scalacheck

libraryDependencies += "pl.iterators" %% "kebs-scalacheck" % "1.9.0"

Support for akka-http

libraryDependencies += "pl.iterators" %% "kebs-akka-http" % "1.9.0"

Support for tagged types

libraryDependencies += "pl.iterators" %% "kebs-tagged" % "1.9.0"

or for tagged-types code generation support

libraryDependencies += "pl.iterators" %% "kebs-tagged-meta" % "1.9.0" addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M11" cross CrossVersion.full)

Builds for Scala 2.12 and 2.13 are provided.

Examples

Please check out examples

- kebs generates slick mappers for your case-class wrappers (kebs-slick)

If you want to model the following table

case class UserId(userId: String)             extends AnyVal
case class EmailAddress(emailAddress: String) extends AnyVal
case class FullName(fullName: String)         extends AnyVal

//...

class People(tag: Tag) extends Table[Person](tag, "people") {
  def userId: Rep[UserId]                           = column[UserId]("user_id")
  def emailAddress: Rep[EmailAddress]               = column[EmailAddress]("email_address")
  def fullName: Rep[FullName]                       = column[FullName]("full_name")
  def mobileCountryCode: Rep[String]                = column[String]("mobile_country_code")
  def mobileNumber: Rep[String]                     = column[String]("mobile_number")
  def billingAddressLine1: Rep[AddressLine]         = column[AddressLine]("billing_address_line1")
  def billingAddressLine2: Rep[Option[AddressLine]] = column[Option[AddressLine]]("billing_address_line2")
  def billingPostalCode: Rep[PostalCode]            = column[PostalCode]("billing_postal_code")
  def billingCity: Rep[City]                        = column[City]("billing_city")
  def billingCountry: Rep[Country]                  = column[Country]("billing_country")
  def taxId: Rep[TaxId]                             = column[TaxId]("tax_id")
  def bankName: Rep[BankName]                       = column[BankName]("bank_name")
  def bankAccountNumber: Rep[BankAccountNumber]     = column[BankAccountNumber]("bank_account_number")
  def recipientName: Rep[RecipientName]             = column[RecipientName]("recipient_name")
  def additionalInfo: Rep[AdditionalInfo]           = column[AdditionalInfo]("additional_info")
  def workCity: Rep[City]                           = column[City]("work_city")
  def workArea: Rep[Area]                           = column[Area]("work_area")

  protected def mobile = (mobileCountryCode, mobileNumber) <> (Mobile.tupled, Mobile.unapply)
  protected def billingAddress =
    (billingAddressLine1, billingAddressLine2, billingPostalCode, billingCity, billingCountry) <> (Address.tupled, Address.unapply)
  protected def billingInfo =
    (billingAddress, taxId, bankName, bankAccountNumber, recipientName, additionalInfo) <> (BillingInfo.tupled, BillingInfo.unapply)

  override def * : ProvenShape[Person] =
    (userId, emailAddress, fullName, mobile, billingInfo, workCity, workArea) <> (Person.tupled, Person.unapply)
}

then you are forced to write this:

object People {
  implicit val userIdColumnType: BaseColumnType[UserId]                 = MappedColumnType.base(_.userId, UserId.apply)
  implicit val emailAddressColumnType: BaseColumnType[EmailAddress]     = MappedColumnType.base(_.emailAddress, EmailAddress.apply)
  implicit val fullNameColumnType: BaseColumnType[FullName]             = MappedColumnType.base(_.fullName, FullName.apply)
  implicit val addressLineColumnType: BaseColumnType[AddressLine]       = MappedColumnType.base(_.line, AddressLine.apply)
  implicit val postalCodeColumnType: BaseColumnType[PostalCode]         = MappedColumnType.base(_.postalCode, PostalCode.apply)
  implicit val cityColumnType: BaseColumnType[City]                     = MappedColumnType.base(_.city, City.apply)
  implicit val areaColumnType: BaseColumnType[Area]                     = MappedColumnType.base(_.area, Area.apply)
  implicit val countryColumnType: BaseColumnType[Country]               = MappedColumnType.base(_.country, Country.apply)
  implicit val taxIdColumnType: BaseColumnType[TaxId]                   = MappedColumnType.base(_.taxId, TaxId.apply)
  implicit val bankNameColumnType: BaseColumnType[BankName]             = MappedColumnType.base(_.name, BankName.apply)
  implicit val recipientNameColumnType: BaseColumnType[RecipientName]   = MappedColumnType.base(_.name, RecipientName.apply)
  implicit val additionalInfoColumnType: BaseColumnType[AdditionalInfo] = MappedColumnType.base(_.content, AdditionalInfo.apply)
  implicit val bankAccountNumberColumnType: BaseColumnType[BankAccountNumber] =
    MappedColumnType.base(_.number, BankAccountNumber.apply)
}
    

kebs can do it automagically for you

import pl.iterators.kebs._

class People(tag: Tag) extends Table[Person](tag, "people") {
  def userId: Rep[UserId]                           = column[UserId]("user_id")
  //...
}

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:

import pl.iterators.kebs.Kebs
object MyPostgresProfile extends ExPostgresDriver with PgArraySupport {
  override val api: API = new API {}
  trait API extends super.API with ArrayImplicits with Kebs
}

import MyPostgresProfile.api._

kebs-slick can also generate mappings for Postgres ARRAY type, which is a common source of boilerplate in slick-pg. Instead of:

object MyPostgresProfile extends ExPostgresDriver with PgArraySupport {
  override val api: API = new API {}
  trait API extends super.API with ArrayImplicits {
    implicit val institutionListTypeWrapper =
      new SimpleArrayJdbcType[Long]("int8").mapTo[Institution](Institution, _.value).to(_.toList)
    implicit val marketFinancialProductWrapper =
      new SimpleArrayJdbcType[String]("text").mapTo[MarketFinancialProduct](MarketFinancialProduct, _.value).to(_.toList)
  }
}

import MyPostgresProfile.api._

class ArrayTestTable(tag: Tag) extends Table[(Long, List[Institution], Option[List[MarketFinancialProduct]])](tag, "ArrayTest") {
  def id                   = column[Long]("id", O.AutoInc, O.PrimaryKey)
  def institutions         = column[List[Institution]]("institutions")
  def mktFinancialProducts = column[Option[List[MarketFinancialProduct]]]("mktFinancialProducts")

  def * = (id, institutions, mktFinancialProducts)
}

you can do just:

object MyPostgresProfile extends ExPostgresDriver with PgArraySupport {
  override val api: API = new API {}
  trait API extends super.API with ArrayImplicits with Kebs
}

import MyPostgresProfile.api._
class ArrayTestTable(tag: Tag) extends Table[(Long, List[Institution], Option[List[MarketFinancialProduct]])](tag, "ArrayTest") {
  def id                   = column[Long]("id", O.AutoInc, O.PrimaryKey)
  def institutions         = column[List[Institution]]("institutions")
  def mktFinancialProducts = column[Option[List[MarketFinancialProduct]]]("mktFinancialProducts")

  def * = (id, institutions, mktFinancialProducts)
}

kebs also supports Enumeratum Let's go back to the previous example. If you wanted to add a column of type EnumEntry, then you would have to write mapping for it:

sealed trait WorkerAccountStatus extends EnumEntry
object WorkerAccountStatus extends Enum[WorkerAccountStatus] {
  case object Unapproved extends WorkerAccountStatus
  case object Active     extends WorkerAccountStatus
  case object Blocked    extends WorkerAccountStatus

  override val values = findValues
}

object People {

  //...
  
  implicit val workerAccountStatusColumnType: BaseColumnType[WorkerAccountStatus] =
    MappedColumnType.base(_.entryName, WorkerAccountStatus.withName)
}

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

  //...
  
  def status: Rep[WorkerAccountStatus]              = column[WorkerAccountStatus]("status")

  //...

  override def * : ProvenShape[Person] =
    (userId, emailAddress, fullName, mobile, billingInfo, workCity, workArea, status) <> (Person.tupled, Person.unapply)
}

kebs takes care of this as well:

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

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

  //...

  def status: Rep[WorkerAccountStatus]              = column[WorkerAccountStatus]("status")

  //...

  override def * : ProvenShape[Person] =
    (userId, emailAddress, fullName, mobile, billingInfo, workCity, workArea, status) <> (Person.tupled, Person.unapply)
  }

You can also choose between a few strategies of writing enums. If you just import enums._, then you'll get its entryName in db. If you import enums.lowercase._ or enums.uppercase._ then it'll save enum name in, respectively, lower or upper case Of course, enums also work with traits:

import pl.iterators.kebs.Kebs
import pl.iterators.kebs.enums.KebsEnums

object MyPostgresProfile extends ExPostgresDriver {
  override val api: API = new API {}
  trait API extends super.API with Kebs with KebsEnums.Lowercase /* or KebsEnums, KebsEnums.Uppercase etc. */
}

import MyPostgresProfile.api._

kebs also supports ValueEnums, to save something other than entry's name to db. For example, if you wanted WorkerAccountStatus to be saved as int value, you'd write:

sealed abstract class WorkerAccountStatusInt(val value: Int) extends IntEnumEntry
object WorkerAccountStatusInt extends IntEnum[WorkerAccountStatusInt] {
    case object Unapproved extends WorkerAccountStatusInt(0)
    case object Active     extends WorkerAccountStatusInt(1)
    case object Blocked    extends WorkerAccountStatusInt(2)
   
    override val values = findValues
}

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

Writing JSON formats in spray can be really unwieldy. For every case-class you want serialized, you have to count the number of fields it has. And if you want a 'flat' format for 1-element case classes, you have to wire it yourself

def jsonFlatFormat[P, T <: Product](construct: P => T)(implicit jw: JsonWriter[P], jr: JsonReader[P]): JsonFormat[T] =
new JsonFormat[T] {
  override def read(json: JsValue): T = construct(jr.read(json))
  override def write(obj: T): JsValue = jw.write(obj.productElement(0).asInstanceOf[P])
}

All of this can be left to kebs-spray-json. Let's pretend we are to write an akka-http router:

class ThingRouter(thingsService: ThingsService)(implicit ec: ExecutionContext) {
  import ThingProtocol._
  def createRoute = (post & pathEndOrSingleSlash & entity(as[ThingCreateRequest])) { request =>
    complete {
      thingsService.create(request).map[ToResponseMarshallable] {
        case ThingCreateResponse.Created(thing) => Created  -> thing
        case ThingCreateResponse.AlreadyExists  => Conflict -> Error("Already exists")
      }
    }
  }
}

The source of boilerplate is ThingProtocol which can grow really big

 trait JsonProtocol extends DefaultJsonProtocol with SprayJsonSupport {
    implicit val urlJsonFormat = new JsonFormat[URL] {
      override def read(json: JsValue): URL = json match {
        case JsString(url) => Try(new URL(url)).getOrElse(deserializationError("Invalid URL format"))
        case _             => deserializationError("URL should be string")
      }

      override def write(obj: URL): JsValue = JsString(obj.toString)
    }

    implicit val uuidFormat = new JsonFormat[UUID] {
      override def write(obj: UUID): JsValue = JsString(obj.toString)

      override def read(json: JsValue): UUID = json match {
        case JsString(uuid) => Try(UUID.fromString(uuid)).getOrElse(deserializationError("Expected UUID format"))
        case _              => deserializationError("Expected UUID format")
      }
    }
  }

object ThingProtocol extends JsonProtocol {
  def jsonFlatFormat[P, T <: Product](construct: P => T)(implicit jw: JsonWriter[P], jr: JsonReader[P]): JsonFormat[T] =
    new JsonFormat[T] {
      override def read(json: JsValue): T = construct(jr.read(json))
      override def write(obj: T): JsValue = jw.write(obj.productElement(0).asInstanceOf[P])
    }

  implicit val errorJsonFormat              = jsonFormat1(Error.apply)
  implicit val thingIdJsonFormat            = jsonFlatFormat(ThingId.apply)
  implicit val tagIdJsonFormat              = jsonFlatFormat(TagId.apply)
  implicit val thingNameJsonFormat          = jsonFlatFormat(ThingName.apply)
  implicit val thingDescriptionJsonFormat   = jsonFlatFormat(ThingDescription.apply)
  implicit val locationJsonFormat           = jsonFormat2(Location.apply)
  implicit val createThingRequestJsonFormat = jsonFormat5(ThingCreateRequest.apply)
  implicit val thingJsonFormat              = jsonFormat6(Thing.apply)
}

But all of this can be generated automatically, can't it? You only need to import KebsSpray trait and you're done:

object ThingProtocol extends JsonProtocol with KebsSpray

Additionally, kebs-spray-json tries hard to be smart. It prefers 'flat' format when it comes across 1-element case-classes In case like this:

case class ThingId(uuid: UUID)
case class ThingName(name: String)

case class Thing(id: ThingId, name: ThingName, ...)

it'll do what you probably expected - {"id": "uuid", "name": "str"}. But it also takes into account if you want RootJsonFormat or not. So case class Error(message: String) in Conflict -> Error("Already exists") will be formatted as {"message": "Already exists"} in JSON.

What if you do not want to use 'flat' format by default? You have three options to choose from:

  • redefine implicits for case-classes you want serialized 'non-flat'
case class Book(name: String, chapters: List[Chapter])
case class Chapter(name: String)

implicit val chapterRootFormat: RootJsonFormat[Chapter] = jsonFormatN[Chapter]

test("work with nested single field objects") {
    val json =
      """
        | {
        |   "name": "Functional Programming in Scala",
        |   "chapters": [{"name":"first"}, {"name":"second"}]
        | }
      """.stripMargin
    
    json.parseJson.convertTo[Book] shouldBe Book(
      name = "Functional Programming in Scala",
      chapters = List(Chapter("first"), Chapter("second"))
    )
}
  • mix-in KebsSpray.NonFlat if you want flat format to become globally turned off for a protocol
object KebsProtocol extends DefaultJsonProtocol with KebsSpray.NoFlat
  • use noflat annotation on selected case-classes (thanks to @dbronecki)
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

object ThingProtocol extends JsonProtocol with KebsSpray.Snakified

Another advantage is that snakified names are computed during computation, so in run-time they're just string constants.

kebs-spray-json also can deal with enumeratum enums.

object ThingProtocol extends JsonProtocol with KebsSpray with KebsEnumFormats

As in slick's example, you have two additional enum serialization strategies: uppercase i lowercase (KebsEnumFormats.Uppercase, KebsEnumFormats.Lowercase), as well as support for ValueEnumEntry

It can also generate recursive formats via jsonFormatRec macro, as in the following example:

case class Thing(thingId: String, parent: Option[Thing])
implicit val thingFormat: RootJsonFormat[Thing] = jsonFormatRec[Thing]

kebs-spray-json also provides JSON formats for case classes with more than 22 fields.

- kebs eliminates play-json induced boilerplate (kebs-play-json)

To be honest play-json has never been a source of extensive boilerplate for me- thanks to Json.format[CC] macro. Only flat formats have had to be written over and over. And there is no support for enumeratum (support for enumeratum is provided by enumeratum-play-json and has been removed from kebs). So if you find yourself writing lots of code similar to:

def flatFormat[P, T <: Product](construct: P => T)(implicit jf: Format[P]): Format[T] =
  Format[T](jf.map(construct), Writes(a => jf.writes(a.productElement(0).asInstanceOf[P])))

implicit val thingIdJsonFormat          = flatFormat(ThingId.apply)
implicit val tagIdJsonFormat            = flatFormat(TagId.apply)
implicit val thingNameJsonFormat        = flatFormat(ThingName.apply)
implicit val thingDescriptionJsonFormat = flatFormat(ThingDescription.apply)

implicit val errorJsonFormat              = Json.format[Error]
implicit val locationJsonFormat           = Json.format[Location]
implicit val createThingRequestJsonFormat = Json.format[ThingCreateRequest]
implicit val thingJsonFormat              = Json.format[Thing]

, you can delegate it to kebs-play-json

import pl.iterators.kebs.json._
  
implicit val errorJsonFormat              = Json.format[Error]
implicit val locationJsonFormat           = Json.format[Location]
implicit val createThingRequestJsonFormat = Json.format[ThingCreateRequest]
implicit val thingJsonFormat              = Json.format[Thing]

(or, trait-style)

object AfterKebs extends JsonProtocol with KebsPlay {
implicit val errorJsonFormat              = Json.format[Error]
implicit val locationJsonFormat           = Json.format[Location]
implicit val createThingRequestJsonFormat = Json.format[ThingCreateRequest]
implicit val thingJsonFormat              = Json.format[Thing]
}

- kebs eliminates Circe induced boilerplate (kebs-circe)

Still in experimental stage! Circe might be a source of boilerplate depending on the type of derivation you use - if it's semi-auto derivation, you'll have to write a lot of encoders/decoders for your case classes:

object BeforeKebs {
    object ThingProtocol extends CirceProtocol with CirceAkkaHttpSupport {
      import io.circe._
      import io.circe.generic.semiauto._
      implicit val thingCreateRequestEncoder: Encoder[ThingCreateRequest] = deriveEncoder
      implicit val thingCreateRequestDecoder: Decoder[ThingCreateRequest] = deriveDecoder
      implicit val thingIdEncoder: Encoder[ThingId]                       = deriveEncoder
      implicit val thingIdDecoder: Decoder[ThingId]                       = deriveDecoder
      implicit val thingNameEncoder: Encoder[ThingName]                   = deriveEncoder
      implicit val thingNameDecoder: Decoder[ThingName]                   = deriveDecoder
      implicit val thingDescriptionEncoder: Encoder[ThingDescription]     = deriveEncoder
      implicit val thingDescriptionDecoder: Decoder[ThingDescription]     = deriveDecoder
      implicit val tagIdEncoder: Encoder[TagId]                           = deriveEncoder
      implicit val tagIdDecoder: Decoder[TagId]                           = deriveDecoder
      implicit val locationEncoder: Encoder[Location]                     = deriveEncoder
      implicit val locationDecoder: Decoder[Location]                     = deriveDecoder
      implicit val thingEncoder: Encoder[Thing]                           = deriveEncoder
      implicit val thingDecoder: Decoder[Thing]                           = deriveDecoder
      implicit val errorMessageDecoder: Decoder[ErrorMessage]             = deriveDecoder
      implicit val errorMessageEncoder: Encoder[ErrorMessage]             = deriveEncoder
    }
    import ThingProtocol._
    class ThingRouter(thingsService: ThingsService)(implicit ec: ExecutionContext) {

      def createRoute: Route = (post & pathEndOrSingleSlash & entity(as[ThingCreateRequest])) { request =>
        complete {
          thingsService.create(request).map[ToResponseMarshallable] {
            case ThingCreateResponse.Created(thing) => Created  -> thing
            case ThingCreateResponse.AlreadyExists  => Conflict -> ErrorMessage("Already exists")
          }
        }
      }
    }
  }

Kebs can get rid of this for you:

object AfterKebs {
    object ThingProtocol extends KebsCirce with CirceProtocol with CirceAkkaHttpSupport
    import ThingProtocol._

    class ThingRouter(thingsService: ThingsService)(implicit ec: ExecutionContext) {

      def createRoute: Route = (post & pathEndOrSingleSlash & entity(as[ThingCreateRequest])) { request =>
        complete {
          thingsService.create(request).map[ToResponseMarshallable] {
            case ThingCreateResponse.Created(thing) => Created  -> thing
            case ThingCreateResponse.AlreadyExists  => Conflict -> ErrorMessage("Already exists")
          }
        }
      }
    }
  }

If you want to disable flat formats, you can mix-in KebsCirce.NoFlat:

object KebsProtocol extends KebsCirce with KebsCirce.NoFlat

You can also support snake-case fields in JSON:

object KebsProtocol extends KebsCirce with KebsCirce.Snakified

And capitalized:

 object KebsProtocol extends KebsCirce with KebsCirce.Capitalized

- kebs generates akka-http Unmarshaller (kebs-akka-http)

It makes it very easy to use 1-element case-classes or enumeratum enums/value enums in eg. parameters directive:

sealed abstract class Column(val value: Int) extends IntEnumEntry
object Column extends IntEnum[Column] {
    case object Name extends Column(1)
    case object Date extends Column(2)
    case object Type extends Column(3)
    
    override val values = findValues
}

sealed trait SortOrder extends EnumEntry
object SortOrder extends Enum[SortOrder] {
    case object Asc  extends SortOrder
    case object Desc extends SortOrder
    
    override val values = findValues
}

case class Offset(value: Int) extends AnyVal
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._

val route = get {
  parameters('sortBy.as[Column], 'order.as[SortOrder] ? (SortOrder.Desc: SortOrder), 'offset.as[Offset] ? Offset(0), 'limit.as[Limit])
    .as(PaginationQuery) { query =>
      //...
    }

}

Tagged types

Starting with version 1.6.0, kebs contain an implementation of, so-called, tagged types. If you want to know what a tagged type is, please see eg. Introduction to Tagged Types or Scalaz tagged types description. In general, taggging of a type is a mechanism for distinguishing between various instances of the same type. For instance, you might want to use an Int to represent an user id or purchase id. But if you use just an Int the compiler will not protest if you use purchase id integer in place of user id integer and vice versa. To gain additional type safety you could use 1-element case-class wrappers, or, tagged types. In short, you would create Int @@ UserId and Int @@ PurchaseId types, where @@ is tag operator. Thus, you can distinguish between various usages of Int while still retaining all Int properties ie. Int @@ UserId is still an Int, but it is not Int @@ PurchaseId.

This representation is very useful at times, but there is some boilerplate involved which kebs strives to eliminate. Let's take a look at examples. To get only the kebs' implementation of tagged types, please add kebs-tagged module to your build. You'll then be able to use tagging:

import pl.iterators.kebs.tagged._

trait UserId
trait PurchaseId

val userId: Int @@ UserId = 10.taggedWith[UserId] 
val purchaseId: Int @@ PurchaseId = 10.@@[PurchaseId]

val userIds: List[Int @@ UserId] = List(10, 15, 20)[email protected]@@[UserId]
val purchaseIds: List[Int @@ PurchaseId] = List(10, 15, 20).taggedWithF[PurchaseId]

Additionally, if you want to use tagged types in Slick, just mix-in pl.iterators.kebs.tagged.slick.SlickSupport (or import pl.iterators.kebs.tagged.slick._).

import pl.iterators.kebs.tagged._
import pl.iterators.kebs.tagged.slick.SlickSupport

object SlickTaggedExample extends SlickSupport {
  trait UserIdTag
  type UserId = Long @@ UserIdTag

  trait EmailTag
  type Email = String @@ EmailTag

  trait FirstNameTag
  type FirstName = String @@ FirstNameTag

  trait LastNameTag
  type LastName = String @@ LastNameTag

  final case class User(id: UserId, email: Email, firstName: Option[FirstName], lastName: Option[LastName], isAdmin: Boolean)

  class Users(tag: Tag) extends Table[User](tag, "user") {
    def id: Rep[UserId]                   = column[UserId]("id")
    def email: Rep[Email]                 = column[Email]("email")
    def firstName: Rep[Option[FirstName]] = column[Option[FirstName]]("first_name")
    def lastName: Rep[Option[LastName]]   = column[Option[LastName]]("last_name")
    def isAdmin: Rep[Boolean]             = column[Boolean]("is_admin")

    override def * : ProvenShape[User] =
      (id, email, firstName, lastName, isAdmin) <> (User.tupled, User.unapply)
  }

}

More often than not, you want to perform some validation before tagging, or, you just want to have a smart constructor that will return tagged representation whenever criteria are met. You do not have to write it by hand, you can just use kebs-tagged-meta which generates all this code for you using scalameta. You just have to tag an object, or a trait, containing your tagged types with @tagged annotation.

import pl.iterators.kebs.tagged._
import pl.iterators.kebs.tag.meta.tagged

@tagged object Tags {
  trait NameTag
  trait IdTag[+A]
  trait PositiveIntTag

  type Name  = String @@ NameTag
  type Id[A] = Int @@ IdTag[A]

  type PositiveInt = Int @@ PositiveIntTag
  object PositiveInt {
    sealed trait Error
    case object Negative extends Error
    case object Zero     extends Error

    def validate(i: Int) = if (i == 0) Left(Zero) else if (i < 0) Left(Negative) else Right(i)
  }
}

The annotation will translate your code to something like

object Tags {
  trait NameTag
  trait IdTag[+A]
  trait PositiveIntTag
  
  type Name = String @@ NameTag
  type Id[A] = Int @@ IdTag[A]
  type PositiveInt = Int @@ PositiveIntTag
  
  object Name {
    def apply(arg: String) = from(arg)
    def from(arg: String) = arg.taggedWith[NameTag]
  }
  object Id {
    def apply[A](arg: Int) = from[A](arg)
    def from[A](arg: Int) = arg.taggedWith[IdTag[A]]
  }
  
  object PositiveInt {
    sealed trait Error
    case object Negative extends Error
    case object Zero extends Error
    def validate(i: Int) = if (i == 0) Left(Zero) else if (i < 0) Left(Negative) else Right(i)
    
    def apply(arg: Int) = from(arg).getOrElse(throw new IllegalArgumentException(arg.toString))
    def from(arg: Int) = validate(arg).right.map(arg1 => arg1.taggedWith[PositiveIntTag])
  }
  
  object PositiveIntTag {
    implicit val PositiveIntCaseClass1Rep = new CaseClass1Rep[PositiveInt, Int](PositiveInt.apply(_), identity)
  }
  object IdTag {
    implicit def IdCaseClass1Rep[A] = new CaseClass1Rep[Id[A], Int](Id.apply(_), identity)
  }
  object NameTag {
    implicit val NameCaseClass1Rep = new CaseClass1Rep[Name, String](Name.apply(_), identity)
  }
}

You can use generated from and apply methods as constructors of tagged type instance.

trait User

val someone = Name("Someone")
//someone: String @@ Tags.NameTag = Someone

val userId = Id[User](10)
//userId: Int @@ Tags.IdTag[User] = 10

val right = PositiveInt.from(10)
//right: scala.util.Either[Tags.PositiveInt.Error,Int @@ Tags.PositiveIntTag] = Right(10)

val notRight = PositiveInt.from(-10)
//notRight: scala.util.Either[Tags.PositiveInt.Error,Int @@ Tags.PositiveIntTag] = Left(Negative)

val alsoRight = PositiveInt(10)
//alsoRight: Int @@ Tags.PositiveIntTag = 10

PositiveInt(-10)
// java.lang.IllegalArgumentException: -10

There are some conventions that are assumed during generation.

  • tags have to be empty traits (possibly generic)
  • tagged types have to be aliases in form of type X = SomeType @@ Tag (possibly generic)
  • validation methods for tagged type X have to be defined in object X and have to:
    • be public
    • be named validate
    • take no type parameters
    • 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.

JsonSchema support

Kebs contains a macro, which generate wrapped jsonschema object of https://github.com/andyglow/scala-jsonschema. Kebs also provides proper implicits conversions for their tagged types and common Java types. To get your json schema you need to use import pl.iterators.kebs.jsonschema.KebsJsonSchema (together with pl.iterators.kebs.jsonschema.KebsJsonSchemaPredefs if you need support for more Java types).

import com.github.andyglow.json.JsonFormatter
import com.github.andyglow.jsonschema.AsValue
import json.schema.Version.Draft07
import pl.iterators.kebs.jsonschema.{KebsJsonSchema, JsonSchemaWrapper}

case class WrappedInt(int: Int)
case class WrappedIntAnyVal(int: Int) extends AnyVal
case class Sample(someNumber: Int,
                  someText: String,
                  arrayOfNumbers: List[Int],
                  wrappedNumber: WrappedInt,
                  wrappedNumberAnyVal: WrappedIntAnyVal)

object Sample extends KebsJsonSchema {

  object SchemaPrinter {
    def printWrapper[T](id: String = "id")(implicit schemaWrapper: JsonSchemaWrapper[T]): String =
      JsonFormatter.format(AsValue.schema(schemaWrapper.schema, Draft07(id)))
  }

  SchemaPrinter.printWrapper[Sample]()

}

Scalacheck support

Kebs provides support to use tagged types in your Arbitrary from scalacheck. Along with this kebs provides support for Java types. Kebs also introduces term of minimal and maximal generator. The minimal generator is an generator which always generates empty collection of Option, Set, Map etc. The maximum - in the opposite - always generate non-empty collections. Kebs provides an useful trait called AllGenerators which binds minimal, normal and maximal generator all together, so you can get easy generate the representation you currently need for tests.

import pl.iterators.kebs.scalacheck.{KebsArbitraryPredefs, KebsScalacheckGenerators}
import java.net.{URI, URL}
import java.time.{Duration, Instant, LocalDate, LocalDateTime, LocalTime, ZonedDateTime}

case class WrappedInt(int: Int)
case class WrappedIntAnyVal(int: Int) extends AnyVal
case class BasicSample(
    someNumber: Int,
    someText: String,
    wrappedNumber: WrappedInt,
    wrappedNumberAnyVal: WrappedIntAnyVal,
)

case class CollectionsSample(
    listOfNumbers: List[Int],
    arrayOfNumbers: Array[Int],
    setOfNumbers: Set[Int],
    vectorOfNumbers: Vector[Int],
    optionOfNumber: Option[Int],
    mapOfNumberString: Map[Int, String],
)

case class JavaTypesSample(
    instant: Instant,
    zonedDateTime: ZonedDateTime,
    localDateTime: LocalDateTime,
    localDate: LocalDate,
    localTime: LocalTime,
    duration: Duration,
    url: URL,
    uri: URI
)

object Sample extends KebsScalacheckGenerators with KebsArbitraryPredefs {

    val basic = allGenerators[BasicSample].normal.generate

    val minimalCollections = allGenerators[CollectionsSample].minimal.generate
    val maximalCollections = allGenerators[CollectionsSample].maximal.generate

    val javaTypes =  allGenerators[JavaTypesSample].normal.generate

}

Kebs for IntelliJ

The code generated by macros in kebs-tagged-meta is not visible to IntelliJ IDEA. There is Kebs for IntelliJ 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.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].