Frictionless spec-first programming against OpenAPI
-
Setting up code generation
State of the art in specification-first programming relies on first generating some code (data types, interfaces, stubs, ...) from the specification. This usually means configuring a build step to run prior to compilation, potentially with the help of a dedicated build plugin for your build tool of choice.
-
Dealing with low-level details
... like serialization or HTTP protocol.
These present substantial accidental coplexity, especially for the inital, explorative phase of a project.
Just Import 'N' Go 😎
Use compile-time metaprogramming to generate code from spec "on-the-fly" in the usual compile-time, without a previous codegen step.
This approach requires no build setup other than adding jing
to library dependencies.
Additionally, the generated endpoints are ready to use, using reasonable default choices of HTTP and JSON libraries behind the scenes.
- Add JING to library dependencies
- sbt
libraryDependencies += "dev.continuously.jing" %% "jing-openapi" % "0.0.2"
- Scala CLI
//> using dep dev.continuously.jing::jing-openapi:0.0.2
- Just "import" an API
val api = jing.openapi("https://petstore3.swagger.io/api/v3/openapi.json")
- And discover the rest from there 😉
val api = jing.openapi("https://petstore3.swagger.io/api/v3/openapi.json")
api
.paths
.`/pet/findByStatus`
.Get
.as[ClientEndpoint]
.params:
( status = "available" )
.runAgainst("https://petstore3.swagger.io/api/v3")
See more in TestApp.scala.
-
Discoverability.
Import an API and discover the rest from there, without prior knowledge of the API or deep knowledge of the
jing
library.👍 type-drivenness, IDE-friendliness, helpful compilation errors
👎 chains of implicits, orphan typeclasses
-
Safety
Make it very hard to shoot yourself in the foot.
👍 type-safety, illegal states unrepresentable
-
Simple things simple.
👍 shortcuts for common scenarios
-
Complex things possible.
If needed, delegate to state-of-the-art 3rd party libraries, but provide a smooth handover of control.
👎 low complexity ceiling
-
Full OpenAPI support. OpenAPI is unnecessarily bloated.
jing
will aim to support a reasonable, computation sympathetic subset of OpenAPI. Feel free to open a ticket if you need something that's missing.Nevertheless, even if your API specification uses features unsupported by JING, you should be able to use at least the parts where those features are not used. That is, JING will not reject your whole spec just because it uses an unsupported schema of an optional parameter in one of the endpoints.
Successful proof of concept.
Lots of OpenAPI features are still missing.
There's lot's of space for improvement even with regards to the above principles (but feel free to call me out on violations thereof, at least I will know that you care).
How can JING match the capabilities of standalone code generators, given the restrictions of Scala macros?
The main limitation is that a macro cannot introduce new (value or type) definitions that would be visible from outside the macro expansion.
For example, JING cannot generate a class
for each schema and each endpoint found in the specification, or place the generated endpoints into an object
.
To stay within these constraints, code generated by JING is quite different from code produced by other generators. Basically
val api = jing.openapi("path/to/openapi.json")
produces a single value. The type of this value is a big structural type, something like this:
val api: {
val schemas: {
type Pet
val Pet: {
val schema: Schema[Pet]
def apply(...): Value[Pet] // constructor of Pet
def unapply(x: Value[Pet]): Some[...] // deconstructor of Pet
}
// ...
}
val paths: {
val `/pet`: {
val Post: HttpEndpoint[..., ...]
val Put: HttpEndpoint[..., ...]
}
// ...
}
}
In the macro-generated body of this value definition, classes can be defined and instantiated, but they won't be visible from the outside.
Probably not yet. Feel free to raise a ticket with a question or feature request.