Construction d'API Modernes avec Go, OpenAPI et CUE
Introduction
Dans ce cours, nous explorerons la puissante combinaison de Go, OpenAPI et CUE pour construire des API robustes, typées et bien documentées. Cette approche moderne tire parti des forces de chaque technologie pour créer des implémentations d'API maintenables et validées.
Technologies Clés
Langage de Programmation Go
Go offre une excellente base pour la construction de services web grâce à :
-
Sa solide bibliothèque standard
-
Sa concurrence intégrée
-
Ses excellentes caractéristiques de performance
-
Sa syntaxe claire et maintenable
-
Son riche écosystème d'outils et de packages
Le Package HTTP de Go
Le package net/http sert à la fois de :
Côté Serveur
-
Implémentation native de serveur HTTP
-
Routage et gestion des requêtes
-
Support des middlewares
-
Utilitaires d'écriture de réponses
Côté Client
-
Opérations client HTTP
-
Composition des requêtes
-
Gestion des réponses
-
Mise en commun des connexions
Langage CUE
CUE (Configure, Unify, Execute) apporte plusieurs capacités cruciales :
Définition de Schéma
-
Définitions de type sûr
-
Spécifications de contraintes
-
Composition et héritage
-
Valeurs par défaut
Génération de Schéma JSON
-
Génération de schéma OpenAPI 3.0
-
Validation de type
-
Génération de documentation
-
Application des contrats
Validation à l'exécution
-
Validation des requêtes/réponses
-
Vérifications de cohérence des données
-
Messagerie d'erreur
-
Assertions de type
Objectifs
-
Concevoir des API en utilisant les spécifications OpenAPI 3.0
-
Définir des schémas en utilisant le puissant système de contraintes de CUE
-
Générer de la documentation OpenAPI à partir des définitions CUE
-
Implémenter des API en utilisant la bibliothèque standard de Go
-
Valider les requêtes et les réponses en utilisant CUE
-
Créer des implémentations d'API maintenables et de type sûr
Pourquoi cette combinaison ?
Sécurité des types
-
Typage statique de Go
-
Système de contraintes de CUE
-
Définitions de schémas d'OpenAPI
Expérience Développeur
-
Séparation claire des préoccupations
-
Validation automatisée
-
API auto-documentées
-
Formats adaptés aux outils
Avantages de la Maintenance
-
Source unique de vérité pour les types
-
Génération de schémas automatisée
-
Validation à l'exécution
-
Définitions de contrats claires
Guide d'Installation des Outils
Configuration de l'Environnement de Développement
Installation de Goenv
macOS (avec Homebrew)
brew install goenv
Linux/Unix
git clone https://github.com/go-nv/goenv.git ~/.goenv
Configurer le Shell (ajouter à ~/.bashrc ou ~/.zshrc)
export GOENV_ROOT="$['HOME']/.goenv"
export PATH="$['GOENV_ROOT']/bin:$['PATH']"
eval "$(goenv init -)"
Installation de Go avec Goenv
Lister les versions disponibles
goenv install --list
Installer la dernière version stable
goenv install 1.23.3
Définir la version globale de Go
goenv global 1.23.3
Vérifier l'installation
go version
Installer CUE
Utiliser Go install
go install cuelang.org/go/cmd/cue@latest
Vérifier l'installation de CUE
cue version
Le package Go net/http
Le package Gonet/httpfournit des implémentations de client et de serveur HTTP. Il est utilisé pour effectuer des appels d'API et construire des serveurs web.
Fonctionnalités clés
-
Prise en charge de HTTP/1.1, HTTP2
-
Prise en charge de TLS (HTTPS)
-
Abstraction client et serveur HTTP
-
Prise en charge du routage via
http.ServeMux
Client HTTP
Le typehttp.Clientest utilisé pour effectuer des requêtes HTTP.
Créer une requête 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))
}
Création d'une requête 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)
}
Serveur HTTP
Le typehttp.Serverest utilisé pour implémenter des serveurs HTTP.
Serveur HTTP de base
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)
}
Utilisation de http.ServeMux pour le routage
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()
}
Documentation du package Http
Analyse de JSON avec le package GJSON
Aperçu
-
Analyseur JSON rapide pour Go
-
Utilise la syntaxe de chemin pour extraire des valeurs de JSON
-
Aucune sérialisation/désérialisation requise
-
Zéro dépendances
-
Thread-safe
Fonctionnalités Clés
Syntaxe des Chemins
{"name": {"first": "John"}}
// Access with: "name.first" -> "John"
Types Pris en Charge
-
Chaînes de caractères
-
Nombres
-
Booléens
-
Null
-
Tableaux
-
Objets
Performances
-
Analyse très rapide
-
Pas de surcharge d'allocation
-
Opère directement sur []byte
Opérations Courantes
value := gjson.Get(json, "path.to.value")
value.String() // Get as string
value.Int() // Get as integer
value.Array() // Get as array
value.Map() // Get as map
Modificateurs
-
@reverse: Inverser le tableau
-
@flatten: Aplatir le tableau
-
@join: Joindre les éléments du tableau
-
@valid: Valider le JSON
Cas d'Utilisation
-
Analyse des réponses d'API
-
Gestion de la configuration
-
Extraction de données JSON
-
Opérations JSON critiques en termes de performances
Liens de Documentation GJSON
Dépôt GitHub
https://github.com/tidwall/gjson
Documentation GoDoc
https://pkg.go.dev/github.com/tidwall/gjson
Démo 1 : Interagir avec l'API gpt-4o d'OpenAI depuis Go
var (
apiURL = "https://api.openai.com/v1/chat/completions" // remplacer par le point de terminaison réel
apiKey = os.Getenv("OPENAI_API_KEY")
imagePath = "assets/from-go-apis-to-ai-enhanced-frontends.webp"
prompt = "Décrivez le paysage de l'image."
)
fmt.Println(fmt.Sprintf("Extraction d'informations de : %v\n", imagePath))
// Charger l'image et l'encoder en base64
imageBytes, err := os.ReadFile(imagePath)
if err != nil {
fmt.Println("Erreur lors de la lecture du fichier image :", 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()
```go
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)
Langage CUE (Configure Unify Execute)
Aperçu
CUE est un langage de configuration et de contrainte créé par Google pour aider à gérer les configurations complexes et la validation des données.
Concepts Clés
Modèle de Données
-
Modèle de monde ouvert
-
Héritage et composition
-
Fortement typé
-
Déclaratif
Fonctionnalités Clés
Contraintes de Type
#Person: {
name: string
age: int & >=0 & <=120
email: string & =~"^ [a-zA-Z0-9._%+-]+@ [a-zA-Z0-9.-]+\\. [a-zA-Z]{2,}$"
}
Contraintes de Valeur
settings: {
timeout: int & >=0 & <=60
mode: "dev" | "prod" | "stage"
}
Intégration avec Go
Balises de structure Go
type Person struct {
Name string`json:"name" cue:"string"`
Age int `json:"age" cue:">=0 & <=120"`
Email string`json:"email"`
}
Exemple de validation
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()
}
Cas d'utilisation courants
Gestion de la configuration
#Database: {
host: string
port: int & >=1024 & <=65535
user: string
password: string
}
prod: #Database & {
host: "prod.db.example.com"
port: 5432
user: "admin"
}
Validation des données
#APIConfig: {
endpoints: [...{
path: string & =~"^/"
method: "GET" | "POST" | "PUT" | "DELETE"
auth: bool | *true
}]
}
Génération OpenAPI
openapi: "3.0.0"
info: {
title: "My API"
version: "1.0.0"
}
Outillage
Ligne de commande
-
cue eval
-
cue fmt
-
cue vet
-
cue export
Support IDE
-
Extension VSCode
-
Plugin GoLand/IntelliJ
Bonnes pratiques
Définition de schéma
-
Utiliser le préfixe # pour les définitions
-
Garder les contraintes simples et claires
-
Utiliser des noms significatifs
Gestion des erreurs
if err := val.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
Ressources
Documentation officielle
Ressources d'apprentissage
-
Tutoriels officiels
-
Exemples GitHub
-
Guides de la communauté
Modèles courants
Valeurs par défaut
#Config: {
debug: bool | *false
port: int | *8080
}
Composition
#Base: {
version: string
}
#Service: #Base & {
name: string
ports: [...int]
}
Relation avec Go
Points d'intégration
-
API Go directe
-
Prise en charge des balises de structure
-
Génération de code
-
Validation à l'exécution
Avantages
-
Sécurité des types
-
Validation de schéma
-
Gestion de la configuration
-
Modélisation des données
Cas d'utilisation
-
Définitions d'API
-
Fichiers de configuration
-
Validation des données
-
Génération de schémas
Messages d'erreur
invalid value "foo" (does not match ">=0 & <=120")
conflicting values false and true
field "required" not allowed
Liens de documentation du langage CUE
Ressources officielles
-
Site web principal : https://cuelang.org/
-
Documentation officielle : https://cuelang.org/docs/
-
Référence API : https://pkg.go.dev/cuelang.org/go/cue
Ressources GitHub
-
Dépôt principal : https://github.com/cue-lang/cue
-
Terrain de jeu CUE : https://cuelang.org/play/
Ressources d'apprentissage
-
Guide de référence : https://cuelang.org/docs/references/
-
Spécification : https://cuelang.org/docs/references/spec/
Démo 2 : API de base avec Go et Cue
Les Entités
#User: {
id: string
name: string & =~"^ [A-Za-z ]+$"
}
Entités en tant que schéma JSON / Yaml :
rm -rf contracts/basic_schema.yaml # supprimer si déjà existant
cue def contracts/user.cue -o contracts/basic_schema.yaml --out openapi+yaml # générer le schéma
cat contracts/basic_schema.yaml # afficher le contenu du fichier résultant
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 ]+$
## Le contrat OpenAPI :
En améliorant la sortie précédente, nous incluons manuellement la définition des `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 ]+$
## Le serveur :
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 string`json:"id"`
Name string`json:"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))
}
Testons cette API de base :
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
: Résultat du scénario de chemin heureux :
:
: {"id":"1","name":"John Doe"}
:
:
: Résultat du scénario d'échec de validation :
:
: name: invalid value "1234" (out of bound =~"^ [A-Za-z ]+$")
Démo 3 : Interface utilisateur Swagger
Conversion de Yaml en JSON
mkdir -p demos/demo3
go run cli.go y2j contracts/demo2.yaml demos/demo3/openapi.json
: Conversion réussie de contracts/demo2.yaml en demos/demo3/openapi.json
Serveur mis à jour
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 string`json:"id"`
Name string`json:"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() {
// Points de terminaison de l'API
http.HandleFunc("POST /users", userHandler)
// Servir la spécification 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 l'interface utilisateur 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("Serveur démarrant sur http://localhost:8080")
log.Printf("Documentation de l'API disponible sur http://localhost:8080/docs")
log.Fatal(http.ListenAndServe(":8080", nil))
}
const swaggerTemplate =`
Documentation de l'API
`
Démo 4 : Récupération d'informations sur les images
Points forts
Gestion du type d'image
Le hook React récupère le type d'image directement à partir de l'objet File de l'image téléchargée et définit un champ caché
Gestion de la taille de l'image
La taille de l'image est appliquée dans l'API via les longueurs de chaîne min et max.
Un calcul approximatif de la taille base64 d'une chaîne de 10 Mo :
-
Tout d'abord, convertissez 10 Mo en octets :
-
10MB = 10 * 1024* 1024 = 10,485,760 bytes
-
Appliquez la formule : 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
Ainsi, une chaîne de 10 Mo sera d'environ 13,98 Mo une fois encodée en base64 (13 981 016 octets ≈ 13,98 Mo)
Les Entités
import "strings"
// Image upload contract
#ImageUpload: {
// Unique identifier
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"
// Image encodée en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Statut de téléchargement de l'image
#ImageUploadStatus: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Statut de téléchargement de l'image
status: string & strings.MinRunes(3) & strings.MaxRunes(300) & =~"^ [A-Za-z0-9 -_.]+$"
}
Entités en tant que schéma JSON / Yaml :
rm -rf contracts/image.yaml # delete if already exists
cue def contracts/image.cue -o contracts/image.yaml --out openapi+yaml # generate schema
cat contracts/image.yaml # show the contents of the resulting file
openapi: 3.0.0
info:
title: Généré par cue.
version: aucune version
paths: {}
components:
schemas:
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Image encodée en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Statut de téléchargement d'image
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -.]+$
status:
description: Statut de téléchargement de l'image
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -.]+$
## Le contrat OpenAPI :
Nous incluons manuellement la définition de `paths` dans la sortie précédente.
openapi: 3.0.0
info:
title: Contrat de téléchargement d'image
version: 1.0.0
paths:
/extract-image-info:
post:
summary: Extraire les informations de l'image
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Image traitée avec succès
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
'400':
description: Échec du traitement de l'image
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUploadStatus'
components:
schemas:
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
blob:
description: Image encodée en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
ImageUploadStatus:
description: Statut de téléchargement de l'image
type: object
required:
- id
- prompt
- status
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
minLength: 3
maxLength: 100
pattern: ^ [A-Za-z0-9 -_.]+$
status:
description: Statut de téléchargement de l'image
type: string
minLength: 3
maxLength: 300
pattern: ^ [A-Za-z0-9 -_.]+$
Conversion de Yaml en JSON
mkdir -p demos/demo4
go run cli.go y2j contracts/demo4.yaml demos/demo4/openapi.json
: Converti avec succès contracts/demo4.yaml en demos/demo4/openapi.json
Le serveur :
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" // remplacer par le point de terminaison réel
apiKey = os.Getenv("OPENAI_API_KEY")
)
//go:embed *
var content embed.FS
type ImageUpload struct {
ID string`json:"id"`
Prompt string`json:"prompt"`
Blob string`json:"blob"`
}
type ImageUploadStatus struct {
ID string`json:"id"`
Prompt string`json:"prompt"`
Status string`json:"status"`
}
const schema =`
import "strings"
// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & =~"^.{3,100}" & =~"^ [A-Za-z0-9 -_.]+"
// Image encodée en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Statut de téléchargement d'image
#ImageUploadStatus: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & strings.MinRunes(3) & strings.MaxRunes(100) & =~"^ [A-Za-z0-9 -_.]+$"
// Statut de téléchargement d'image
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 =`
Documentation 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
}
# Démo 5 : Triple Streaming
## Entités
import "strings"
// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"
// Diffusion activée
stream: bool
// Image encodée en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrat d'informations sur l'image
#ImageInfo: {
// Informations sur l'image
info: string
}
## Entités en tant que schéma JSON / Yaml :
rm -rf contracts/image2.yaml # delete if already exists
cue def contracts/image2.cue -o contracts/image2.yaml --out openapi+yaml # generate schema
cat contracts/image2.yaml # show the contents of the resulting file
openapi: 3.0.0
info:
title: Generated by cue.
version: no version
paths: {}
components:
schemas:
ImageInfo:
description: Contrat d'informations d'image
type: object
required:
- info
properties:
info:
description: Informations d'image
type: string
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Flux activé
type: boolean
blob:
description: Image encodée en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
Le contrat OpenAPI :
Nous incluons manuellement la définition depathsà la sortie précédente.
openapi: 3.0.0
info:
title: Généré par cue.
version: no version
paths:
/extract-image-info:
post:
summary: Extraire les informations de l'image
requestBody:
required: true
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageUpload'
responses:
'200':
description: Image traitée avec succès
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
'400':
description: Le traitement de l'image a échoué
content:
application/json:
schema:
$['ref']: '#/components/schemas/ImageInfo'
components:
schemas:
ImageInfo:
description: Contrat d'informations sur l'image
type: object
required:
- info
properties:
info:
description: Informations sur l'image
type: string
ImageUpload:
description: Contrat de téléchargement d'image
type: object
required:
- id
- prompt
- stream
- blob
properties:
id:
description: Identifiant unique
type: string
pattern: ^ [0-9a-zA-Z -]{36}$
prompt:
description: Invite d'image
type: string
allOf:
- pattern: ^.{3,100}$
- pattern: ^ [A-Za-z0-9 -_.]+$
stream:
description: Flux activé
type: boolean
blob:
description: Image encodée en Base64
type: string
minLength: 3
maxLength: 13900000
pattern: ^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$
## Conversion de Yaml en JSON
mkdir -p demos/demo5
go run cli.go y2j contracts/demo5.yaml demos/demo5/openapi.json
: Conversion réussie de contracts/demo5.yaml en demos/demo5/openapi.json
## Le serveur :
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" // remplacer par le point de terminaison réel
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"
// Contrat de téléchargement d'image
#ImageUpload: {
// Identifiant unique
id: string & =~"^ [0-9a-zA-Z -]{36}$"
// Invite d'image
prompt: string & ="^.{3,100}$" & ="^ [A-Za-z0-9 -_.]+$"
// Flux activé
stream: bool
// Image encodée en Base64
blob: string & strings.MinRunes(3) & strings.MaxRunes(13_900_000) & =~"^data:image/(jpeg|png|gif|webp);base64, [A-Za-z0-9+/]+=*$"
}
// Contrat d'informations sur l'image
#ImageInfo: {
// Informations sur l'image
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 {
// Définir les en-têtes CORS pour autoriser toutes les origines. Vous voudrez peut-être restreindre cela à des origines spécifiques dans un environnement de production.
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")
// Définir le type de contenu sur 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 =`
Documentation de l'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()
// Vérifier le code de statut non-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)
}
// Utiliser bufio pour lire la réponse ligne par ligne
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// OpenAI diffuse chaque morceau préfixé par "data:"
if !strings.HasPrefix(line, "data:") {
continue
}
// "data: [DONE]" indique la fin du flux
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
}
// Analyse chaque morceau à l'aide de gjson
result := gjson.Parse(jsonData)
// Le contenu pertinent se trouve dans "choices" -> tableau -> "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
}
// Affiche le morceau de contenu (le cas échéant)
if content != "" {
fmt.Print(content)
fmt.Fprintf(w, "data: %s\n\n", content)
w.(http.Flusher).Flush()
}
// Met en pause un instant pour simuler le temps de traitement
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
}
# Références
- [Thérapie cognitivo-comportementale pour guérir la phobie des serveurs](https://x.com/dhh/status/1841876620141068560)
