Skip to main content

Announcing "entoas" - An Extension to Automatically Generate OpenAPI Specification Documents from Ent Schemas

· 6 min read

The OpenAPI Specification (OAS, formerly known as Swagger Specification) is a technical specification defining a standard, language-agnostic interface description for REST APIs. This allows both humans and automated tools to understand the described service without the actual source code or additional documentation. Combined with the Swagger Tooling you can generate both server and client boilerplate code for more than 20 languages, just by passing in the OAS document.

In a previous blogpost, we presented to you a new feature of the Ent extension elk: a fully compliant OpenAPI Specification document generator.

Today, we are very happy to announce, that the specification generator is now an official extension to the Ent project and has been moved to the ent/contrib repository. In addition, we have listened to the feedback of the community and have made some changes to the generator, that we hope you will like.

Getting Started​

To use the entoas extension use the entc (ent codegen) package as described here. First install the extension to your Go module:

go get entgo.io/contrib/entoas

Now follow the next two steps to enable it and to configure Ent to work with the entoas extension:

1. Create a new Go file named ent/entc.go and paste the following content:

// +build ignore

package main

import (
"log"

"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)

func main() {
ex, err := entoas.NewExtension()
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

2. Edit the ent/generate.go file to execute the ent/entc.go file:

package ent

//go:generate go run -mod=mod entc.go

With these steps complete, all is set up for generating an OAS document from your schema! If you are new to Ent and want to learn more about it, how to connect to different types of databases, run migrations or work with entities, then head over to the Setup Tutorial.

Generate an OAS document​

The first step on our way to the OAS document is to create an Ent schema graph. For the sake of brevity here is an example schema to use:

ent/schema/schema.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)

// Fridge holds the schema definition for the Fridge entity.
type Fridge struct {
ent.Schema
}

// Fields of the Fridge.
func (Fridge) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
}
}

// Edges of the Fridge.
func (Fridge) Edges() []ent.Edge {
return []ent.Edge{
edge.To("compartments", Compartment.Type),
}
}

// Compartment holds the schema definition for the Compartment entity.
type Compartment struct {
ent.Schema
}

// Fields of the Compartment.
func (Compartment) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

// Edges of the Compartment.
func (Compartment) Edges() []ent.Edge {
return []ent.Edge{
edge.From("fridge", Fridge.Type).
Ref("compartments").
Unique(),
edge.To("contents", Item.Type),
}
}

// Item holds the schema definition for the Item entity.
type Item struct {
ent.Schema
}

// Fields of the Item.
func (Item) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

// Edges of the Item.
func (Item) Edges() []ent.Edge {
return []ent.Edge{
edge.From("compartment", Compartment.Type).
Ref("contents").
Unique(),
}
}

The code above is the Ent-way to describe a schema-graph. In this particular case we created three Entities: Fridge, Compartment and Item. Additionally, we added some edges to the graph: A Fridge can have many Compartments and a Compartment can contain many Items.

Now run the code generator:

go generate ./...

In addition to the files Ent normally generates, another file named ent/openapi.json has been created. Here is a sneak peek into the file:

ent/openapi.json
{
"info": {
"title": "Ent Schema API",
"description": "This is an auto generated API description made out of an Ent schema definition",
"termsOfService": "",
"contact": {},
"license": {
"name": ""
},
"version": "0.0.0"
},
"paths": {
"/compartments": {
"get": {
[...]

If you feel like it, copy its contents and paste them into the Swagger Editor. It should look like this:

Swagger Editor

Swagger Editor

Basic Configuration​

The description of our API does not yet reflect what it does, but entoas lets you change that! Open up ent/entc.go and pass in the updated title and description of our Fridge API:

ent/entc.go
//go:build ignore
// +build ignore

package main

import (
"log"

"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)

func main() {
ex, err := entoas.NewExtension(
entoas.SpecTitle("Fridge CMS"),
entoas.SpecDescription("API to manage fridges and their cooled contents. **ICY!**"),
entoas.SpecVersion("0.0.1"),
)
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Rerunning the code generator will create an updated OAS document.

ent/openapi.json
{
"info": {
"title": "Fridge CMS",
"description": "API to manage fridges and their cooled contents. **ICY!**",
"termsOfService": "",
"contact": {},
"license": {
"name": ""
},
"version": "0.0.1"
},
"paths": {
"/compartments": {
"get": {
[...]

Operation configuration​

There are times when you do not want to generate endpoints for every operation for every node. Fortunately, entoas lets us configure what endpoints to generate and which to ignore. entoas' default policy is to expose all routes. You can either change this behaviour to not expose any route but those explicitly asked for, or you can just tell entoas to exclude a specific operation by using an entoas.Annotation. Policies are used to enable / disable the generation of sub-resource operations as well:

ent/schema/fridge.go
// Edges of the Fridge.
func (Fridge) Edges() []ent.Edge {
return []ent.Edge{
edge.To("compartments", Compartment.Type).
// Do not generate an endpoint for POST /fridges/{id}/compartments
Annotations(
entoas.CreateOperation(
entoas.OperationPolicy(entoas.PolicyExclude),
),
),
}
}

// Annotations of the Fridge.
func (Fridge) Annotations() []schema.Annotation {
return []schema.Annotation{
// Do not generate an endpoint for DELETE /fridges/{id}
entoas.DeleteOperation(entoas.OperationPolicy(entoas.PolicyExclude)),
}
}

And voilà! the operations are gone.

For more information about how entoas's policies work and what you can do with it, have a look at the godoc.

Simple Models​

By default entoas generates one response-schema per endpoint. To learn about the naming strategy have a look at the godoc.

One Schema per Endpoint

One Schema per Endpoint

Many users have requested to change this behaviour to simply map the Ent schema to the OAS document. Therefore, you now can configure entoas to do that:

ex, err := entoas.NewExtension(
entoas.SpecTitle("Fridge CMS"),
entoas.SpecDescription("API to manage fridges and their cooled contents. **ICY!**"),
entoas.SpecVersion("0.0.1"),
entoas.SimpleModels(),
)
Simple Schemas

Simple Schemas

Wrapping Up​

In this post we announced entoas, the official integration of the former elk OpenAPI Specification generation into Ent. This feature connects between Ent's code-generation capabilities and OpenAPI/Swagger's rich tooling ecosystem.

Have questions? Need help with getting started? Feel free to join our Discord server or Slack channel.

For more Ent news and updates: