From 7053af3c0d91ab4a9486fcc0010e29b73b10429d Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 15 Aug 2025 16:57:33 +0200 Subject: [PATCH] feat(core): implement file-based API versioning system (DAW/furt#83) - Add VERSION file in repository root - Add read_version() function with error handling - Update /health endpoint to show file-based version - Add version display during server startup - Fallback to ?.?.? when VERSION file unreadable Enables deployment tracking across dev/test/prod environments --- VERSION | 1 + src/main.lua | 79 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6da28dd --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.1 \ No newline at end of file diff --git a/src/main.lua b/src/main.lua index eaf98bd..3345cf4 100644 --- a/src/main.lua +++ b/src/main.lua @@ -13,6 +13,25 @@ local AuthRoute = require("src.routes.auth") -- Load configuration local config = require("config.server") +-- Read version from VERSION file +local function read_version() + local file, err = io.open("VERSION", "r") + if not file then + print("WARNING: Could not read VERSION file: " .. (err or "unknown error")) + return "?.?.?" + end + + local version = file:read("*line") + file:close() + + if not version or version:match("^%s*$") then + print("WARNING: VERSION file is empty or contains only whitespace") + return "?.?.?" + end + + return version:match("^%s*(.-)%s*$") -- trim whitespace +end + -- HTTP-Server Module local FurtServer = {} @@ -47,23 +66,23 @@ function FurtServer:parse_request(client) if not request_line then return nil end - + -- Parse request line: "POST /v1/mail/send HTTP/1.1" local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)") if not method then return nil end - + -- Parse headers local headers = {} local content_length = 0 - + while true do local line = client:receive() if not line or line == "" then break end - + local key, value = line:match("([^:]+): (.+)") if key and value then headers[key:lower()] = value @@ -72,13 +91,13 @@ function FurtServer:parse_request(client) end end end - + -- Parse body local body = "" if content_length > 0 then body = client:receive(content_length) end - + return { method = method, path = path, @@ -97,11 +116,11 @@ function FurtServer:add_cors_headers(request) "https://dragons-at-work.de", "https://www.dragons-at-work.de" } - + -- Check if request has Origin header local origin = request and request.headers and request.headers.origin local cors_origin = "*" -- Default fallback - + -- If origin is provided and in allowed list, use it if origin then for _, allowed in ipairs(allowed_origins) do @@ -111,7 +130,7 @@ function FurtServer:add_cors_headers(request) end end end - + return { ["Access-Control-Allow-Origin"] = cors_origin, ["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS", @@ -125,38 +144,38 @@ end function FurtServer:create_response(status, data, content_type, additional_headers, request) content_type = content_type or "application/json" local body = "" - + if type(data) == "table" then body = cjson.encode(data) else body = tostring(data or "") end - + -- Start with CORS headers local headers = self:add_cors_headers(request) - + -- Add standard headers headers["Content-Type"] = content_type headers["Content-Length"] = tostring(#body) headers["Connection"] = "close" headers["Server"] = "Furt-Lua/1.1" - + -- Add additional headers if provided if additional_headers then for key, value in pairs(additional_headers) do headers[key] = value end end - + -- Build response local response = string.format("HTTP/1.1 %d %s\r\n", status, self:get_status_text(status)) - + for key, value in pairs(headers) do response = response .. key .. ": " .. value .. "\r\n" end - + response = response .. "\r\n" .. body - + return response end @@ -184,23 +203,23 @@ function FurtServer:handle_client(client) client:send(response) return end - - print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), + + print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), request.method, request.path)) - + -- Handle OPTIONS preflight requests (CORS) if request.method == "OPTIONS" then local response = self:create_response(204, "", "text/plain", nil, request) client:send(response) return end - + -- Route handling local handler = nil if self.routes[request.method] and self.routes[request.method][request.path] then handler = self.routes[request.method][request.path] end - + if handler then local success, result = pcall(handler, request, self) if success then @@ -223,13 +242,16 @@ function FurtServer:start() if not self.server then error("Failed to bind to " .. self.host .. ":" .. self.port) end - + + local version = read_version() + print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) + print("Version: " .. version) print("API-Key authentication: ENABLED") print("Rate limiting: ENABLED (60 req/hour per API key, 100 req/hour per IP)") print("CORS enabled for configured origins") print("Press Ctrl+C to stop") - + while true do local client = self.server:accept() if client then @@ -245,10 +267,11 @@ local server = FurtServer:new() -- Public routes (no authentication required) server:add_route("GET", "/health", function(request, server) + local version = read_version() local response_data = { status = "healthy", service = "furt-lua", - version = "1.1.0", + version = version, timestamp = os.time(), features = { smtp_configured = config.mail and config.mail.username ~= nil, @@ -268,15 +291,15 @@ if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then headers_count = 0, warning = "This is a development endpoint" } - + -- Count headers for _ in pairs(request.headers) do response_data.headers_count = response_data.headers_count + 1 end - + return server:create_response(200, response_data, nil, nil, request) end) - print("⚠️ Test endpoint enabled (development mode)") + print("[WARN] Test endpoint enabled (development mode)") end -- Protected routes (require authentication)