initial commit

This commit is contained in:
2026-06-10 13:01:57 +02:00
commit 450ae25c84
21 changed files with 2613 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
package api
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/restxlsx/restxlsx/internal/excel"
"github.com/restxlsx/restxlsx/internal/logger"
)
type DDLHandler struct {
engine *excel.Engine
log *logger.Logger
}
func NewDDLHandler(engine *excel.Engine, log *logger.Logger) *DDLHandler {
return &DDLHandler{engine: engine, log: log}
}
type createSheetReq struct {
Name string `json:"name"`
}
func (h *DDLHandler) CreateSheet(w http.ResponseWriter, r *http.Request) {
var req createSheetReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
if req.Name == "" {
writeJSON(w, 400, map[string]string{"error": "name is required"})
return
}
h.engine.Lock()
defer h.engine.Unlock()
if err := h.engine.CreateSheet(req.Name); err != nil {
writeJSON(w, 409, map[string]string{"error": err.Error()})
return
}
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 201, map[string]any{"sheet": req.Name, "status": "created"})
}
func (h *DDLHandler) DropSheet(w http.ResponseWriter, r *http.Request) {
sheet := chi.URLParam(r, "sheet")
if sheet == "" {
writeJSON(w, 400, map[string]string{"error": "sheet name required"})
return
}
h.engine.Lock()
defer h.engine.Unlock()
if err := h.engine.DropSheet(sheet); err != nil {
writeJSON(w, 404, map[string]string{"error": err.Error()})
return
}
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 200, map[string]any{"sheet": sheet, "status": "deleted"})
}
type createTableReq struct {
Sheet string `json:"sheet"`
Name string `json:"name"`
Columns []string `json:"columns"`
}
func (h *DDLHandler) CreateTable(w http.ResponseWriter, r *http.Request) {
var req createTableReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
if req.Name == "" || req.Sheet == "" || len(req.Columns) == 0 {
writeJSON(w, 400, map[string]string{"error": "sheet, name, and columns are required"})
return
}
h.engine.Lock()
defer h.engine.Unlock()
if h.engine.GetTable(req.Sheet) == nil {
if err := h.engine.CreateSheet(req.Sheet); err != nil {
writeJSON(w, 409, map[string]string{"error": err.Error()})
return
}
}
if err := h.engine.CreateTable(req.Sheet, req.Columns, []map[string]any{}); err != nil {
writeJSON(w, 409, map[string]string{"error": err.Error()})
return
}
t := h.engine.GetTable(req.Sheet)
t.Name = req.Name
h.engine.Tables[req.Name] = t
delete(h.engine.Tables, req.Sheet)
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 201, map[string]any{"table": req.Name, "sheet": req.Sheet, "columns": req.Columns, "status": "created"})
}
func (h *DDLHandler) DropTable(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
if table == "" {
writeJSON(w, 400, map[string]string{"error": "table name required"})
return
}
h.engine.Lock()
defer h.engine.Unlock()
if err := h.engine.DropTable(table); err != nil {
writeJSON(w, 404, map[string]string{"error": err.Error()})
return
}
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 200, map[string]any{"table": table, "status": "deleted"})
}
+10
View File
@@ -0,0 +1,10 @@
package api
import "net/http"
func JSONContentType(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
+311
View File
@@ -0,0 +1,311 @@
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))
}
+174
View File
@@ -0,0 +1,174 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/restxlsx/restxlsx/internal/excel"
"github.com/restxlsx/restxlsx/internal/logger"
)
type RestAPI struct {
engine *excel.Engine
log *logger.Logger
}
func NewRestAPI(engine *excel.Engine, log *logger.Logger) *RestAPI {
return &RestAPI{engine: engine, log: log}
}
func (h *RestAPI) ListTables(w http.ResponseWriter, r *http.Request) {
h.engine.Lock()
tables := h.engine.ListTables()
h.engine.Unlock()
writeJSON(w, 200, map[string][]string{"tables": tables})
}
func (h *RestAPI) ListRecords(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
h.engine.Lock()
t := h.engine.GetTable(table)
if t == nil {
h.engine.Unlock()
writeJSON(w, 404, map[string]string{"error": "table not found"})
return
}
rows := t.Rows
h.engine.Unlock()
writeJSON(w, 200, rows)
}
func (h *RestAPI) GetRecord(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
id := chi.URLParam(r, "id")
h.engine.Lock()
t := h.engine.GetTable(table)
if t == nil {
h.engine.Unlock()
writeJSON(w, 404, map[string]string{"error": "table not found"})
return
}
idCol := h.engine.IDColumn(table)
for _, row := range t.Rows {
if fmt.Sprintf("%v", row[idCol]) == id {
h.engine.Unlock()
writeJSON(w, 200, row)
return
}
}
h.engine.Unlock()
writeJSON(w, 404, map[string]string{"error": "record not found"})
}
func (h *RestAPI) CreateRecord(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
var rec map[string]any
if err := json.NewDecoder(r.Body).Decode(&rec); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
h.engine.Lock()
defer h.engine.Unlock()
t := h.engine.GetTable(table)
if t == nil {
writeJSON(w, 404, map[string]string{"error": "table not found"})
return
}
idCol := h.engine.IDColumn(table)
if idCol != "" {
if _, ok := rec[idCol]; !ok {
rec[idCol] = len(t.Rows) + 1
}
}
t.Rows = append(t.Rows, rec)
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 201, rec)
}
func (h *RestAPI) UpdateRecord(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
id := chi.URLParam(r, "id")
var rec map[string]any
if err := json.NewDecoder(r.Body).Decode(&rec); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
h.engine.Lock()
defer h.engine.Unlock()
t := h.engine.GetTable(table)
if t == nil {
writeJSON(w, 404, map[string]string{"error": "table not found"})
return
}
idCol := h.engine.IDColumn(table)
for i, row := range t.Rows {
if fmt.Sprintf("%v", row[idCol]) == id {
for k, v := range rec {
t.Rows[i][k] = v
}
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 200, t.Rows[i])
return
}
}
writeJSON(w, 404, map[string]string{"error": "record not found"})
}
func (h *RestAPI) DeleteRecord(w http.ResponseWriter, r *http.Request) {
table := chi.URLParam(r, "table")
id := chi.URLParam(r, "id")
h.engine.Lock()
defer h.engine.Unlock()
t := h.engine.GetTable(table)
if t == nil {
writeJSON(w, 404, map[string]string{"error": "table not found"})
return
}
idCol := h.engine.IDColumn(table)
for i, row := range t.Rows {
if fmt.Sprintf("%v", row[idCol]) == id {
t.Rows = append(t.Rows[:i], t.Rows[i+1:]...)
if err := h.engine.Flush(); err != nil {
writeJSON(w, 503, map[string]string{"error": "flush failed: " + err.Error()})
return
}
writeJSON(w, 204, nil)
return
}
}
writeJSON(w, 404, map[string]string{"error": "record not found"})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
json.NewEncoder(w).Encode(data)
}
}
+548
View File
@@ -0,0 +1,548 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"unicode"
"github.com/restxlsx/restxlsx/internal/auth"
"github.com/restxlsx/restxlsx/internal/excel"
"github.com/restxlsx/restxlsx/internal/logger"
)
type token struct {
kind tokenKind
value string
}
type tokenKind int
const (
tokEOF tokenKind = iota
tokIdent
tokString
tokNumber
tokStar
tokComma
tokLParen
tokRParen
tokEq
tokSemicolon
tokKeyword
)
type sqlStmt struct {
kind string // SELECT, INSERT, UPDATE, DELETE
table string
columns []string
values []string
sets map[string]string
whereCol string
whereVal string
}
type SQLHandler struct {
engine *excel.Engine
log *logger.Logger
}
func NewSQLHandler(engine *excel.Engine, log *logger.Logger) *SQLHandler {
return &SQLHandler{engine: engine, log: log}
}
func (h *SQLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
stmt, err := parseSQL(body.Query)
if err != nil {
writeJSON(w, 400, map[string]string{"error": "parse error: " + err.Error()})
return
}
role := auth.RoleFromContext(r.Context())
switch stmt.kind {
case "SELECT":
case "INSERT", "UPDATE", "DELETE":
if !role.CanWrite() {
writeJSON(w, 403, map[string]string{"error": "forbidden: write access required"})
return
}
default:
writeJSON(w, 400, map[string]string{"error": "unsupported statement: " + stmt.kind})
return
}
h.engine.Lock()
result, err := h.execute(stmt)
h.engine.Unlock()
if err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, result)
}
func (h *SQLHandler) execute(stmt sqlStmt) (any, error) {
table := h.engine.GetTable(stmt.table)
if table == nil {
return nil, fmt.Errorf("table %q not found", stmt.table)
}
switch stmt.kind {
case "SELECT":
return h.executeSelect(table, stmt)
case "INSERT":
return h.executeInsert(table, stmt)
case "UPDATE":
return h.executeUpdate(table, stmt)
case "DELETE":
return h.executeDelete(table, stmt)
}
return nil, fmt.Errorf("unsupported: %s", stmt.kind)
}
func (h *SQLHandler) executeSelect(table *excel.Table, stmt sqlStmt) (any, error) {
var results []map[string]any
for _, row := range table.Rows {
if stmt.whereCol != "" {
if fmt.Sprintf("%v", row[stmt.whereCol]) != stmt.whereVal {
continue
}
}
if len(stmt.columns) == 1 && stmt.columns[0] == "*" {
results = append(results, row)
} else {
projected := make(map[string]any)
for _, col := range stmt.columns {
if v, ok := row[col]; ok {
projected[col] = v
}
}
results = append(results, projected)
}
}
if stmt.whereCol != "" && len(results) == 1 {
return results[0], nil
}
if results == nil {
return []map[string]any{}, nil
}
return results, nil
}
func (h *SQLHandler) executeInsert(table *excel.Table, stmt sqlStmt) (any, error) {
if len(stmt.columns) != len(stmt.values) {
return nil, fmt.Errorf("column count (%d) != value count (%d)", len(stmt.columns), len(stmt.values))
}
rec := make(map[string]any)
for i, col := range stmt.columns {
rec[col] = parseValue(stmt.values[i])
}
idCol := h.engine.IDColumn(table.Name)
if idCol != "" {
if _, ok := rec[idCol]; !ok {
rec[idCol] = len(table.Rows) + 1
}
}
table.Rows = append(table.Rows, rec)
if err := h.engine.Flush(); err != nil {
return nil, err
}
return rec, nil
}
func (h *SQLHandler) executeUpdate(table *excel.Table, stmt sqlStmt) (any, error) {
var updated []map[string]any
for i, row := range table.Rows {
if stmt.whereCol != "" {
if fmt.Sprintf("%v", row[stmt.whereCol]) != stmt.whereVal {
continue
}
}
for k, v := range stmt.sets {
table.Rows[i][k] = parseValue(v)
}
updated = append(updated, table.Rows[i])
}
if err := h.engine.Flush(); err != nil {
return nil, err
}
if updated == nil {
return []map[string]any{}, nil
}
return updated, nil
}
func (h *SQLHandler) executeDelete(table *excel.Table, stmt sqlStmt) (any, error) {
var deleted []map[string]any
kept := make([]map[string]any, 0, len(table.Rows))
for _, row := range table.Rows {
if stmt.whereCol != "" {
if fmt.Sprintf("%v", row[stmt.whereCol]) == stmt.whereVal {
deleted = append(deleted, row)
continue
}
} else {
deleted = append(deleted, row)
continue
}
kept = append(kept, row)
}
table.Rows = kept
if err := h.engine.Flush(); err != nil {
return nil, err
}
return map[string]any{"deleted": len(deleted)}, nil
}
// ─── SQL Parser ────────────────────────────────────────────────
type lexer struct {
input string
pos int
}
func (l *lexer) peek() byte {
if l.pos >= len(l.input) {
return 0
}
return l.input[l.pos]
}
func (l *lexer) advance() byte {
b := l.input[l.pos]
l.pos++
return b
}
func (l *lexer) skipWS() {
for l.pos < len(l.input) && (l.input[l.pos] == ' ' || l.input[l.pos] == '\t' || l.input[l.pos] == '\n') {
l.pos++
}
}
var keywords = map[string]bool{
"select": true, "from": true, "where": true,
"insert": true, "into": true, "values": true,
"update": true, "set": true, "delete": true,
}
func lex(input string) ([]token, error) {
l := &lexer{input: input}
var toks []token
for l.pos < len(l.input) {
l.skipWS()
if l.pos >= len(l.input) {
break
}
c := l.peek()
switch {
case c == ',':
toks = append(toks, token{tokComma, ","})
l.advance()
case c == '(':
toks = append(toks, token{tokLParen, "("})
l.advance()
case c == ')':
toks = append(toks, token{tokRParen, ")"})
l.advance()
case c == ';':
toks = append(toks, token{tokSemicolon, ";"})
l.advance()
case c == '=':
toks = append(toks, token{tokEq, "="})
l.advance()
case c == '*':
toks = append(toks, token{tokStar, "*"})
l.advance()
case c == '\'' || c == '"':
quote := l.advance()
var buf strings.Builder
for l.pos < len(l.input) && l.peek() != quote {
buf.WriteByte(l.advance())
}
if l.pos >= len(l.input) {
return nil, fmt.Errorf("unterminated string literal")
}
l.advance()
toks = append(toks, token{tokString, buf.String()})
case c >= '0' && c <= '9' || c == '-':
var buf strings.Builder
for l.pos < len(l.input) && (l.peek() >= '0' && l.peek() <= '9' || l.peek() == '.') {
buf.WriteByte(l.advance())
}
toks = append(toks, token{tokNumber, buf.String()})
case unicode.IsLetter(rune(c)) || c == '_':
var buf strings.Builder
for l.pos < len(l.input) && (unicode.IsLetter(rune(l.peek())) || unicode.IsDigit(rune(l.peek())) || l.peek() == '_') {
buf.WriteByte(l.advance())
}
word := buf.String()
if keywords[strings.ToLower(word)] {
toks = append(toks, token{tokKeyword, strings.ToUpper(word)})
} else {
toks = append(toks, token{tokIdent, word})
}
default:
return nil, fmt.Errorf("unexpected character: %c", c)
}
}
if len(toks) == 0 {
return nil, fmt.Errorf("empty query")
}
toks = append(toks, token{tokEOF, ""})
return toks, nil
}
func parseSQL(input string) (sqlStmt, error) {
toks, err := lex(input)
if err != nil {
return sqlStmt{}, err
}
pos := 0
next := func() token {
if pos >= len(toks) {
return token{tokEOF, ""}
}
t := toks[pos]
pos++
return t
}
peek := func() token {
if pos >= len(toks) {
return token{tokEOF, ""}
}
return toks[pos]
}
expect := func(kind tokenKind, msg string) (token, error) {
t := next()
if t.kind != kind {
return t, fmt.Errorf("%s: expected %d got %q", msg, kind, t.value)
}
return t, nil
}
first := next()
if first.kind != tokKeyword {
return sqlStmt{}, fmt.Errorf("expected keyword, got %q", first.value)
}
switch first.value {
case "SELECT":
return parseSelect(&pos, toks, next, peek, expect)
case "INSERT":
return parseInsert(&pos, toks, next, peek, expect)
case "UPDATE":
return parseUpdate(&pos, toks, next, peek, expect)
case "DELETE":
return parseDelete(&pos, toks, next, peek, expect)
default:
return sqlStmt{}, fmt.Errorf("unsupported statement: %s", first.value)
}
}
func parseSelect(pos *int, toks []token, next, peek func() token, expect func(tokenKind, string) (token, error)) (sqlStmt, error) {
stmt := sqlStmt{kind: "SELECT"}
if peek().kind == tokStar {
next()
stmt.columns = []string{"*"}
} else {
for {
t, err := expect(tokIdent, "select column")
if err != nil {
return stmt, err
}
stmt.columns = append(stmt.columns, t.value)
if peek().kind != tokComma {
break
}
next()
}
}
if _, err := expect(tokKeyword, "FROM"); err != nil {
return stmt, err
}
t, err := expect(tokIdent, "table name")
if err != nil {
return stmt, err
}
stmt.table = t.value
if peek().kind == tokKeyword && strings.ToUpper(peek().value) == "WHERE" {
next()
t, err := expect(tokIdent, "where column")
if err != nil {
return stmt, err
}
stmt.whereCol = t.value
if _, err := expect(tokEq, "="); err != nil {
return stmt, err
}
v := next()
if v.kind != tokString && v.kind != tokNumber {
return stmt, fmt.Errorf("expected value in WHERE, got %q", v.value)
}
stmt.whereVal = v.value
}
return stmt, nil
}
func parseInsert(pos *int, toks []token, next, peek func() token, expect func(tokenKind, string) (token, error)) (sqlStmt, error) {
stmt := sqlStmt{kind: "INSERT"}
if _, err := expect(tokKeyword, "INTO"); err != nil {
return stmt, err
}
t, err := expect(tokIdent, "table name")
if err != nil {
return stmt, err
}
stmt.table = t.value
if _, err := expect(tokLParen, "("); err != nil {
return stmt, err
}
for {
t, err := expect(tokIdent, "column name")
if err != nil {
return stmt, err
}
stmt.columns = append(stmt.columns, t.value)
if peek().kind != tokComma {
break
}
next()
}
if _, err := expect(tokRParen, ")"); err != nil {
return stmt, err
}
if _, err := expect(tokKeyword, "VALUES"); err != nil {
return stmt, err
}
if _, err := expect(tokLParen, "("); err != nil {
return stmt, err
}
for {
v := next()
if v.kind != tokString && v.kind != tokNumber {
return stmt, fmt.Errorf("expected value, got %q", v.value)
}
stmt.values = append(stmt.values, v.value)
if peek().kind != tokComma {
break
}
next()
}
if _, err := expect(tokRParen, ")"); err != nil {
return stmt, err
}
return stmt, nil
}
func parseUpdate(pos *int, toks []token, next, peek func() token, expect func(tokenKind, string) (token, error)) (sqlStmt, error) {
stmt := sqlStmt{kind: "UPDATE", sets: make(map[string]string)}
t, err := expect(tokIdent, "table name")
if err != nil {
return stmt, err
}
stmt.table = t.value
if _, err := expect(tokKeyword, "SET"); err != nil {
return stmt, err
}
for {
t, err := expect(tokIdent, "column name")
if err != nil {
return stmt, err
}
col := t.value
if _, err := expect(tokEq, "="); err != nil {
return stmt, err
}
v := next()
if v.kind != tokString && v.kind != tokNumber {
return stmt, fmt.Errorf("expected value in SET, got %q", v.value)
}
stmt.sets[col] = v.value
if peek().kind != tokComma {
break
}
next()
}
if peek().kind == tokKeyword && strings.ToUpper(peek().value) == "WHERE" {
next()
t, err := expect(tokIdent, "where column")
if err != nil {
return stmt, err
}
stmt.whereCol = t.value
if _, err := expect(tokEq, "="); err != nil {
return stmt, err
}
v := next()
if v.kind != tokString && v.kind != tokNumber {
return stmt, fmt.Errorf("expected value in WHERE, got %q", v.value)
}
stmt.whereVal = v.value
}
return stmt, nil
}
func parseDelete(pos *int, toks []token, next, peek func() token, expect func(tokenKind, string) (token, error)) (sqlStmt, error) {
stmt := sqlStmt{kind: "DELETE"}
if _, err := expect(tokKeyword, "FROM"); err != nil {
return stmt, err
}
t, err := expect(tokIdent, "table name")
if err != nil {
return stmt, err
}
stmt.table = t.value
if peek().kind == tokKeyword && strings.ToUpper(peek().value) == "WHERE" {
next()
t, err := expect(tokIdent, "where column")
if err != nil {
return stmt, err
}
stmt.whereCol = t.value
if _, err := expect(tokEq, "="); err != nil {
return stmt, err
}
v := next()
if v.kind != tokString && v.kind != tokNumber {
return stmt, fmt.Errorf("expected value in WHERE, got %q", v.value)
}
stmt.whereVal = v.value
}
return stmt, nil
}
func parseValue(s string) any {
if i, err := strconv.Atoi(s); err == nil {
return i
}
if f, err := strconv.ParseFloat(s, 64); err == nil {
return f
}
return s
}
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>restxlsx - Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
<script>
SwaggerUIBundle({
spec: JSON.parse('{{SPEC}}'),
dom_id: '#swagger-ui',
});
</script>
</body>
</html>