Files
restxlsx/internal/api/openapi.go
T
2026-06-10 13:01:57 +02:00

312 lines
8.1 KiB
Go

package api
import (
_ "embed"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/restxlsx/restxlsx/internal/excel"
)
//go:embed swagger-ui.html
var swaggerHTML string
type OpenAPI struct {
engine *excel.Engine
debug bool
}
func NewOpenAPI(engine *excel.Engine, debug bool) *OpenAPI {
return &OpenAPI{engine: engine, debug: debug}
}
func (o *OpenAPI) Spec() map[string]any {
spec := map[string]any{
"openapi": "3.0.3",
"info": map[string]any{
"title": "restxlsx",
"description": "RESTful API over Excel tables. Supports REST CRUD, GraphQL, SQL, and DDL.",
"version": "0.2.0",
},
"paths": o.buildPaths(),
}
if o.debug {
spec["servers"] = []map[string]any{
{"url": "http://localhost:3000", "description": "Local dev"},
}
}
return spec
}
func (o *OpenAPI) buildPaths() map[string]any {
paths := make(map[string]any)
paths["/api/tables"] = map[string]any{
"get": map[string]any{
"summary": "List all tables",
"operationId": "listTables",
"responses": map[string]any{
"200": map[string]any{
"description": "List of table names",
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "object",
"properties": map[string]any{
"tables": map[string]any{
"type": "array",
"items": map[string]any{
"type": "string",
},
},
},
},
},
},
},
},
},
}
for _, name := range o.engine.ListTables() {
t := o.engine.GetTable(name)
tablePath := fmt.Sprintf("/api/%s", name)
itemPath := fmt.Sprintf("/api/%s/{id}", name)
schema := tableSchema(t)
paths[tablePath] = map[string]any{
"get": map[string]any{
"summary": fmt.Sprintf("List all %s", name),
"operationId": fmt.Sprintf("list%s", name),
"responses": map[string]any{
"200": map[string]any{
"description": fmt.Sprintf("Array of %s", name),
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "array",
"items": schema,
},
},
},
},
},
},
"post": map[string]any{
"summary": fmt.Sprintf("Create a new %s", name),
"operationId": fmt.Sprintf("create%s", name),
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": schema,
},
},
},
"responses": map[string]any{
"201": map[string]any{
"description": "Created",
"content": map[string]any{
"application/json": map[string]any{
"schema": schema,
},
},
},
},
},
}
paths[itemPath] = map[string]any{
"get": map[string]any{
"summary": fmt.Sprintf("Get %s by ID", name),
"operationId": fmt.Sprintf("get%s", name),
"parameters": []map[string]any{
{"name": "id", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
},
"responses": map[string]any{
"200": map[string]any{
"description": fmt.Sprintf("A single %s", name),
"content": map[string]any{
"application/json": map[string]any{
"schema": schema,
},
},
},
"404": map[string]any{"description": "Not found"},
},
},
"put": map[string]any{
"summary": fmt.Sprintf("Update %s by ID", name),
"operationId": fmt.Sprintf("update%s", name),
"parameters": []map[string]any{
{"name": "id", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
},
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": schema,
},
},
},
"responses": map[string]any{
"200": map[string]any{"description": "Updated"},
"404": map[string]any{"description": "Not found"},
},
},
"delete": map[string]any{
"summary": fmt.Sprintf("Delete %s by ID", name),
"operationId": fmt.Sprintf("delete%s", name),
"parameters": []map[string]any{
{"name": "id", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
},
"responses": map[string]any{
"204": map[string]any{"description": "Deleted"},
"404": map[string]any{"description": "Not found"},
},
},
}
}
paths["/api/sql"] = map[string]any{
"post": map[string]any{
"summary": "Execute a SQL query",
"operationId": "execSQL",
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{"type": "string"},
},
},
},
},
},
"responses": map[string]any{
"200": map[string]any{"description": "Query result"},
"400": map[string]any{"description": "Bad request"},
"403": map[string]any{"description": "Forbidden"},
},
},
}
paths["/api/ddl/sheets"] = map[string]any{
"post": map[string]any{
"summary": "Create a new sheet",
"operationId": "createSheet",
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
},
},
},
},
},
"responses": map[string]any{
"201": map[string]any{"description": "Created"},
"403": map[string]any{"description": "Forbidden"},
},
},
}
paths["/api/ddl/sheets/{sheet}"] = map[string]any{
"delete": map[string]any{
"summary": "Delete a sheet",
"operationId": "deleteSheet",
"parameters": []map[string]any{
{"name": "sheet", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
},
"responses": map[string]any{
"200": map[string]any{"description": "Deleted"},
"403": map[string]any{"description": "Forbidden"},
"404": map[string]any{"description": "Not found"},
},
},
}
paths["/api/ddl/tables"] = map[string]any{
"post": map[string]any{
"summary": "Create a table on a sheet",
"operationId": "createTable",
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "object",
"properties": map[string]any{
"sheet": map[string]any{"type": "string"},
"name": map[string]any{"type": "string"},
"columns": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
},
},
},
},
},
"responses": map[string]any{
"201": map[string]any{"description": "Created"},
"403": map[string]any{"description": "Forbidden"},
},
},
}
paths["/api/ddl/tables/{table}"] = map[string]any{
"delete": map[string]any{
"summary": "Drop a table",
"operationId": "deleteTable",
"parameters": []map[string]any{
{"name": "table", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
},
"responses": map[string]any{
"200": map[string]any{"description": "Deleted"},
"403": map[string]any{"description": "Forbidden"},
"404": map[string]any{"description": "Not found"},
},
},
}
return paths
}
func tableSchema(t *excel.Table) map[string]any {
props := make(map[string]any)
required := make([]string, 0)
for _, col := range t.Columns {
props[col] = map[string]any{"type": "string"}
required = append(required, col)
}
return map[string]any{
"type": "object",
"properties": props,
"required": required,
}
}
func (o *OpenAPI) Handler(w http.ResponseWriter, r *http.Request) {
spec := o.Spec()
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(spec)
}
func (o *OpenAPI) SwaggerHandler(w http.ResponseWriter, r *http.Request) {
specBytes, _ := json.Marshal(o.Spec())
specStr := strings.ReplaceAll(string(specBytes), `"`, `\"`)
html := strings.Replace(swaggerHTML, "{{SPEC}}", specStr, 1)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}