Target - Scala

Quick Start

gugugu-scala \
        --input=dir/containing/gugugu/definitions \
        --output=dir/containing/scala/code \
        --package-prefix=com.example.foo.generated \
        --with-codec \
        --with-server \
        --with-client \
        --no-higher-kinds-import \
        ;

Module

Gugugu module is represented by Scala packages, with module name lower-cased, without underscores.

Types

Primitives

Gugugu Type

Scala Type

Unit

Unit

Bool

Boolean

Int32

Int

Double

Double

String

String

Maybe A

Option[A]

List A

Vector[A]

Record Type

Record type are represented by case class.

data Book
  = Book
    { id   :: Int32
    , name :: String
    }

becomes

case class Book
  ( id   : Int32
  , name : String
  )

Enum Type

Enum type are represented by sealed trait.

data Color
  = Red
  | Green
  | Blue

becomes

sealed trait Color

object Color {
  case object Red   extends Color
  case object Green extends Color
  case object Blue  extends Color
}

Foreign Type

data DateTime
  {-# FOREIGN scala java.time.LocalDateTime #-}

Foreign type generates no scala codes. Gugugu just replaces the DateTime with the corresponding scala type.

Encoder and Decoder

All types in this section are located in package gugugu.lang.scala.runtime.codec with default configuration.

The generated codecs have type

trait Encoder[A] {
  def encode[S, R](s: S, a: A, impl: EncoderImpl[S, R]): S
}

trait Decoder[A] {
  def decode[S, R](s: S, impl: DecoderImpl[S, R]): (S, A)
}

object Encoder {
  def apply[A](implicit encoder: Encoder[A]): Encoder[A]
  def encode[S, R, A](a: A, impl: EncoderImpl[S, R])
                     (implicit encoder: Encoder[A]): R
}

object Decoder {
  def apply[A](implicit decoder: Decoder[A]): Decoder[A]
  def decode[S, R, A](r: R, impl: DecoderImpl[S, R])
                     (implicit decoder: Decoder[A]): A
}

The encoder and decoder are always defined as implicit values of the companion object, so you can get them with the expression Encoder[A] or Decoder[A].

The EncoderImpl[S, R] and DecoderImpl[S, R] are two values you have to provide to describe how to encode and decode a value.

Use the Encoder.encode[S, R, A] to encode a value of type A to type R, with the encoder and the EncoderImpl[S, R]. Likewise, use the Decoder.decode[S, R, A] to decode a value of type A from type R, with the decoder and the DecoderImpl[S, R].

The encoder and decoder are polymorphic over S and R. With different EncoderImpl/DecoderImpl provided, you can encode/decode values to/from different types you want.

EncoderImpl and DecoderImpl

The S is the state used in encoding/decoding. The R is the serialized type encoding to or decoding from.

You might find examples/lang/scala/src/main/scala/guguguexamples/codec/JsonCodecImpl.scala useful to write a EncoderImpl/DecoderImpl.

Most functions in the traits works with only S except the following two.

trait EncoderImpl[S, R] {
  def encodeWithState(k: S => S): R
}

trait DecoderImpl[S, R] {
  def decodeWithState[A](r: R, k: S => (S, A)): A
}

For an EncoderImpl.encodeWithState, you usually should

  • Provide an initial state

  • Feed it to the function provided

  • Transform the state returned to serialized type, R

For a DecoderImpl.decodeWithState, you usually should

  • Transform the R into an initial state S

  • Feed it to the function provided

  • Make sure the returned state did not go wrong

  • Return the decoded value returned by the function provided.

The generated encoders/decoders are designed to be compatible with either an immutable state type or a mutable one. The state object will never be reused in generated code. The modification can happen in place if you take care of it in your code.

The generated code will never throw any exceptions, but you usually want to do so in your EncoderImpl/DecoderImpl when things go wrong.

Encode/Decode Record Type

trait EncoderImpl[S, R] {
  def encodeRecord(s: S, nFields: Int, k: S => S): S
  def encodeRecordField( s: S
                       , i: Int, name: String
                       , k: S => S
                       ): S
}

trait DecoderImpl[S, R] {
  def decodeRecord[A](s: S, nFields: Int, k: S => (S, A)): (S, A)
  def decodeRecordField[A]( s: S
                          , i: Int, name: String
                          , k: S => (S, A)
                          ): (S, A)
}

The generated encoder/decoder for record type consists of a call to EncoderImpl.encodeRecord/DecoderImpl.decodeRecord. And the provided callback will call the EncoderImpl.encodeRecordField/DecoderImpl.decodeRecordField several times with indices and names of the fields.

Encode/Decode Enum Type

trait EncoderImpl[S, R] {
  def encodeEnum[A]( s: S, a: A
                   , asIndex: A => Int
                   , asName: A => String
                   ): S
}

trait DecoderImpl[S, R] {
  def decodeEnum[A]( s: S
                   , byIndex: Int => Option[A]
                   , byName: String => Option[A]
                   ): (S, A)
}

The generated encoder/decoder for enum type consists of a call to EncoderImpl.encodeEnum/DecoderImpl.decodeEnum. You should encode/decode the value with the name or the index.

Encode/Decode Primitive and Foreign Types

trait EncoderImpl[S, R] {
  def encodeUnit(s: S, v: Unit): S
  def encodeBool(s: S, v: Boolean): S
  def encodeInt32(s: S, v: Int): S
  def encodeDouble(s: S, v: Double): S
  def encodeString(s: S, v: String): S
}

trait DecoderImpl[S, R] {
  def decodeUnit(s: S): (S, Unit)
  def decodeBool(s: S): (S, Boolean)
  def decodeInt32(s: S): (S, Int)
  def decodeDouble(s: S): (S, Double)
  def decodeString(s: S): (S, String)
}

The primitive types and foreign types will generate functions like above. And the encoder/decoder simply calls the function you provide.

Encode/Decode Maybe and List

trait EncoderImpl[S, R] {
  def encodeMaybe(s: S, isNothing: Boolean, k: S => S): S

  def encodeList(s: S, len: Int, k: S => S): S
  def encodeListNth(s: S, i: Int, k: S => S): S
}

trait DecoderImpl[S, R] {
  def decodeMaybe[A](s: S, k: (S, Boolean) => (S, A)): (S, A)

  def decodeList[A](s: S, k: (S, Int) => (S, A)): (S, A)
  def decodeListNth[A](s: S, i: Int, k: S => (S, A)): (S, A)
}

The List functions works like the record functions, except they do not care about the name.

You have to tell the callback provided by DecoderImpl.decodeMaybe whether the value is empty or not.

You have to tell the callback provided by DecoderImpl.decodeList the length of the list.

Client and Server

All types in this section are located in package gugugu.lang.scala.runtime.transport with default configuration.

module Hello where

foo :: FooReq -> IO FooRes
bar :: BarReq -> IO BarRes

becomes

trait HelloModule[F[_], G[_], M[_]] {
  def foo(fa: F[FooReq]): M[G[FooRes]]
  def bar(fa: F[BarReq]): M[G[BarRes]]
}

object HelloModule {
  def toTransport[F[_], G[_], M[_], RA, RB, SA, SB]( impl: HelloModule[F, G, M]
                                                   , decoderImpl: DecoderImpl[SA, RA]
                                                   , encoderImpl: EncoderImpl[SB, RB]
                                                   ): ServerTransport[F, G, M, RA, RB]

  def fromTransport[F[_], G[_], M[_], RA, RB, SA, SB]( transport: ClientTransport[F, G, M, RA, RB]
                                                     , encoderImpl: EncoderImpl[SA, RA]
                                                     , decoderImpl: DecoderImpl[SB, RB]
                                                     ): HelloModule[F, G, M]
}

The HelloModule can be used as the client when used in client code, or as the server implementation in server code.

The RA is the serialized type used by request, and the RB is the serialized type used by response. They are usually the same type but not necessary.

Some typical use of F, G and M are list below.

F can be

  • type I[A] = A, when you just want to pass the value.

  • type WithMeta[A] = (SomeMeta, A), when you want some metadata with your request, such as authentication data.

  • Stream, when you want to process many data in one request.

G can be

  • type I[A] = A, when you just want to pass the value.

  • type WithMeta[A] = (SomeMeta, A), when you want to return some metadata to with your response, such as request ID, processed time, etc.

  • Stream, when you want to return many data in one request.

  • Try. when you want error handling.

M can be

Warning

Do not use any type that cannot be converted into type WithMeta[A] = (SomeMeta, Option[A]) (such as Stream) as F or G if you want to work with other target that does not support polymorphism over higher-kinded types. Most targets do not support polymorphism over higher-kinded types.

Server Usage

case class QualName[A](namespace: Vector[A], name: A)

trait ServerTransport[F[_], G[_], M[_], RA, RB] {
  def ask( name: QualName[String]
         , codecHandler: ServerCodecHandler[F, G, M, RA, RB]
         ): Option[F[RA] => M[G[RB]]]
}

trait ServerCodecHandler[F[_], G[_], M[_], RA, RB] {
  def apply[A, B]( fr: F[RA]
                 , decodeA: RA => A
                 , encodeB: B => RB
                 , k: F[A] => M[G[B]]
                 ): M[G[RB]]
}

HelloModule.toTransport converts a HelloModule[F, G, M] into a ServerTransport[F, G, M, RA, RB]. A ServerTransport[F, G, M, RA, RB] can handle request about type RA and return an response about type RB.

To call the ServerTransport, you need a ServerCodecHandler[F, G, M, RA, RB] to handle the encoding/decoding, because Gugugu only knows how to decode from RA to A and encode from B to RB, but does not know how to decode from F[RA] to F[A] and encode from G[B] to G[RB], and how to handle possible exceptions. The decoder and the encoder may throw exceptions if you do that in the corresponding EncoderImpl/DecoderImpl, and you are responsible to handle that.

Please consult examples/lang/scala/src/main/scala/guguguexamples/jsonhttp/server/ for how to use the ServerTransport.

Client Usage

case class QualName[A](namespace: Vector[A], name: A)

trait ClientTransport[F[_], G[_], M[_], RA, RB] {
  def send[A, B]( name: QualName[String]
                , fa: F[A]
                , encodeA: A => RA
                , decodeB: RB => B
                ): M[G[B]]
}

HelloModule.fromTransport converts a ClientTransport[F, G, M, RA, RB] into a HelloModule[F, G, M]. Like ServerTransport, it can handle request about type RA and return an response about type RB. Like ServerCodecHandler, you are responsible to handle possible exceptions.

Please consult examples/lang/scala/src/main/scala/guguguexamples/jsonhttp/client/ for how to write a ClientTransport.

Command Line Options

Usage: gugugu-scala (-i|--input INPUT) (-o|--output OUTPUT)
                    (-p|--package-prefix PACKAGE_PREFIX)
                    [-r|--runtime-package RUNTIME_PACKAGE] [--with-codec]
                    [--with-server] [--with-client] [--no-higher-kinds-import]
                    [--trans-module-code ARG] [--trans-module-value ARG]
                    [--trans-module-type ARG] [--trans-func-code ARG]
                    [--trans-func-value ARG] [--trans-type-code ARG]
                    [--trans-type-func ARG] [--trans-field-code ARG]
                    [--trans-field-value ARG] [--trans-enum-code ARG]
                    [--trans-enum-value ARG] [--version]

Available options:
  -i,--input INPUT         the directory containing the definition files
  -o,--output OUTPUT       the directory to put the generated sources
  -p,--package-prefix PACKAGE_PREFIX
                           the package prefix, e.g. com.example.foo.generated
  -r,--runtime-package RUNTIME_PACKAGE
                           location of gugugu runtime package
                           (default: "gugugu.lang.scala.runtime")
  --with-codec             pass this flag to generate codecs, default to false
  --with-server            pass this flag to generate server, default to false,
                           implies with-codec
  --with-client            pass this flag to generate client, default to false,
                           implies with-codec
  --no-higher-kinds-import pass this flag to disable import
                           scala.language.higherKinds, which is not necessary in
                           scala 2.13+
  --trans-module-code ARG  module name transformer for code (default: lower)
  --trans-module-value ARG module name transformer for value (default: snake)
  --trans-module-type ARG  module name transformer for type of client/server
                           (default: id)
  --trans-func-code ARG    function name transformer for code (default: id)
  --trans-func-value ARG   function name transformer for value (default: snake)
  --trans-type-code ARG    type name transformer for code (default: id)
  --trans-type-func ARG    type name transformer in function (default: id)
  --trans-field-code ARG   record field name transformer for code (default: id)
  --trans-field-value ARG  record field name transformer for value
                           (default: snake)
  --trans-enum-code ARG    enum name transformer for code (default: id)
  --trans-enum-value ARG   enum name transformer for value
                           (default: upper-snake)
  -h,--help                Show this help text
  --help-transformers      list available name transformers
  --version                show version