Table of Contents
Skeuomorph is a library for transforming one schemas into others. Currently we have Avro, Protobuf, Mu, and Openapi schemas defined. What we can do with skeuomorph is get something defined in one schema, for example an Avro file, and conver it to a Mu service declaration.
This process is done in several steps. This process is also outlined in the README of the project:
Parsing is normally done by an external library, since creating our own parser with somethign like Atto would be really hard to get it right.
In the case of avro, we use the Java SDK directly to parse Avro files, and in protobuf we could use something like scalapb.
Once we have whatever the library produces (this is normally an object
of a Java class, org.apache.avro.Protocol
in the case of Avro), we
convert it to our initial schema. In the case of Avro, the initial
schema is skeuomorph.avro.Protocol
.
After having parsed the original file, we want to transform our current
schema into the target schema. To do so we use qq.droste.Trans
, which
is basically a function F[A] => G[A]
, where F
would be AvroF
in
our first example, and G
would be MuF
.
Finally, for code generation we use the Printer trait. Printers are the dual of Parsers:
// more or less :)
type Printer[A] = A => String
type Parser[A] => String => A
We combine them using the Contravariant
, ContravariantMonoidal
, and
Decidable
typeclasses. You can think of printing like the dual of
parsing, and all the typeclasses we use are the duals of Functor
,
Applicative
, and Alternative
respectively.
You can see how we're using printers in the print.scala file.
Currently I've been following some rules to where to put code in Skeuomorph. I'm not saying this is how it should be done, I just think knowing it can help you understand code better!
Normally, in all schema.scala
files I'm putting the declaration for
types of the given schema. So, for example all the possible types in
Protobuf are declared in skeuomorph.protobuf.schema
file.
Here's the schema for possible avro types as a reference:
@deriveTraverse sealed trait AvroF[A]
object AvroF {
@deriveTraverse final case class Field[A](
name: String,
aliases: List[String],
doc: Option[String],
order: Option[Order],
tpe: A
)
type TypeName = String
final case class TNull[A]() extends AvroF[A]
final case class TBoolean[A]() extends AvroF[A]
final case class TInt[A]() extends AvroF[A]
final case class TLong[A]() extends AvroF[A]
final case class TFloat[A]() extends AvroF[A]
final case class TDouble[A]() extends AvroF[A]
final case class TBytes[A]() extends AvroF[A]
final case class TString[A]() extends AvroF[A]
final case class TNamedType[A](name: TypeName) extends AvroF[A]
final case class TArray[A](item: A) extends AvroF[A]
final case class TMap[A](values: A) extends AvroF[A]
final case class TRecord[A](
name: TypeName,
namespace: Option[String],
aliases: List[TypeName],
doc: Option[String],
fields: List[Field[A]])
extends AvroF[A]
final case class TEnum[A](
name: TypeName,
namespace: Option[String],
aliases: List[TypeName],
doc: Option[String],
symbols: List[String])
extends AvroF[A]
final case class TUnion[A](options: NonEmptyList[A]) extends AvroF[A]
final case class TFixed[A](name: TypeName, namespace: Option[String], aliases: List[TypeName], size: Int)
extends AvroF[A]
}
(@deriveTraverse annotation comes from droste, and it... derives a Traverse instance :D)
Then we use the possible types from the schema in the Protocol
definition. Here is skeuomorph.protobuf.schema
fileAvro's protocol
deifinition FWIW:
final case class Protocol[A](
name: String,
namespace: Option[String],
types: List[A],
messages: List[Protocol.Message[A]]
)