Saltar al contenido principal

Generación automática de CRUD REST con Ent y ogen

· 11 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

A finales de 2021 anunciamos que Ent había obtenido una nueva extensión oficial para generar un documento OpenAPI Specification completamente compatible: entoas.

Hoy nos complace anunciar una nueva extensión diseñada para funcionar con entoas: ogent. Utiliza la potencia de ogen (sitio web) para proporcionar una implementación de tipo seguro y sin reflexión del documento OpenAPI Specification generado por entoas.

ogen es un generador de código Go con opiniones definidas para documentos OpenAPI Specification v3. ogen genera implementaciones tanto de servidor como de cliente para cualquier documento OpenAPI Specification. Lo único que debe hacer el usuario es implementar una interfaz para acceder a la capa de datos de su aplicación. ogen incluye muchas características interesantes, como la integración con OpenTelemetry. Asegúrate de probarlo y mostrarle algo de aprecio.

La extensión presentada en este artículo sirve de puente entre Ent y el código generado por ogen. Utiliza la configuración de entoas para generar las partes faltantes del código de ogen.

El siguiente diagrama muestra cómo interactúa Ent con las extensiones entoas y ogent, y el papel de ogen en este proceso.

Diagram

Diagram

Si eres nuevo en Ent y quieres aprender más sobre cómo conectarte a diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, visita el Tutorial de Configuración.

El código de este artículo está disponible en los ejemplos de los módulos.

Empezando

[]

Aunque Ent es compatible con versiones de Go 1.16+, ogen requiere que tengas al menos la versión 1.17.

Para utilizar la extensión ogent, emplea el paquete entc (ent codegen) como se describe aquí. Primero instala las extensiones entoas y ogent en tu módulo de Go:

go get ariga.io/ogent@main

Ahora sigue estos dos pasos para habilitarlas y configurar Ent para trabajar con las extensiones:

1. Crea un nuevo archivo Go llamado ent/entc.go y pega el siguiente contenido:

ent/entc.go
//go:build ignore

package main

import (
"log"

"ariga.io/ogent"
"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/ogen-go/ogen"
)

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

2. Edita el archivo ent/generate.go para ejecutar el archivo ent/entc.go:

ent/generate.go
package ent

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

¡Con estos pasos completados, todo está listo para generar un documento OAS e implementar código de servidor a partir de tu esquema!

Generar un servidor HTTP API CRUD

El primer paso para crear el servidor HTTP API es diseñar un grafo de esquemas Ent. Por brevedad, aquí tienes un esquema de ejemplo:

ent/schema/todo.go
package schema

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

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

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

El código anterior muestra la forma "Ent" de describir un grafo de esquemas. En este caso particular, hemos creado una entidad todo (tarea pendiente).

Ahora ejecuta el generador de código:

go generate ./...

Deberías ver varios archivos generados por el generador de código de Ent. El archivo llamado ent/openapi.json ha sido generado por la extensión entoas. Aquí tienes un vistazo:

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": {
"/todos": {
"get": {
[...]
Swagger Editor Example

Swagger Editor Example

Sin embargo, este artículo se centra en la implementación del servidor, por lo que nos interesa especialmente el directorio ent/ogent. Todos los archivos que terminan en _gen.go son generados por ogen. El archivo oas_server_gen.go contiene la interfaz que los usuarios de ogen deben implementar para ejecutar el servidor.

ent/ogent/oas_server_gen.go
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// CreateTodo implements createTodo operation.
//
// Creates a new Todo and persists it to storage.
//
// POST /todos
CreateTodo(ctx context.Context, req CreateTodoReq) (CreateTodoRes, error)
// DeleteTodo implements deleteTodo operation.
//
// Deletes the Todo with the requested ID.
//
// DELETE /todos/{id}
DeleteTodo(ctx context.Context, params DeleteTodoParams) (DeleteTodoRes, error)
// ListTodo implements listTodo operation.
//
// List Todos.
//
// GET /todos
ListTodo(ctx context.Context, params ListTodoParams) (ListTodoRes, error)
// ReadTodo implements readTodo operation.
//
// Finds the Todo with the requested ID and returns it.
//
// GET /todos/{id}
ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error)
// UpdateTodo implements updateTodo operation.
//
// Updates a Todo and persists changes to storage.
//
// PATCH /todos/{id}
UpdateTodo(ctx context.Context, req UpdateTodoReq, params UpdateTodoParams) (UpdateTodoRes, error)
}

ogent añade una implementación para ese manejador en el archivo ogent.go. Para ver cómo definir qué rutas generar y qué relaciones precargar, consulta la documentación de entoas.

Este es un ejemplo de una ruta READ generada:

// ReadTodo handles GET /todos/{id} requests.
func (h *OgentHandler) ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error) {
q := h.client.Todo.Query().Where(todo.IDEQ(params.ID))
e, err := q.Only(ctx)
if err != nil {
switch {
case ent.IsNotFound(err):
return &R404{
Code: http.StatusNotFound,
Status: http.StatusText(http.StatusNotFound),
Errors: rawError(err),
}, nil
case ent.IsNotSingular(err):
return &R409{
Code: http.StatusConflict,
Status: http.StatusText(http.StatusConflict),
Errors: rawError(err),
}, nil
default:
// Let the server handle the error.
return nil, err
}
}
return NewTodoRead(e), nil
}

Ejecutar el servidor

El siguiente paso es crear un archivo main.go y conectar todos los componentes para crear un servidor de aplicaciones que sirva la API Todo. La siguiente función main inicializa una base de datos SQLite en memoria, ejecuta las migraciones para crear las tablas necesarias y sirve la API descrita en ent/openapi.json en localhost:8080:

main.go
package main

import (
"context"
"log"
"net/http"

"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
"<your-project>/ent"
"<your-project>/ent/ogent"
_ "github.com/mattn/go-sqlite3"
)

func main() {
// Create ent client.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal(err)
}
// Run the migrations.
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatal(err)
}
// Start listening.
srv, err := ogent.NewServer(ogent.NewOgentHandler(client))
if err != nil {
log.Fatal(err)
}
if err := http.ListenAndServe(":8080", srv); err != nil {
log.Fatal(err)
}
}

Tras ejecutar el servidor con go run -mod=mod main.go, ya puedes interactuar con la API.

Primero, creemos un nuevo Todo. Para demostración, no enviaremos cuerpo en la solicitud:

curl -X POST -H "Content-Type: application/json" localhost:8080/todos
{
"error_message": "body required"
}

Como ves, ogen maneja este caso porque entoas marcó el cuerpo como obligatorio al intentar crear un nuevo recurso. Intentémoslo de nuevo, esta vez proporcionando un cuerpo:

curl -X POST -H "Content-Type: application/json" -d '{"title":"Give ogen and ogent a Star on GitHub"}'  localhost:8080/todos
{
"error_message": "decode CreateTodo:application/json request: invalid: done (field required)"
}

¡Ups! ¿Qué salió mal? ogen te respalda: el campo done es obligatorio. Para solucionarlo, ve a tu definición de esquema y marca el campo done como opcional:

ent/schema/todo.go
package schema

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

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

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

Como hemos modificado nuestra configuración, debemos regenerar el código y reiniciar el servidor:

go generate ./...
go run -mod=mod main.go

Ahora, al intentar crear el Todo de nuevo:

curl -X POST -H "Content-Type: application/json" -d '{"title":"Give ogen and ogent a Star on GitHub"}'  localhost:8080/todos
{
"id": 1,
"title": "Give ogen and ogent a Star on GitHub",
"done": false
}

¡Voilà! Hay un nuevo elemento Todo en la base de datos.

Supongamos que has completado tu Todo y has destacado tanto ogen como ogent (¡realmente deberías!). Marca el todo como completado con una solicitud PATCH:

curl -X PATCH -H "Content-Type: application/json" -d '{"done":true}'  localhost:8080/todos/1
{
"id": 1,
"title": "Give ogen and ogent a Star on GitHub",
"done": true
}

Añadir endpoints personalizados

Como ves, el Todo ahora está marcado como completado. Pero sería mejor tener una ruta adicional para marcar un Todo como hecho: PATCH todos/:id/done. Para lograrlo debemos hacer dos cosas: documentar la nueva ruta en nuestro documento OAS e implementar la ruta. Abordemos lo primero usando el constructor de mutaciones de entoas. Edita tu archivo ent/entc.go y añade la descripción de la ruta:

ent/entc.go
//go:build ignore

package main

import (
"log"

"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/ariga/ogent"
"github.com/ogen-go/ogen"
)

func main() {
spec := new(ogen.Spec)
oas, err := entoas.NewExtension(
entoas.Spec(spec),
entoas.Mutations(func(_ *gen.Graph, spec *ogen.Spec) error {
spec.AddPathItem("/todos/{id}/done", ogen.NewPathItem().
SetDescription("Mark an item as done").
SetPatch(ogen.NewOperation().
SetOperationID("markDone").
SetSummary("Marks a todo item as done.").
AddTags("Todo").
AddResponse("204", ogen.NewResponse().SetDescription("Item marked as done")),
).
AddParameters(ogen.NewParameter().
InPath().
SetName("id").
SetRequired(true).
SetSchema(ogen.Int()),
),
)
return nil
}),
)
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
ogent, err := ogent.NewExtension(spec)
if err != nil {
log.Fatalf("creating ogent extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ogent, oas))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Tras regenerar el código (go generate ./...), debería aparecer una nueva entrada en ent/openapi.json:

"/todos/{id}/done": {
"description": "Mark an item as done",
"patch": {
"tags": [
"Todo"
],
"summary": "Marks a todo item as done.",
"operationId": "markDone",
"responses": {
"204": {
"description": "Item marked as done"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "integer"
},
"required": true
}
]
}
Custom Endpoint

Custom Endpoint

El archivo ent/ogent/oas_server_gen.go generado por ogen también reflejará los cambios:

ent/ogent/oas_server_gen.go
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// CreateTodo implements createTodo operation.
//
// Creates a new Todo and persists it to storage.
//
// POST /todos
CreateTodo(ctx context.Context, req CreateTodoReq) (CreateTodoRes, error)
// DeleteTodo implements deleteTodo operation.
//
// Deletes the Todo with the requested ID.
//
// DELETE /todos/{id}
DeleteTodo(ctx context.Context, params DeleteTodoParams) (DeleteTodoRes, error)
// ListTodo implements listTodo operation.
//
// List Todos.
//
// GET /todos
ListTodo(ctx context.Context, params ListTodoParams) (ListTodoRes, error)
// MarkDone implements markDone operation.
//
// PATCH /todos/{id}/done
MarkDone(ctx context.Context, params MarkDoneParams) (MarkDoneNoContent, error)
// ReadTodo implements readTodo operation.
//
// Finds the Todo with the requested ID and returns it.
//
// GET /todos/{id}
ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error)
// UpdateTodo implements updateTodo operation.
//
// Updates a Todo and persists changes to storage.
//
// PATCH /todos/{id}
UpdateTodo(ctx context.Context, req UpdateTodoReq, params UpdateTodoParams) (UpdateTodoRes, error)
}

Si intentas ejecutar el servidor ahora, el compilador de Go mostrará errores porque el generador de código ogent no sabe implementar la nueva ruta. Debes hacerlo manualmente. Reemplaza el main.go actual con este archivo para implementar el nuevo método:

main.go
package main

import (
"context"
"log"
"net/http"

"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
"github.com/ariga/ogent/example/todo/ent"
"github.com/ariga/ogent/example/todo/ent/ogent"
_ "github.com/mattn/go-sqlite3"
)

type handler struct {
*ogent.OgentHandler
client *ent.Client
}

func (h handler) MarkDone(ctx context.Context, params ogent.MarkDoneParams) (ogent.MarkDoneNoContent, error) {
return ogent.MarkDoneNoContent{}, h.client.Todo.UpdateOneID(params.ID).SetDone(true).Exec(ctx)
}

func main() {
// Create ent client.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal(err)
}
// Run the migrations.
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatal(err)
}
// Create the handler.
h := handler{
OgentHandler: ogent.NewOgentHandler(client),
client: client,
}
// Start listening.
srv := ogent.NewServer(h)
if err := http.ListenAndServe(":8180", srv); err != nil {
log.Fatal(err)
}
}

Al reiniciar el servidor, puedes enviar esta solicitud para marcar un todo como completado:

curl -X PATCH localhost:8180/todos/1/done

Próximas mejoras

Hay mejoras planeadas para ogent, destacando especialmente una forma segura de añadir capacidades de filtrado a las rutas LIST mediante código generado con tipos seguros. Queremos escuchar primero tus comentarios.

Conclusión

En este artículo anunciamos ogent, el generador oficial de implementaciones para documentos de Especificación OpenAPI generados por entoas. Esta extensión aprovecha el poder de ogen, un generador de código Go extremadamente potente y rico en funciones para documentos OpenAPI v3, para proporcionar servidores de API RESTful HTTP listos para usar y extensibles.

Ten en cuenta que tanto ogen como entoas/ogent aún no han alcanzado su primera versión principal y están en desarrollo activo. No obstante, la API puede considerarse estable.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]