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)) }