Creación de APIs Modernas con Go, OpenAPI y CUE
Introducción
En este curso, exploraremos la potente combinación de Go, OpenAPI y CUE para construir APIs robustas, seguras en tipos y bien documentadas. Este enfoque moderno aprovecha las fortalezas de cada tecnología para crear implementaciones de API mantenibles y validadas.
Tecnologías Clave
Lenguaje de Programación Go
Go proporciona una excelente base para construir servicios web con su:
-
Potente biblioteca estándar
-
Concurrencia integrada
-
Excelentes características de rendimiento
-
Sintaxis clara y mantenible
-
Rico ecosistema de herramientas y paquetes
Paquete HTTP de Go
El paquete net/http sirve como ambos:
Lado del servidor
-
Implementación nativa de servidor HTTP
-
Enrutamiento y manejo de solicitudes
-
Soporte para middleware
-
Utilidades de escritura de respuestas
Lado del cliente
-
Operaciones de cliente HTTP
-
Composición de solicitudes
-
Manejo de respuestas
-
Agrupación de conexiones
Lenguaje CUE
CUE (Configure, Unify, Execute) aporta varias capacidades cruciales:
Definición de Esquemas
-
Definiciones con seguridad de tipo
-
Especificaciones de restricciones
-
Composición y herencia
-
Valores predeterminados
Generación de Esquemas JSON
-
Generación de esquemas OpenAPI 3.0
-
Validación de tipos
-
Generación de documentación
-
Aplicación de contratos
Validación en Tiempo de Ejecución
-
Validación de solicitudes/respuestas
-
Verificaciones de consistencia de datos
-
Mensajería de errores
-
Aserciones de tipo
Objetivos
-
Diseñar APIs utilizando especificaciones OpenAPI 3.0
-
Definir esquemas utilizando el potente sistema de restricciones de CUE
-
Generar documentación OpenAPI a partir de definiciones CUE
-
Implementar APIs utilizando la biblioteca estándar de Go
-
Validar solicitudes y respuestas utilizando CUE
-
Crear implementaciones de API mantenibles y con seguridad de tipo
¿Por qué esta combinación?
Seguridad de Tipo
-
Tipado estático de Go
-
Sistema de restricciones de CUE
-
Definiciones de esquema de OpenAPI
Experiencia del Desarrollador
-
Clara separación de responsabilidades
-
Validación automatizada
-
APIs autodocumentadas
-
Formatos amigables para herramientas
Beneficios de Mantenimiento
-
Fuente única de verdad para tipos
-
Generación automática de esquemas
-
Validación en tiempo de ejecución
-
Definiciones claras de contratos
Guía de Instalación de Herramientas
Configuración del Entorno de Desarrollo
Instalación de Goenv
macOS (usando Homebrew)
brew install goenv
Linux/Unix
git clone https://github.com/go-nv/goenv.git ~/.goenv
Configurar Shell (añadir a ~/.bashrc o ~/.zshrc)
export GOENV_ROOT="$['HOME']/.goenv"
export PATH="$['GOENV_ROOT']/bin:$['PATH']"
eval "$(goenv init -)"
Instalación de Go usando Goenv
Listar versiones disponibles
goenv install --list
Instalar la última versión estable
goenv install 1.23.3
Establecer la versión global de Go
goenv global 1.23.3
Verificar instalación
go version
Instalación de CUE
Usando Go install
go install cuelang.org/go/cmd/cue@latest
Verificar instalación de CUE
cue version
El paquete Go net/http
El paquete Gonet/httpproporciona implementaciones de cliente y servidor HTTP. Se utiliza para realizar llamadas a la API y construir servidores web.
Características clave
-
Soporte para HTTP/1.1, HTTP2
-
Soporte TLS (HTTPS)
-
Abstracción de cliente y servidor HTTP
-
Soporte de enrutamiento a través de
http.ServeMux
Cliente HTTP
El tipohttp.Clientse utiliza para realizar solicitudes HTTP.
Creación de una solicitud HTTP GET
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
fmt.Println("Response Body:", string(body))
}
### Creando una solicitud HTTP POST
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
jsonData := []byte({"key": "value"})
resp, err := http.Post("https://api.example.com/data", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
fmt.Println("Response Status:", resp.Status)
}
## Servidor HTTP
El tipo `http.Server` se utiliza para implementar servidores HTTP.
### Servidor HTTP Básico
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}
### Uso de http.ServeMux para enrutamiento
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
server.ListenAndServe()
}
## Documentación del paquete Http
[https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
# Análisis de JSON con el paquete GJSON
## Resumen
- Analizador JSON rápido para Go
- Utiliza sintaxis de rutas para extraer valores de JSON
- No requiere serialización/deserialización
- Cero dependencias
- Seguro para hilos
## Características Clave
### Sintaxis de Ruta
{"name": {"first": "John"}}
// Access with: "name.first" -> "John"
### Tipos Soportados
- Cadenas de texto
- Números
- Booleanos
- Nulo
- Arrays
- Objetos
### Rendimiento
- Análisis muy rápido
- Sin sobrecarga de asignación
- Opera directamente en []byte
## Operaciones Comunes
value := gjson.Get(json, "path.to.value")
value.String() // Obtener como cadena
value.Int() // Obtener como entero
value.Array() // Obtener como array
value.Map() // Obtener como mapa
## Modificadores
- @reverse: Invertir array
- @flatten: Aplanar array
- @join: Unir elementos de array
- @valid: Validar JSON
## Casos de Uso
- Análisis de respuestas de API
- Manejo de configuración
- Extracción de datos JSON
- Operaciones JSON críticas para el rendimiento
## Enlaces de Documentación de GJSON
### Repositorio de GitHub
[https://github.com/tidwall/gjson](https://github.com/tidwall/gjson)
### Documentación de GoDoc
[https://pkg.go.dev/github.com/tidwall/gjson](https://pkg.go.dev/github.com/tidwall/gjson)
# Demo 1: Interactuar con la API gpt-4o de OpenAI desde Go
var (
apiURL = "https://api.openai.com/v1/chat/completions" // reemplazar con el endpoint real
apiKey = os.Getenv("OPENAI_API_KEY")
imagePath = "assets/from-go-apis-to-ai-enhanced-frontends.webp"
prompt = "Describe el paisaje en la imagen."
)
fmt.Println(fmt.Sprintf("Extrayendo información de: %v\n", imagePath))
// Cargar imagen y codificar como base64
imageBytes, err := os.ReadFile(imagePath)
if err != nil {
fmt.Println("Error al leer el archivo de imagen:", err)
return
}
imageBase64 := base64.StdEncoding.EncodeToString(imageBytes)
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": fmt.Sprintf("data:image/webp;base64, %s", imageBase64),
},
},
},
},
},
})
if err != nil {
fmt.Println("Error marshalling JSON:", err)
return
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
responseString := string(body)
// fmt.Println("Response:", responseString)
result := gjson.Get(responseString, "choices.0.message.content")
fmt.Println(result.Str)
# Lenguaje CUE (Configurar Unificar Ejecutar)
## Visión general
CUE es un lenguaje de configuración y restricciones creado por Google para ayudar a gestionar configuraciones complejas y la validación de datos.
## Conceptos clave
### Modelo de datos
- Modelo de mundo abierto
- Herencia y composición
- Fuertemente tipado
- Declarativo
### Características clave
#### Restricciones de tipo
#Person: {
name: string
age: int & >=0 & <=120
email: string & =~"^ [a-zA-Z0-9._%+-]+@ [a-zA-Z0-9.-]+\. [a-zA-Z]{2,}$"
}
#### Restricciones de valor
settings: {
timeout: int & >=0 & <=60
mode: "dev" | "prod" | "stage"
}
## Integración con Go
### Etiquetas de Struct de Go
type Person struct {
Name stringjson:"name" cue:"string"
Age int json:"age" cue:">=0 & <=120"
Email stringjson:"email"
}
### Ejemplo de Validación
import "cuelang.org/go/cue"
const schema =
#Person: {
name: string
age: int & >=0
email: string
}
func validate(data interface{}) error {
ctx := cue.context.New()
v := ctx.CompileString(schema)
return v.Validate()
}
## Casos de Uso Comunes
### Gestión de Configuración
#Database: {
host: string
port: int & >=1024 & <=65535
user: string
password: string
}
prod: #Database & {
host: "prod.db.example.com"
port: 5432
user: "admin"
}
### Validación de Datos
#APIConfig: {
endpoints: [...{
path: string & =~"^/"
method: "GET" | "POST" | "PUT" | "DELETE"
auth: bool | *true
}]
}
### Generación de OpenAPI
openapi: "3.0.0"
info: {
title: "My API"
version: "1.0.0"
}
## Herramientas
### Línea de Comandos
- cue eval
- cue fmt
- cue vet
- cue export
### Soporte IDE
- Extensión de VSCode
- Plugin de GoLand/IntelliJ
## Mejores Prácticas
### Definición de Esquema
- Usar el prefijo # para las definiciones
- Mantener las restricciones simples y claras
- Usar nombres significativos
### Manejo de Errores
if err := val.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
## Recursos
### Documentación Oficial
- https://cuelang.org/docs/
- https://pkg.go.dev/cuelang.org/go/cue
### Recursos de Aprendizaje
- Tutoriales oficiales
- Ejemplos de GitHub
- Guías de la comunidad
## Patrones Comunes
### Valores Predeterminados
#Config: {
debug: bool | *false
port: int | *8080
}
### Composición
#Base: {
version: string
}
#Service: #Base & {
name: string
ports: [...int]
}
## Relación con Go
### Puntos de Integración
- API directa de Go
- Soporte para etiquetas de struct
- Generación de código
- Validación en tiempo de ejecución
### Beneficios
- Seguridad de tipos
- Validación de esquemas
- Gestión de configuración
- Modelado de datos
### Casos de Uso
- Definiciones de API
- Archivos de configuración
- Validación de datos
- Generación de esquemas
## Mensajes de Error
invalid value "foo" (does not match ">=0 & <=120")
conflicting values false and true
field "required" not allowed
## Enlaces a la Documentación del Lenguaje CUE
### Recursos Oficiales
- Sitio Web Principal: [https://cuelang.org/](https://cuelang.org/)
- Documentación Oficial: [https://cuelang.org/docs/](https://cuelang.org/docs/)
- Referencia de la API: [https://pkg.go.dev/cuelang.org/go/cue](https://pkg.go.dev/cuelang.org/go/cue)
### Recursos de GitHub
- Repositorio Principal: [https://github.com/cue-lang/cue](https://github.com/cue-lang/cue)
- CUE Playground: [https://cuelang.org/play/](https://cuelang.org/play/)
### Recursos de Aprendizaje
- Guía de Referencia: [https://cuelang.org/docs/references/](https://cuelang.org/docs/references/)
- Especificación: [https://cuelang.org/docs/references/spec/](https://cuelang.org/docs/references/spec/)
# Demo 2: API Básica con Go y Cue
## Las Entidades
#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}
## Entidades como Esquema JSON / Yaml:
rm -rf contracts/basic_schema.yaml # delete if already exists
cue def contracts/user.cue -o contracts/basic_schema.yaml --out openapi+yaml # generate schema
cat contracts/basic_schema.yaml # mostrar el contenido del archivo resultante
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths: {}
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
pattern: ^ [A-Za-z ]+$
El Contrato OpenAPI:
Mejorando la salida anterior, incluimos manualmente la definición depaths.
# basic_schema.yaml
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/User'
responses:
'200':
description: User created
content:
application/json:
schema:
$['ref']: '#/components/schemas/User'
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
pattern: ^ [A-Za-z ]+$
El servidor:
package main
// basic_cueapi.go
import (
"encoding/json"
"fmt"
"log"
"net/http"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
)
type User struct {
ID stringjson:"id"
Name stringjson:"name"
}
const schema =
#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}
var ctx = cuecontext.New()
var userSchema = ctx.CompileString(schema)
func validateUser(u User) error {
val := ctx.Encode(u)
return val.Unify(userSchema.LookupPath(cue.ParsePath("#User"))).Err()
}
func userHandler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validateUser(user); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v", err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("POST /users", userHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Probemos esta API básica:
echo "Happy Path Scenario Result:"
echo ""
curl -X POST localhost:8080/users -d '{"id":"1","name":"John Doe"}' # happy path test
echo ""
echo ""
echo "Fail Validation Scenario Result:"
echo ""
curl -X POST localhost:8080/users -d '{"id":"1","name":"1234"}' # fail the schema validation
: Happy Path Scenario Result:
:
: {"id":"1","name":"John Doe"}
:
:
: Fail Validation Scenario Result:
:
: name: invalid value "1234" (out of bound =~"^ [A-Za-z ]+$")
# Demo 3: Interfaz de usuario de Swagger
## Conversión de Yaml a JSON
mkdir -p demos/demo3
go run cli.go y2j contracts/demo2.yaml demos/demo3/openapi.json
: contracts/demo2.yaml convertido exitosamente a demos/demo3/openapi.json
## Servidor Actualizado
package main
import (
"embed"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
)
//go:embed *
var content embed.FS
type User struct {
ID stringjson:"id"
Name stringjson:"name"
}
const schema =
#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}
var ctx = cuecontext.New()
var userSchema = ctx.CompileString(schema)
func validateUser(u User) error {
val := ctx.Encode(u)
return val.Unify(userSchema.LookupPath(cue.ParsePath("#User"))).Err()
}
func userHandler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validateUser(user); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v", err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func main() {
// Puntos finales de la API
http.HandleFunc("POST /users", userHandler)
// Servir la especificación OpenAPI
http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
spec, _ := content.ReadFile("openapi.json")
w.Write(spec)
})
// Servir la interfaz de usuario de Swagger
http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
tmpl.Execute(w, nil)
})
log.Printf("Servidor iniciando en http://localhost:8080")
log.Printf("Documentación de la API disponible en http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentación de la API
`
# Demo 4: Recuperación de Información de Imagen
## Puntos Destacados
### Gestión del tipo de imagen
El hook de React recupera el tipo de imagen directamente del objeto `File` de la imagen subida y establece el campo oculto
### Gestión del tamaño de la imagen
El tamaño de la imagen se aplica en la API a través de la longitud de cadena `min` y `max`.
Un cálculo aproximado del tamaño base64 de una cadena de 10MB:
- Primero, convierte 10MB a bytes:
- 10MB = 10 * 1024* 1024 = 10,485,760 bytes
- Aplica la fórmula: ceil(n / 3) * 4
- n = 10,485,760
- 10,485,760 / 3 = 3,495,253.33...
- ceil(3,495,253.33...) = 3,495,254
- 3,495,254 * 4 = 13,981,016 bytes
Así, una cadena de `10MB` será aproximadamente `13.98MB` cuando se codifique en base64 (13,981,016 bytes ≈ 13.98MB)
## Las Entidades
import "strings"
// Image upload contract
#ImageUpload: {
// Unique identifier
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Indicación de imagen
prompt: string & =~"^.{3,100}$" & =~"^ [A-Za-z0-9 -_.]+$"
// Imagen codificada en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Estado de carga de imagen
#ImageUploadStatus: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Indicación de imagen
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Estado de carga de imagen
status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}
Entidades como esquema JSON / Yaml:
rm -rf contracts/image.yaml # eliminar si ya existe
cue def contracts/image.cue -o contracts/image.yaml --out openapi+yaml # generar esquema
cat contracts/image.yaml # mostrar el contenido del archivo resultante
openapi: 3.0.0
info:
title: Generado por cue.
version: no version
paths: {}
components:
schemas:
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Estado de carga de imagen
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -_.]+$
status:
description: Estado de carga de imagen
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -_.]+$
El Contrato OpenAPI:
Incluimos manualmente la definición depathsen la salida anterior.
openapi: 3.0.0
info:
title: Contrato de carga de imagen
version: 1.0.0
paths:
/extract-image-info:
post:
summary: Extraer información de imagen
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Imagen procesada con éxito
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
'400':
description: Fallo en el procesamiento de la imagen
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
components:
schemas:
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Estado de carga de imagen
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -.]+$
status:
description: Estado de carga de la imagen
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -.]+$
## Conversión de Yaml a JSON
mkdir -p demos/demo4
go run cli.go y2j contracts/demo4.yaml demos/demo4/openapi.json
: contracts/demo4.yaml convertido exitosamente a demos/demo4/openapi.json
## El servidor:
package main
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/tidwall/gjson"
)
var (
apiURL = "https://api.openai.com/v1/chat/completions" // replace with actual endpoint
apiKey = os.Getenv("OPENAI_API_KEY")
)
//go:embed *
var content embed.FS
type ImageUpload struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Blob stringjson:"blob"
}
type ImageUploadStatus struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Status stringjson:"status"
}
const schema =`
import "strings"
// Contrato de carga de imagen
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Mensaje de imagen
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"
// Imagen codificada en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Estado de carga de imagen
#ImageUploadStatus: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Mensaje de imagen
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Estado de carga de imagen
status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}
`
var ctx = cuecontext.New()
var compiledSchema = ctx.CompileString(schema)
func validateImageUpload(p ImageUpload) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUpload"))).Err()
}
func validateImageUploadStatus(p ImageUploadStatus) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUploadStatus"))).Err()
}
func processImageUploadHandler(w http.ResponseWriter, r *http.Request) {
var (
image ImageUpload
status ImageUploadStatus
)
if err := json.NewDecoder(r.Body).Decode(&image); err != nil {
fmt.Println(fmt.Errorf("BAD_PAYLOAD:::: +%v", err))
status = ImageUploadStatus{
ID: "bad-id",
Prompt: "bad-prompt",
Status: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}
if err := validateImageUpload(image); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageUploadStatus{
ID: "bad-id",
Prompt: "bad-prompt",
Status: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}
if err, info := getInfoFromImage(image.Blob, image.Prompt); err != nil {
fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageUploadStatus{
ID: "bad-id",
Prompt: "bad-prompt",
Status: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
} else {
w.Header().Set("Content-Type", "application/json")
status = ImageUploadStatus{
ID: image.ID,
Prompt: image.Prompt,
Status: info,
}
fmt.Println(fmt.Sprintf("Extracted Image Data; +%v", status))
json.NewEncoder(w).Encode(status)
}
}
func main() {
// Puntos finales de la API
http.HandleFunc("POST /extract-image-info", processImageUploadHandler)
// Servir especificación OpenAPI
http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
spec, _ := content.ReadFile("openapi.json")
w.Write(spec)
})
// Servir interfaz de usuario de Swagger
http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
tmpl.Execute(w, nil)
})
log.Printf("Servidor iniciando en http://localhost:8080")
log.Printf("Documentación de la API disponible en http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentación de la API
`
func getInfoFromImage(imageUrl, prompt string) (error, string) {
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": imageUrl,
},
},
},
},
},
})
if err != nil {
return fmt.Errorf("Error marshalling JSON: %w", err), ""
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
return fmt.Errorf("Error creating request: %w", err), ""
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error making request: %w", err), ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Error reading response body: %w", err), ""
}
responseString := string(body)
// fmt.Println("Response:", responseString)
result := gjson.Get(responseString, "choices.0.message.content")
response := result.Str
fmt.Println("Response:", response)
return nil, response
}
Demostración 5: Transmisión Triple
Entidades
import "strings"
// Contrato de carga de imagen
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Indicación de imagen
prompt: string & =~"^.{3,100}$" & =~"^ [A-Za-z0-9 -_.]+$"
// Transmisión habilitada
stream: bool
// Imagen codificada en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrato de información de imagen
#ImageInfo: {
// Información de imagen
info: string
}
## Entidades como esquema JSON / Yaml:
rm -rf contracts/image2.yaml # eliminar si ya existe
cue def contracts/image2.cue -o contracts/image2.yaml --out openapi+yaml # generar esquema
cat contracts/image2.yaml # mostrar el contenido del archivo resultante
openapi: 3.0.0
info:
title: Generado por cue.
version: sin versión
paths: {}
components:
schemas:
ImageInfo:
description: Contrato de información de imagen
type: object
required:
- info
properties:
info:
description: Información de imagen
type: string
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Stream habilitado
type: boolean
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
El Contrato OpenAPI:
Incluimos manualmente la definición de paths en la salida anterior.
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths:
/extract-image-info:
post:
summary: Extraer información de la imagen
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Imagen procesada correctamente
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
'400':
description: Falló el procesamiento de la imagen
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
components:
schemas:
ImageInfo:
description: Contrato de información de imagen
type: object
required:
- info
properties:
info:
description: Información de la imagen
type: string
ImageUpload:
description: Contrato de carga de imagen
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Mensaje de imagen
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Transmisión habilitada
type: boolean
blob:
description: Imagen codificada en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
## Conversión de Yaml a JSON
mkdir -p demos/demo5
go run cli.go y2j contracts/demo5.yaml demos/demo5/openapi.json
: contracts/demo5.yaml se ha convertido correctamente a demos/demo5/openapi.json
## El servidor:
package main
import (
"bufio"
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/tidwall/gjson"
)
var (
apiURL = "https://api.openai.com/v1/chat/completions" // reemplazar con el endpoint real
apiKey = os.Getenv("OPENAI_API_KEY")
)
//go:embed *
var content embed.FS
type ImageUpload struct {
ID stringjson:"id"
Prompt stringjson:"prompt"
Stream bool json:"stream"
Blob stringjson:"blob"
}
type ImageInfo struct {
Info stringjson:"info"
}
const schema =`
import "strings"
// Contrato de carga de imagen
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Mensaje de imagen
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"
// Transmisión habilitada
stream: bool
// Imagen codificada en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrato de información de imagen
#ImageInfo: {
// Información de imagen
info: string
}
`
var ctx = cuecontext.New()
var compiledSchema = ctx.CompileString(schema)
func validateImageUpload(p ImageUpload) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUpload"))).Err()
}
func validateImageInfoStatus(p ImageInfo) error {
val := ctx.Encode(p)
return val.Unify(compiledSchema.LookupPath(cue.ParsePath("#ImageUploadStatus"))).Err()
}
func processImageUploadHandler(w http.ResponseWriter, r *http.Request) {
var (
image ImageUpload
status ImageInfo
)
if err := json.NewDecoder(r.Body).Decode(&image); err != nil {
fmt.Println(fmt.Errorf("BAD_PAYLOAD:::: +%v", err))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}
if err := validateImageUpload(image); err != nil {
fmt.Println(fmt.Errorf("INVALID_PAYLOAD:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
}
if image.Stream {
// Establece los encabezados CORS para permitir todos los orígenes. Es posible que desees restringir esto a orígenes específicos en un entorno de producción.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
//w.Header().Set("Access-Control-Max-Age", "3600")
// Establece el tipo de contenido a text/event-stream
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
if err := getInfoFromImageStreaming(w, image.Blob, image.Prompt); err != nil {
fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
fmt.Fprintf(w, "data: %s\n\n", err.Error())
w.(http.Flusher).Flush()
fmt.Fprintf(w, "data: %s\n\n", " [DONE]")
w.(http.Flusher).Flush()
return
}
} else {
if err, info := getInfoFromImage(image.Blob, image.Prompt); err != nil {
fmt.Println(fmt.Errorf("INFO_RETRIEVAL_ERROR:::: +%v with image size: %d", err, len(image.Blob)))
status = ImageInfo{
Info: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(status)
return
} else {
w.Header().Set("Content-Type", "application/json")
status = ImageInfo{
Info: info,
}
fmt.Println(fmt.Sprintf("Extracted Image Data; +%v", status))
json.NewEncoder(w).Encode(status)
}
}
}
func main() {
// API endpoints
http.HandleFunc("POST /extract-image-info", processImageUploadHandler)
// Serve OpenAPI spec
http.HandleFunc("GET /openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
spec, _ := content.ReadFile("openapi.json")
w.Write(spec)
})
// Serve Swagger UI
http.HandleFunc("GET /docs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tmpl := template.Must(template.New("swagger").Parse(swaggerTemplate))
tmpl.Execute(w, nil)
})
log.Printf("Server starting on http://localhost:8080")
log.Printf("API documentation available at http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentación de la API
`
func getInfoFromImageStreaming(w http.ResponseWriter, imageUrl, prompt string) error {
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": imageUrl,
},
},
},
},
},
"stream": true, // Enable streaming
})
if err != nil {
return fmt.Errorf("Error marshalling JSON: %w", err)
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
return fmt.Errorf("Error creating request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error al realizar la solicitud: %w", err)
}
defer resp.Body.Close()
// Comprobar el código de estado no OK
if resp.StatusCode != http.StatusOK {
respBody, _ := bufio.NewReader(resp.Body).ReadString('\n')
fmt.Printf("Código de estado no OK: %d\nCuerpo: %s\n", resp.StatusCode, respBody)
return fmt.Errorf("Código de estado no OK: %d", resp.StatusCode)
}
// Usar bufio para leer la respuesta línea por línea
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// OpenAI transmite cada fragmento prefijado por "data:"
if !strings.HasPrefix(line, "data:") {
continue
}
// "data: [DONE]" indica el final del flujo
jsonData := strings.TrimPrefix(line, "data: ")
if jsonData == " [DONE]" {
fmt.Println("\n--- Stream finished ---")
fmt.Fprintf(w, "data: %s\n\n", " [DONE]")
w.(http.Flusher).Flush()
break
}
// Analizar cada fragmento usando gjson
result := gjson.Parse(jsonData)
// El contenido relevante está en "choices" -> array -> "delta" -> "content"
// p. ej., result.Get("choices.0.delta.content")
content := ""
for _, choice := range result.Get("choices").Array() {
contentDelta := choice.Get("delta.content").String()
content += contentDelta
}
// Imprimir el fragmento de contenido (si lo hay)
if content != "" {
fmt.Print(content)
fmt.Fprintf(w, "data: %s\n\n", content)
w.(http.Flusher).Flush()
}
// Esperar un poco para simular el tiempo de procesamiento
time.Sleep(100 * time.Millisecond)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("Error reading streamed response body: %w", err)
}
return nil
}
func getInfoFromImage(imageUrl, prompt string) (error, string) {
requestBody, err := json.Marshal(map [string]any{
"model": "gpt-4o",
"max_tokens": 4096,
"messages": []map [string]any{
{
"role": "user",
"content": []map [string]any{
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": map [string]any{
"url": imageUrl,
},
},
},
},
},
})
if err != nil {
return fmt.Errorf("Error marshalling JSON: %w", err), ""
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))
if err != nil {
return fmt.Errorf("Error creating request: %w", err), ""
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error making request: %w", err), ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Error reading response body: %w", err), ""
}
responseString := string(body)
// fmt.Println("Response:", responseString)
result := gjson.Get(responseString, "choices.0.message.content")
response := result.Str
fmt.Println("Response:", response)
return nil, response
}
