I’m working on a project meant to reduce the boilerplate for REST/CRUD applications. I’ve basically been working in a silo, so I would love some feedback on this approach. I was thinking about submitting a proposal to talk at LambdaConf, but I’m not sure if this approach is interesting to other developers.
The approach is to first describe the data using a DSL which generates a GADT data structure. For instance:
case class Person(name: String, age: Long, gender: Option[String]) val personSchema = ( kvp("name", string(sv.matchesRegex("^[a-zA-Z ]*$".r))) :: kvp("age", long(iv.min(0))) :: kvp("gender", string.optional) :: KvpNil ).convert[Person] val personWithId = (kvp("id", long) :: personSchema :: KvpNil).convert[WithId[Person]] case class Error(error: String) val errorDef = (kvp("error", string) :: KvpNil).convert[Error]
Next we describe the available operations which includes the Person data definition above. This process also generates a GADT data structure.
val personService = ServiceOps.withPath("person") .withCreate(personSchema, personWithId, errorDef) .withRead(personWithId, errorDef) .withUpdate(personSchema, personWithId, errorDef) .withDelete(personWithId, errorDef)
The personService description is the schema which can be passed to different interpreters. For instance the OpenAPI interpreter will generate an OpenAPI/Swagger compliant string of the service base on the schema.
val openApi = new OpenAPI() CrudOasInterpreter.jsonApiForService(personService).apply(openApi) println(io.swagger.v3.core.util.Json.mapper().writeValueAsString(openApi)
The http4s interpreter will generate HttpRoutes[IO] for each of the defined operations. The interpreter is responsible for generating a runtime which will unmarshal,validate and marhsall based on the schema. Currently, the library supports JSON and BJSON and I am currently working on a Protobuf implementation as well.
val service = HttpInterpreter("/person") .withContentType(jsonFormat) .withSwagger() // createF, readF, updateF and deleteF is the actual business logic of the web service. // where the inputs and output match the schema, so for readF // the function must match (long) => Either[Error,PersonWithId] val http4Service = service.forService( personService, createF, readF, updateF, deleteF ) BlazeBuilder[IO].bindHttp(8080, "localhost").mountService(http4Service, "/") .serve .compile.drain.as(ExitCode.Success)
Here is the complete Example.
I like this approach because updates to the schema will be reflected in each interpreter. (The doc stays up to date with the application!)
I’d also like to experiment with Scala.js to see if a reasonable React application can be generated using the schema which would be compatible with the personService. The goal is to have a full stack application which would be similar doing Ruby Scaffolding, but in a typesafe, functional manner.
I call the project Bones