Skip to content

Instantly share code, notes, and snippets.

@pepegar
Created November 20, 2018 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pepegar/78e6cc0959c4d3acceec46ad40d46802 to your computer and use it in GitHub Desktop.
Save pepegar/78e6cc0959c4d3acceec46ad40d46802 to your computer and use it in GitHub Desktop.

Code generation in Skeuomorph

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

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.

Transformation

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.

Code generation

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.

Skeuomorph code organization

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!

Schemas

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)

Protocols

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]]
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment