Scrooge Shapes
This project provides instances of Shapeless's Generic
type classes for types generated for Apache Thrift definitions by Twitter's Scrooge.
These instances allow you to use the generic derivation mechanisms provided by libraries like Circe or scalacheck-shapeless to generate boilerplate-free type class instances for your Scrooge types.
Note that this library does not bring its own runtime dependency on either Scrooge or libthrift, and the macros provided here have been used on a range of Scrooge versions, from 4.6.0 to 18.2.0. Currently only a single (recent) Scrooge version is being tested and supported, but this may change in the future.
History
This library is based on a demonstration project written by Travis Brown in 2016, and is now used internally at Stripe to support generic derivation and other applications.
Usage
Given a Thrift definition like the following, Scrooge will generate a Scala type that is case class-like, but not a case class:
namespace java com.stripe.scrooge.shapes
struct MapExample {
1: required string id
3: optional map<string,string> metadata
}
Shapeless cannot derive Generic
or LabelledGeneric
instances for types like this on its own, which is where Shapes comes in—you simply import the contents of the package object and then you can use any generic derivation mechanism as if you were working with ordinary case classes or sealed trait hierarchies.
scala> import com.stripe.scrooge.shapes._
import com.stripe.scrooge.shapes._
scala> import io.circe.ObjectEncoder, io.circe.syntax._
import io.circe.ObjectEncoder
import io.circe.syntax._
scala> import io.circe.generic.semiauto.deriveEncoder
import io.circe.generic.semiauto.deriveEncoder
scala> implicit val encodeMapExample: ObjectEncoder[MapExample] = deriveEncoder
encodeMapExample: io.circe.ObjectEncoder[com.stripe.scrooge.shapes.MapExample] = io.circe.generic.encoding.DerivedObjectEncoder$$anon$1@2ecb4528
scala> MapExample("abc", Some(Map("foo" -> "bar", "baz" -> "quz"))).asJson
res0: io.circe.Json =
{
"id" : "abc",
"metadata" : {
"foo" : "bar",
"baz" : "quz"
}
}
See the GenericInstancesTest
test suite for more details about the representations used for these types. In particular note that if you're working with a Thrift union, you may need to provide instances of your target type class for TFieldBlob
explicitly. For example, if you want to want to use Circe's generic derivation with a union like this:
namespace java com.stripe.scrooge.shapes
struct StructExample {
1: required string foo
2: optional string bar
}
union UnionExample {
1: StructExample a
2: i32 b
3: string c
}
You will need some utility code like the following:
import io.circe.Encoder, io.circe.scodec._, io.circe.syntax._
import com.twitter.scrooge.TFieldBlob, scodec.bits.ByteVector
implicit val encodeTFieldBlob: Encoder[TFieldBlob] =
Encoder.instance(blob => ByteVector(blob.data).asJson)
This will allow you to derive Encoder
and Decoder
types for UnionExample
:
import com.stripe.scrooge.shapes._, io.circe.generic.auto._
scala> val ex: UnionExample = UnionExample.A(StructExample("abc", Some("123")))
ex: com.stripe.scrooge.shapes.UnionExample = A(StructExample(abc,Some(123)))
scala> ex.asJson
res0: io.circe.Json =
{
"A" : {
"foo" : "abc",
"bar" : "123"
}
}
Without the TFieldBlob
encoder, Circe would not know how to encode the UnknownUnionField
disjunct that is included in Scrooge's representation of every union, and its generic derivation would fail to compile.
License
scrooge-shapes is licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.