312 lines
8.1 KiB
Go
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))
|
|
}
|