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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
Rinto an initial stateSFeed 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
type I[A] = A, when you want a simple synchronized client or server.Future, when you want an asynchronous client or server withFuture. Note, you can put theExecutionContextas the metadata into the function.type Continuation[A] = (A => Unit) => Unit, when you want a simple callback based service.cats.effect.IO, when you want to work withcats-effect.type ContIO[R, A] = (A => IO[R]) => IO[R], if you want delimited continuation. Vide http://hackage.haskell.org/package/transformers-0.5.6.2/docs/Control-Monad-Trans-Cont.html and https://en.wikipedia.org/wiki/Delimited_continuation.
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