Construindo APIs Modernas com Go, OpenAPI e CUE
Introdução
Neste curso, exploraremos a poderosa combinação de Go, OpenAPI e CUE para construir APIs robustas, com segurança de tipo e bem documentadas. Esta abordagem moderna aproveita os pontos fortes de cada tecnologia para criar implementações de API manteníveis e validadas.
Tecnologias Principais
Linguagem de Programação Go
Go oferece uma excelente base para a construção de serviços web com sua:
-
Biblioteca padrão robusta
-
Concorrência integrada
-
Excelentes características de desempenho
-
Sintaxe clara e mantenível
-
Ecossistema rico de ferramentas e pacotes
Pacote HTTP do Go
O pacote net/http serve como ambos:
Lado do Servidor
-
Implementação nativa de servidor HTTP
-
Roteamento e tratamento de requisições
-
Suporte a middleware
-
Utilitários de escrita de resposta
Lado do Cliente
-
Operações de cliente HTTP
-
Composição de requisições
-
Tratamento de respostas
-
Pool de conexões
Linguagem CUE
CUE (Configure, Unify, Execute) oferece diversas capacidades cruciais:
Definição de Esquema
-
Definições com segurança de tipo
-
Especificações de restrições
-
Composição e herança
-
Valores padrão
Geração de Esquema JSON
-
Geração de esquema OpenAPI 3.0
-
Validação de tipo
-
Geração de documentação
-
Aplicação de contrato
Validação em Tempo de Execução
-
Validação de requisição/resposta
-
Verificações de consistência de dados
-
Mensagens de erro
-
Asserções de tipo
Objetivos
-
Projetar APIs usando especificações OpenAPI 3.0
-
Definir esquemas usando o poderoso sistema de restrições do CUE
-
Gerar documentação OpenAPI a partir de definições CUE
-
Implementar APIs usando a biblioteca padrão do Go
-
Validar requisições e respostas usando CUE
-
Criar implementações de API manteníveis e com segurança de tipo
Por Que Esta Combinação?
Segurança de Tipo
-
Tipagem estática do Go
-
Sistema de restrições do CUE
-
Definições de esquema do OpenAPI
Experiência do Desenvolvedor
-
Clara separação de preocupações
-
Validação automatizada
-
APIs autodocumentadas
-
Formatos amigáveis para ferramentas
Benefícios de Manutenção
-
Fonte única de verdade para tipos
-
Geração automatizada de esquemas
-
Validação em tempo de execução
-
Definições claras de contrato
Guia de Instalação de Ferramentas
Configurando o Ambiente de Desenvolvimento
Instalando o Goenv
macOS (usando Homebrew)
brew install goenv
Linux/Unix
git clone https://github.com/go-nv/goenv.git ~/.goenv
Configurar Shell (adicionar a ~/.bashrc ou ~/.zshrc)
export GOENV_ROOT="$['HOME']/.goenv"
export PATH="$['GOENV_ROOT']/bin:$['PATH']"
eval "$(goenv init -)"
Instalando Go usando Goenv
Listar versões disponíveis
goenv install --list
Instalar a versão estável mais recente
goenv install 1.23.3
Definir a versão global do Go
goenv global 1.23.3
Verificar a instalação
go version
Instalando CUE
Usando Go install
go install cuelang.org/go/cmd/cue@latest
Verificar a instalação do CUE
cue version
O Pacote Go net/http
O pacote Gonet/httpfornece implementações de cliente e servidor HTTP. É usado para fazer chamadas de API e construir servidores web.
Principais Recursos
-
Suporte para HTTP/1.1, HTTP2
-
Suporte a TLS (HTTPS)
-
Abstração de cliente e servidor HTTP
-
Suporte a roteamento via
http.ServeMux
Cliente HTTP
O tipohttp.Clienté usado para realizar requisições HTTP.
Criando uma Requisição 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))
}
Criando uma Requisição 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
O tipo http.Server é usado 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)
}
Usando http.ServeMux para Roteamento
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()
}
Documentação do pacote Http
Analisando JSON com o Pacote GJSON
Visão Geral
-
Analisador JSON rápido para Go
-
Usa sintaxe de caminho para extrair valores de JSON
-
Nenhuma serialização/desserialização necessária
-
Zero dependências
-
Thread-safe
Principais Recursos
Sintaxe de Caminho
{"name": {"first": "John"}}
// Acessar com: "name.first" -> "John"
Tipos Suportados
-
Strings
-
Números
-
Booleanos
-
Nulo
-
Arrays
-
Objetos
Desempenho
-
Análise muito rápida
-
Sem sobrecarga de alocação
-
Opera diretamente em []byte
Operações Comuns
value := gjson.Get(json, "path.to.value")
value.String() // Obter como string
value.Int() // Obter como inteiro
value.Array() // Obter como array
value.Map() // Obter como mapa
Modificadores
-
@reverse: Inverter array
-
@flatten: Achatar array
-
@join: Unir elementos do array
-
@valid: Validar JSON
Casos de Uso
-
Análise de respostas de API
-
Manipulação de configuração
-
Extração de dados JSON
-
Operações JSON críticas para o desempenho
Links da Documentação GJSON
Repositório GitHub
https://github.com/tidwall/gjson
Documentação GoDoc
https://pkg.go.dev/github.com/tidwall/gjson
Demonstração 1: Interaja com a API gpt-4o da OpenAI a partir de Go
var (
apiURL = "https://api.openai.com/v1/chat/completions" // substitua pelo endpoint real
apiKey = os.Getenv("OPENAI_API_KEY")
imagePath = "assets/from-go-apis-to-ai-enhanced-frontends.webp"
prompt = "Descreva a paisagem na imagem."
)
fmt.Println(fmt.Sprintf("Extraindo Informações de: %v\n", imagePath))
// Carrega a imagem e codifica como base64
imageBytes, err := os.ReadFile(imagePath)
if err != nil {
fmt.Println("Erro ao ler o arquivo de imagem:", 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)
Linguagem CUE (Configurar Unificar Executar)
Visão Geral
CUE é uma linguagem de configuração e restrição criada pelo Google para ajudar a gerenciar configurações complexas e validação de dados.
Conceitos Essenciais
Modelo de Dados
-
Modelo de mundo aberto
-
Herança e composição
-
Fortemente tipado
-
Declarativo
Principais Recursos
Restrições de Tipo
#Person: {
name: string
age: int & >=0 & <=120
email: string & =~"^ [a-zA-Z0-9._%+-]+@ [a-zA-Z0-9.-]+\\. [a-zA-Z]{2,}$"
}
Restrições de Valor
settings: {
timeout: int & >=0 & <=60
mode: "dev" | "prod" | "stage"
}
Integração com Go
Tags de Struct Go
type Person struct {
Name string`json:"name" cue:"string"`
Age int `json:"age" cue:">=0 & <=120"`
Email string`json:"email"`
}
Exemplo de Validação
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 Comuns
Gerenciamento de Configuração
#Database: {
host: string
port: int & >=1024 & <=65535
user: string
password: string
}
prod: #Database & {
host: "prod.db.example.com"
port: 5432
user: "admin"
}
Validação de Dados
#APIConfig: {
endpoints: [...{
path: string & =~"^/"
method: "GET" | "POST" | "PUT" | "DELETE"
auth: bool | *true
}]
}
Geração OpenAPI
openapi: "3.0.0"
info: {
title: "My API"
version: "1.0.0"
}
Ferramentas
Linha de Comando
-
cue eval
-
cue fmt
-
cue vet
-
cue export
Suporte a IDE
-
VSCode extension
-
GoLand/IntelliJ plugin
Melhores Práticas
Definição de Esquema
-
Use o prefixo # para definições
-
Mantenha as restrições simples e claras
-
Use nomes significativos
Tratamento de Erros
if err := val.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
Recursos
Documentação Oficial
Recursos de Aprendizagem
-
Tutoriais oficiais
-
Exemplos do GitHub
-
Guias da comunidade
Padrões Comuns
Valores Padrão
#Config: {
debug: bool | *false
port: int | *8080
}
Composição
#Base: {
version: string
}
#Service: #Base & {
name: string
ports: [...int]
}
Relação com Go
Pontos de Integração
-
API Go Direta
-
Suporte a tags de struct
-
Geração de código
-
Validação em tempo de execução
Benefícios
-
Segurança de tipo
-
Validação de esquema
-
Gerenciamento de configuração
-
Modelagem de dados
Casos de Uso
-
Definições de API
-
Arquivos de configuração
-
Validação de dados
-
Geração de esquema
Mensagens de Erro
invalid value "foo" (does not match ">=0 & <=120")
conflicting values false and true
field "required" not allowed
Links da Documentação da Linguagem CUE
Recursos Oficiais
-
Site Principal: https://cuelang.org/
-
Documentação Oficial: https://cuelang.org/docs/
-
Referência da API: https://pkg.go.dev/cuelang.org/go/cue
Recursos do GitHub
-
Repositório Principal: https://github.com/cue-lang/cue
-
CUE Playground: https://cuelang.org/play/
Recursos de Aprendizagem
-
Guia de Referência: https://cuelang.org/docs/references/
-
Especificação: https://cuelang.org/docs/references/spec/
Demonstração 2: API Básica com Go e Cue
As Entidades
#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}
Entidades como Esquema JSON / Yaml:
rm -rf contracts/basic_schema.yaml # apagar se já existir
cue def contracts/user.cue -o contracts/basic_schema.yaml --out openapi+yaml # gerar esquema
cat contracts/basic_schema.yaml # mostrar o conteúdo do arquivo 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 ]+$
## O Contrato OpenAPI:
Melhorando a saída anterior, incluímos manualmente a definição de`paths`.
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 ]+$
## O 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))
}
Vamos testar 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 ]+$")
# Demonstração 3: Swagger UI
## Conversão de Yaml para JSON
mkdir -p demos/demo3
go run cli.go y2j contracts/demo2.yaml demos/demo3/openapi.json
: contracts/demo2.yaml convertido com sucesso para demos/demo3/openapi.json
## Servidor Atualizado
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() {
// Endpoints da API
http.HandleFunc("POST /users", userHandler)
// Servir especificação 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 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("Servidor iniciando em http://localhost:8080")
log.Printf("Documentação da API disponível em http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentação da API
`
# Demonstração 4: Recuperação de Informações de Imagem
## Destaques
### Gerenciamento de tipo de imagem
O hook React recupera o tipo de imagem diretamente do objeto `File` da imagem carregada e define o campo oculto
### Gerenciamento de tamanho de imagem
O tamanho da imagem é imposto na API via comprimento de string `min` e `max`.
Um cálculo aproximado do tamanho base64 de uma string de 10MB:
- Primeiro, converta 10MB para bytes:
- 10MB = 10 * 1024* 1024 = 10,485,760 bytes
- Aplique a 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
Assim, uma string de `10MB` será aproximadamente `13.98MB` quando codificada em base64 (13,981,016 bytes ≈ 13.98MB)
## As Entidades
import "strings"
// Image upload contract
#ImageUpload: {
// Unique identifier
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"
// Imagem codificada em Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Status de upload da imagem
#ImageUploadStatus: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Status de upload da imagem
status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}
## Entidades como Schema JSON / Yaml:
rm -rf contracts/image.yaml # excluir se já existir
cue def contracts/image.cue -o contracts/image.yaml --out openapi+yaml # gerar schema
cat contracts/image.yaml # mostrar o conteúdo do arquivo resultante
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths: {}
components:
schemas:
ImageUpload:
description: Contrato de upload de imagem
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Imagem codificada em Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Status do upload da imagem
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -_.]+$
status:
description: Status de upload da imagem
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -_.]+$
O Contrato OpenAPI:
Incluímos manualmente a definição depathsà saída anterior.
openapi: 3.0.0
info:
title: Contrato de upload de imagem
version: 1.0.0
paths:
/extract-image-info:
post:
summary: Extrair Informações da Imagem
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Imagem processada com sucesso
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
'400':
description: Falha no processamento da imagem
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
components:
schemas:
ImageUpload:
description: Contrato de upload de imagem
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Imagem codificada em Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Status de upload da imagem
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -.]+$
status:
description: Status de upload da imagem
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -.]+$
## Conversão de Yaml para JSON
mkdir -p demos/demo4
go run cli.go y2j contracts/demo4.yaml demos/demo4/openapi.json
: Successfully converted contracts/demo4.yaml to demos/demo4/openapi.json
## O 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 upload de imagem
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"
// Imagem codificada em Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Status de upload de imagem
#ImageUploadStatus: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Status de upload de imagem
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() {
// 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 =`
Documentação da 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
}
# Demonstração 5: Streaming Triplo
sequenceDiagram
participant LLM as Large Language Model
participant BE as Backend
participant UI as UI Layer
participant BR as Browser
BR->>UI: 1. Requisição (prompt/interação)
UI->>BE: 2. Encaminhar prompt
BE->>LLM: 3. Chamar LLM com stream=true
Note right of LLM: LLM gera
tokens incrementalmente
LLM-->>BE: 4. Transmitir tokens parciais
Note right of BE: Backend retransmite tokens
à medida que chegam
BE-->>UI: 5. Transmitir tokens parciais
UI-->>BR: 6. Enviar tokens parciais para o navegador
Note right of BR: Navegador atualiza a UI em tempo real,
exibindo tokens à medida que são transmitidos
## Entidades
import "strings"
// Contrato de upload de imagem
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"
// Stream ativado
stream: bool
// Imagem codificada em Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrato de informações da imagem
#ImageInfo: {
// Informações da imagem
info: string
}
## Entidades como Esquema JSON / Yaml:
rm -rf contracts/image2.yaml # excluir se já existir
cue def contracts/image2.cue -o contracts/image2.yaml --out openapi+yaml # gerar esquema
cat contracts/image2.yaml # mostrar o conteúdo do arquivo resultante
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths: {}
components:
schemas:
ImageInfo:
description: Contrato de informações da imagem
type: object
required:
- info
properties:
info:
description: Informações da imagem
type: string
ImageUpload:
description: Contrato de upload de imagem
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Stream ativado
type: boolean
blob:
description: Imagem codificada em Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
O Contrato OpenAPI:
Incluímos manualmente a definição depathsà saída anterior.
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths:
/extract-image-info:
post:
summary: Extrair Informações da Imagem
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Imagem processada com sucesso
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
'400':
description: Falha no processamento da imagem
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
components:
schemas:
ImageInfo:
description: Contrato de informações da imagem
type: object
required:
- info
properties:
info:
description: Informações da imagem
type: string
ImageUpload:
description: Contrato de upload de imagem
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identificador único
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Prompt da imagem
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Stream habilitado
type: boolean
blob:
description: Imagem codificada em Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
## Conversão de Yaml para JSON
mkdir -p demos/demo5
go run cli.go y2j contracts/demo5.yaml demos/demo5/openapi.json
: Successfully converted contracts/demo5.yaml to demos/demo5/openapi.json
## O 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" // 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"
Stream bool json:"stream"
Blob stringjson:"blob"
}
type ImageInfo struct {
Info stringjson:"info"
}
const schema =`
import "strings"
// Contrato de upload de imagem
#ImageUpload: {
// Identificador único
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Prompt da imagem
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"
// Stream ativado
stream: bool
// Imagem codificada em Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrato de informações da imagem
#ImageInfo: {
// Informações da imagem
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 {
// Define cabeçalhos CORS para permitir todas as origens. Você pode querer restringir isso a origens específicas em um ambiente de produção.
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")
// Define o tipo de conteúdo como 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() {
// Endpoints da API
http.HandleFunc("POST /extract-image-info", processImageUploadHandler)
// Servir especificação 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 interface 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("Servidor iniciando em http://localhost:8080")
log.Printf("Documentação da API disponível em http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentação da 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 making request: %w", err)
}
defer resp.Body.Close()
// Verificar código de status não-OK
if resp.StatusCode != http.StatusOK {
respBody, _ := bufio.NewReader(resp.Body).ReadString('\n')
fmt.Printf("Non-OK status code: %d\nBody: %s\n", resp.StatusCode, respBody)
return fmt.Errorf("Non-OK status code: %d", resp.StatusCode)
}
// Usar bufio para ler a resposta linha por linha
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// OpenAI transmite cada pedaço prefixado por "data:"
if !strings.HasPrefix(line, "data:") {
continue
}
// "data: [DONE]" indica o fim do fluxo
jsonData := strings.TrimPrefix(line, "data: ")
if jsonData == " [DONE]" {
fmt.Println("\n--- Fluxo finalizado ---")
fmt.Fprintf(w, "data: %s\n\n", " [DONE]")
w.(http.Flusher).Flush()
break
}
// Analisa cada pedaço usando gjson
result := gjson.Parse(jsonData)
// O conteúdo relevante está em "choices" -> array -> "delta" -> "content"
// ex., result.Get("choices.0.delta.content")
content := ""
for _, choice := range result.Get("choices").Array() {
contentDelta := choice.Get("delta.content").String()
content += contentDelta
}
// Imprime o pedaço de conteúdo (se houver)
if content != "" {
fmt.Print(content)
fmt.Fprintf(w, "data: %s\n\n", content)
w.(http.Flusher).Flush()
}
// Dorme um pouco para simular o tempo de processamento
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
}
