From 68a17919667976db23317f0efe8c5ad8489de4ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 23:12:48 +0000 Subject: [PATCH 1/2] Add type-safe HTTP server for ReScript using Deno.serve - Add HttpServer.res with FFI bindings to Deno.serve - Add HttpServer.resi interface with full API surface - Add routing helpers (get, post, put, patch, delete) - Add middleware support (cors, logging) - Add response helpers (respond, respondJson, respondHtml, etc.) - Add deno.json and rescript.json configurations - Update .gitignore with ReScript build artifacts --- .gitignore | 3 + deno.json | 13 ++ rescript.json | 8 ++ src/HttpServer.res | 285 ++++++++++++++++++++++++++++++++++++++++++++ src/HttpServer.resi | 80 +++++++++++++ 5 files changed, 389 insertions(+) create mode 100644 deno.json create mode 100644 rescript.json create mode 100644 src/HttpServer.res create mode 100644 src/HttpServer.resi diff --git a/.gitignore b/.gitignore index 0338461..cbdd3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,11 @@ erl_crash.dump /Manifest.toml # ReScript +lib/ /lib/bs/ /.bsb.lock +*.res.js +.merlin # Python (SaltStack only) __pycache__/ diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..cf95308 --- /dev/null +++ b/deno.json @@ -0,0 +1,13 @@ +{ + "name": "@hyperpolymath/rescript-http-server", + "version": "0.1.0", + "exports": "./src/HttpServer.res.js", + "tasks": { + "build": "rescript build", + "clean": "rescript clean", + "dev": "rescript build -w" + }, + "compilerOptions": { + "lib": ["deno.ns", "deno.unstable"] + } +} diff --git a/rescript.json b/rescript.json new file mode 100644 index 0000000..4f0d416 --- /dev/null +++ b/rescript.json @@ -0,0 +1,8 @@ +{ + "name": "@hyperpolymath/rescript-http-server", + "sources": [{"dir": "src", "subdirs": true}], + "package-specs": [{"module": "esmodule", "in-source": true}], + "suffix": ".res.js", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/src/HttpServer.res b/src/HttpServer.res new file mode 100644 index 0000000..bac71e1 --- /dev/null +++ b/src/HttpServer.res @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +@@uncurried + +/** + * Type-safe HTTP server for ReScript using Deno.serve. + */ + +/** HTTP method */ +type method = + | @as("GET") Get + | @as("POST") Post + | @as("PUT") Put + | @as("PATCH") Patch + | @as("DELETE") Delete + | @as("HEAD") Head + | @as("OPTIONS") Options + +/** Deno server instance */ +type server + +/** Request object */ +type request = Fetch.Request.t + +/** Response object */ +type response = Fetch.Response.t + +/** Response init */ +type responseInit = { + status?: int, + statusText?: string, + headers?: Dict.t, +} + +/** Server options */ +type serveOptions = { + port?: int, + hostname?: string, + onListen?: {"port": int, "hostname": string} => unit, + onError?: Error.t => response, +} + +/** Handler function type */ +type handler = request => promise + +/** Info about listening server */ +type listenInfo = { + port: int, + hostname: string, +} + +// FFI bindings +@val @scope("Deno") +external serve: (serveOptions, handler) => server = "serve" + +@val @scope("Deno") +external serveHandler: handler => server = "serve" + +@send +external shutdown: server => promise = "shutdown" + +@send +external finished: server => promise = "finished" + +@get +external addr: server => listenInfo = "addr" + +// Request helpers +@get external requestMethod: request => string = "method" +@get external requestUrl: request => string = "url" +@get external requestHeaders: request => Fetch.Headers.t = "headers" +@send external requestText: request => promise = "text" +@send external requestJson: request => promise = "json" +@send external requestArrayBuffer: request => promise = "arrayBuffer" +@send external requestFormData: request => promise = "formData" + +/** Get the HTTP method */ +let method = (req: request): method => { + switch requestMethod(req) { + | "GET" => Get + | "POST" => Post + | "PUT" => Put + | "PATCH" => Patch + | "DELETE" => Delete + | "HEAD" => Head + | "OPTIONS" => Options + | _ => Get + } +} + +/** Get URL path */ +let path = (req: request): string => { + let url = requestUrl(req) + try { + let urlObj = URL.make(url) + urlObj->URL.pathname + } catch { + | _ => url + } +} + +/** Get URL search params */ +let searchParams = (req: request): URLSearchParams.t => { + let url = requestUrl(req) + try { + let urlObj = URL.make(url) + urlObj->URL.searchParams + } catch { + | _ => URLSearchParams.make("") + } +} + +/** Get a header value */ +let header = (req: request, name: string): option => { + requestHeaders(req)->Fetch.Headers.get(name) +} + +/** Read body as text */ +let text = (req: request): promise => requestText(req) + +/** Read body as JSON */ +let json = (req: request): promise => requestJson(req) + +// Response helpers +@new external makeResponse: (string, responseInit) => response = "Response" +@new external makeResponseWithBody: (string) => response = "Response" + +/** Create a text response */ +let respond = (~status: int=200, ~headers: Dict.t=Dict.make(), body: string): response => { + headers->Dict.set("Content-Type", "text/plain; charset=utf-8") + makeResponse(body, {status, headers}) +} + +/** Create a JSON response */ +let respondJson = (~status: int=200, ~headers: Dict.t=Dict.make(), data: JSON.t): response => { + headers->Dict.set("Content-Type", "application/json; charset=utf-8") + makeResponse(JSON.stringify(data), {status, headers}) +} + +/** Create an HTML response */ +let respondHtml = (~status: int=200, ~headers: Dict.t=Dict.make(), html: string): response => { + headers->Dict.set("Content-Type", "text/html; charset=utf-8") + makeResponse(html, {status, headers}) +} + +/** Create a redirect response */ +let redirect = (~status: int=302, location: string): response => { + makeResponse("", {status, headers: Dict.fromArray([("Location", location)])}) +} + +/** Create a 404 response */ +let notFound = (~body: string="Not Found"): response => { + respond(~status=404, body) +} + +/** Create a 400 response */ +let badRequest = (~body: string="Bad Request"): response => { + respond(~status=400, body) +} + +/** Create a 500 response */ +let serverError = (~body: string="Internal Server Error"): response => { + respond(~status=500, body) +} + +/** Create a 401 response */ +let unauthorized = (~body: string="Unauthorized"): response => { + respond(~status=401, body) +} + +/** Create a 403 response */ +let forbidden = (~body: string="Forbidden"): response => { + respond(~status=403, body) +} + +/** Route definition */ +type route = { + method: method, + pattern: string, + handler: request => promise, +} + +/** Simple path matcher */ +let matchPath = (pattern: string, requestPath: string): bool => { + if pattern == requestPath { + true + } else if pattern->String.endsWith("*") { + let prefix = pattern->String.slice(~start=0, ~end=-1) + requestPath->String.startsWith(prefix) + } else { + false + } +} + +/** Create a simple router */ +let router = (routes: array): handler => { + async request => { + let reqMethod = method(request) + let reqPath = path(request) + + let matchedRoute = routes->Array.find(route => { + route.method == reqMethod && matchPath(route.pattern, reqPath) + }) + + switch matchedRoute { + | Some(route) => await route.handler(request) + | None => notFound() + } + } +} + +/** Route helper for GET */ +let get = (pattern: string, handler: request => promise): route => { + {method: Get, pattern, handler} +} + +/** Route helper for POST */ +let post = (pattern: string, handler: request => promise): route => { + {method: Post, pattern, handler} +} + +/** Route helper for PUT */ +let put = (pattern: string, handler: request => promise): route => { + {method: Put, pattern, handler} +} + +/** Route helper for PATCH */ +let patch = (pattern: string, handler: request => promise): route => { + {method: Patch, pattern, handler} +} + +/** Route helper for DELETE */ +let delete = (pattern: string, handler: request => promise): route => { + {method: Delete, pattern, handler} +} + +/** Middleware type */ +type middleware = (request, handler) => promise + +/** Apply middleware to a handler */ +let withMiddleware = (handler: handler, middleware: middleware): handler => { + async request => { + await middleware(request, handler) + } +} + +/** CORS middleware */ +let cors = ( + ~origin: string="*", + ~methods: string="GET, POST, PUT, PATCH, DELETE, OPTIONS", + ~headers: string="Content-Type, Authorization", +): middleware => { + async (request, next) => { + if method(request) == Options { + makeResponse( + "", + { + status: 204, + headers: Dict.fromArray([ + ("Access-Control-Allow-Origin", origin), + ("Access-Control-Allow-Methods", methods), + ("Access-Control-Allow-Headers", headers), + ]), + }, + ) + } else { + let response = await next(request) + response + } + } +} + +/** Logging middleware */ +let logging = (): middleware => { + async (request, next) => { + let start = Date.now() + let response = await next(request) + let duration = Date.now() -. start + Console.log( + `${requestMethod(request)} ${path(request)} - ${Float.toString(duration)}ms`, + ) + response + } +} diff --git a/src/HttpServer.resi b/src/HttpServer.resi new file mode 100644 index 0000000..7104cff --- /dev/null +++ b/src/HttpServer.resi @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +type method = + | @as("GET") Get + | @as("POST") Post + | @as("PUT") Put + | @as("PATCH") Patch + | @as("DELETE") Delete + | @as("HEAD") Head + | @as("OPTIONS") Options + +type server +type request = Fetch.Request.t +type response = Fetch.Response.t + +type responseInit = { + status?: int, + statusText?: string, + headers?: Dict.t, +} + +type serveOptions = { + port?: int, + hostname?: string, + onListen?: {"port": int, "hostname": string} => unit, + onError?: Error.t => response, +} + +type handler = request => promise + +type listenInfo = { + port: int, + hostname: string, +} + +let serve: (serveOptions, handler) => server +let serveHandler: handler => server +let shutdown: server => promise +let finished: server => promise +let addr: server => listenInfo + +let method: request => method +let path: request => string +let searchParams: request => URLSearchParams.t +let header: (request, string) => option +let text: request => promise +let json: request => promise +let requestUrl: request => string +let requestMethod: request => string +let requestHeaders: request => Fetch.Headers.t + +let respond: (~status: int=?, ~headers: Dict.t=?, string) => response +let respondJson: (~status: int=?, ~headers: Dict.t=?, JSON.t) => response +let respondHtml: (~status: int=?, ~headers: Dict.t=?, string) => response +let redirect: (~status: int=?, string) => response +let notFound: (~body: string=?) => response +let badRequest: (~body: string=?) => response +let serverError: (~body: string=?) => response +let unauthorized: (~body: string=?) => response +let forbidden: (~body: string=?) => response + +type route = { + method: method, + pattern: string, + handler: request => promise, +} + +let router: array => handler +let matchPath: (string, string) => bool +let get: (string, request => promise) => route +let post: (string, request => promise) => route +let put: (string, request => promise) => route +let patch: (string, request => promise) => route +let delete: (string, request => promise) => route + +type middleware = (request, handler) => promise +let withMiddleware: (handler, middleware) => handler +let cors: (~origin: string=?, ~methods: string=?, ~headers: string=?) => middleware +let logging: unit => middleware From a740b90a0ed6db53237d8403019924081c0842c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 23:45:21 +0000 Subject: [PATCH 2/2] Add comprehensive documentation for rescript-full-stack ecosystem - Add detailed README.adoc with API reference, examples, and usage - Update ROADMAP.adoc with versioned milestones (v0.1.0 to v1.0.0) - Add STATE.adoc with current project status and component tracking - Update all .machine_readable/ files with project-specific metadata: - STATE.scm: Project state with completion tracking - META.scm: Architecture decisions and development practices - ECOSYSTEM.scm: Position in rescript-full-stack ecosystem - AGENTIC.scm: AI agent interaction patterns - PLAYBOOK.scm: Operational runbook with procedures - NEUROSYM.scm: Neurosymbolic integration config --- .machine_readable/AGENTIC.scm | 45 +++- .machine_readable/ECOSYSTEM.scm | 59 ++++- .machine_readable/META.scm | 42 ++- .machine_readable/NEUROSYM.scm | 53 +++- .machine_readable/PLAYBOOK.scm | 64 ++++- .machine_readable/STATE.scm | 75 ++++-- README.adoc | 437 ++++++++++++++++++++++++++++++++ ROADMAP.adoc | 150 ++++++++++- STATE.adoc | 119 +++++++++ 9 files changed, 977 insertions(+), 67 deletions(-) diff --git a/.machine_readable/AGENTIC.scm b/.machine_readable/AGENTIC.scm index 651bc74..7460c82 100644 --- a/.machine_readable/AGENTIC.scm +++ b/.machine_readable/AGENTIC.scm @@ -1,16 +1,49 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; AGENTIC.scm - AI agent interaction patterns for rsr-template-repo +;; AGENTIC.scm - AI agent interaction patterns for rescript-http-server (define agentic-config `((version . "1.0.0") + (project . "rescript-http-server") + (claude-code ((model . "claude-opus-4-5-20251101") - (tools . ("read" "edit" "bash" "grep" "glob")) - (permissions . "read-all"))) + (tools . ("read" "edit" "bash" "grep" "glob" "write")) + (permissions . "read-all") + (context-files . ("src/HttpServer.res" + "src/HttpServer.resi" + "README.adoc" + "ROADMAP.adoc")))) + (patterns ((code-review . "thorough") (refactoring . "conservative") - (testing . "comprehensive"))) + (testing . "comprehensive") + (documentation . "asciidoc-preferred"))) + (constraints - ((languages . ()) - (banned . ("typescript" "go" "python" "makefile")))))) + ((languages . ("rescript" "javascript")) + (banned . ("typescript" "go" "python" "node")) + (runtime . "deno-only") + (style . "functional-first"))) + + (task-guidance + ((adding-routes . + "Add route helpers in HttpServer.res, expose in .resi, document in README") + (adding-middleware . + "Follow cors/logging pattern. Async function wrapping handler.") + (bug-fixes . + "Check both .res and .resi for type consistency") + (new-features . + "Update ROADMAP.adoc, STATE.scm, and README.adoc"))) + + (code-patterns + ((handlers . "async request => response") + (middleware . "(request, handler) => promise") + (ffi . "@val @scope(\"Deno\") external") + (error-handling . "Result or Option types, try/catch at boundaries"))) + + (testing-guidance + ((framework . "Deno.test") + (location . "tests/") + (naming . "*_test.res") + (run . "deno test --allow-net"))))) diff --git a/.machine_readable/ECOSYSTEM.scm b/.machine_readable/ECOSYSTEM.scm index 23e27df..2fd2e44 100644 --- a/.machine_readable/ECOSYSTEM.scm +++ b/.machine_readable/ECOSYSTEM.scm @@ -1,20 +1,59 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; ECOSYSTEM.scm - Ecosystem position for rsr-template-repo +;; ECOSYSTEM.scm - Ecosystem position for rescript-http-server ;; Media-Type: application/vnd.ecosystem+scm (ecosystem (version "1.0") - (name "rsr-template-repo") - (type "") - (purpose "") + (name "rescript-http-server") + (type "library") + (purpose "Type-safe HTTP server bindings for ReScript using Deno.serve") (position-in-ecosystem - (category "") - (subcategory "") - (unique-value ())) + (category "runtime") + (subcategory "http-server") + (layer "Runtime Layer (L4)") + (unique-value + ("First-class ReScript bindings to Deno.serve") + ("Type-safe routing with pattern matching") + ("Composable middleware architecture") + ("Zero runtime overhead FFI"))) - (related-projects ()) + (parent-ecosystem + (name "rescript-full-stack") + (url "https://github.com/hyperpolymath/rescript-full-stack") + (role "HTTP server component for backend services")) - (what-this-is ()) + (related-projects + (siblings + (("rescript-tea" . "Elm Architecture for frontend") + ("rescript-postgres" . "Type-safe PostgreSQL client") + ("rescript-websocket" . "WebSocket client/server") + ("rescript-redis" . "Redis with Streams and Cluster") + ("rescript-env" . "Environment variable access") + ("rescript-wasm-runtime" . "WASM module support"))) + (complements + (("@rescript/core" . "Core utilities and types") + ("rescript-schema" . "JSON validation"))) + (alternatives + (("oak" . "Deno middleware framework (TypeScript)") + ("hono" . "Lightweight web framework (TypeScript)") + ("express" . "Node.js framework (not Deno-compatible)")))) - (what-this-is-not ())) + (integration-points + (("rescript-postgres" . "Database access from route handlers") + ("rescript-redis" . "Caching and session storage") + ("rescript-websocket" . "Upgrade HTTP connections to WebSocket") + ("rescript-env" . "Configuration from environment"))) + + (what-this-is + ("ReScript bindings to Deno.serve HTTP server API") + ("Type-safe request/response handling") + ("Pattern-based routing system") + ("Middleware infrastructure for cross-cutting concerns") + ("Part of the rescript-full-stack ecosystem")) + + (what-this-is-not + ("Not a full web framework (no templates, ORM, sessions)") + ("Not Node.js compatible (Deno only)") + ("Not a static file server (use Deno's serveDir)") + ("Not a WebSocket server (use rescript-websocket)"))) diff --git a/.machine_readable/META.scm b/.machine_readable/META.scm index 9df6e6d..fd467d2 100644 --- a/.machine_readable/META.scm +++ b/.machine_readable/META.scm @@ -1,17 +1,45 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; META.scm - Meta-level information for rsr-template-repo +;; META.scm - Meta-level information for rescript-http-server ;; Media-Type: application/meta+scheme (meta - (architecture-decisions ()) + (architecture-decisions + (("deno-native" . + "Built exclusively for Deno runtime, not Node.js. Leverages Deno.serve for HTTP handling.") + ("ffi-bindings" . + "Direct FFI to Deno APIs via @val/@send externals. Zero runtime wrapper overhead.") + ("async-first" . + "All I/O operations return promises. Handlers are async functions.") + ("middleware-composition" . + "Middleware wraps handlers. Applied via withMiddleware for explicit composition.") + ("pattern-routing" . + "Simple pattern matching with exact and wildcard (*) support. Path params planned for v0.2.0."))) (development-practices - (code-style ()) + (code-style + (("format" . "rescript format") + ("lint" . "rescript compiler warnings") + ("naming" . "camelCase for functions, PascalCase for types"))) (security - (principle "Defense in depth")) - (testing ()) + (principle "Defense in depth") + (practices + ("No eval or dynamic code execution") + ("CORS disabled by default") + ("Headers sanitized in responses"))) + (testing + (framework "Deno.test") + (coverage-target "80%") + (strategy "Unit tests for helpers, integration for routing")) (versioning "SemVer") (documentation "AsciiDoc") - (branching "main for stable")) + (branching "main for stable, feature branches for development")) - (design-rationale ())) + (design-rationale + (("minimal-api" . + "Small surface area. Core request/response helpers only. Middleware for extensions.") + ("no-global-state" . + "Server configuration passed explicitly. No singleton patterns.") + ("interface-files" . + ".resi file defines public API. Implementation details hidden.") + ("ecosystem-alignment" . + "Follows rescript-full-stack conventions. Compatible with rescript-tea, rescript-postgres.")))) diff --git a/.machine_readable/NEUROSYM.scm b/.machine_readable/NEUROSYM.scm index fd60688..65c7371 100644 --- a/.machine_readable/NEUROSYM.scm +++ b/.machine_readable/NEUROSYM.scm @@ -1,13 +1,56 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; NEUROSYM.scm - Neurosymbolic integration config for rsr-template-repo +;; NEUROSYM.scm - Neurosymbolic integration config for rescript-http-server (define neurosym-config `((version . "1.0.0") + (project . "rescript-http-server") + (symbolic-layer ((type . "scheme") (reasoning . "deductive") - (verification . "formal"))) + (verification . "type-checked") + (artifacts + ((".machine_readable/STATE.scm" . "Project state tracking") + (".machine_readable/META.scm" . "Architecture decisions") + (".machine_readable/ECOSYSTEM.scm" . "Ecosystem position") + (".machine_readable/PLAYBOOK.scm" . "Operational procedures"))))) + (neural-layer - ((embeddings . false) - (fine-tuning . false))) - (integration . ()))) + ((embeddings . #f) + (fine-tuning . #f) + (llm-assistance . #t) + (assistance-patterns + (("code-generation" . "Route handlers, middleware") + ("documentation" . "API docs, examples") + ("testing" . "Test case generation") + ("debugging" . "Error analysis and fixes"))))) + + (integration + ((state-tracking . + "STATE.scm updated on significant changes") + (decision-logging . + "META.scm records architecture decisions") + (ecosystem-awareness . + "ECOSYSTEM.scm defines relationships") + (llm-context . + "AGENTIC.scm guides AI interactions"))) + + (type-system-integration + ((rescript-types . "Primary type safety layer") + (resi-interfaces . "API boundary definition") + (symbolic-validation . "Scheme S-expressions for metadata"))) + + (knowledge-representation + ((domain . "HTTP server development") + (concepts + (("request" . "Incoming HTTP request with method, path, headers, body") + ("response" . "Outgoing HTTP response with status, headers, body") + ("handler" . "Async function transforming request to response") + ("middleware" . "Function wrapping handler with cross-cutting logic") + ("route" . "Mapping from method+pattern to handler"))) + (relationships + (("handler" "processes" "request") + ("handler" "produces" "response") + ("middleware" "wraps" "handler") + ("router" "dispatches-to" "handler") + ("server" "invokes" "handler"))))))) diff --git a/.machine_readable/PLAYBOOK.scm b/.machine_readable/PLAYBOOK.scm index 23d2745..7d086f7 100644 --- a/.machine_readable/PLAYBOOK.scm +++ b/.machine_readable/PLAYBOOK.scm @@ -1,13 +1,61 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; PLAYBOOK.scm - Operational runbook for rsr-template-repo +;; PLAYBOOK.scm - Operational runbook for rescript-http-server (define playbook `((version . "1.0.0") + (project . "rescript-http-server") + (procedures - ((deploy . (("build" . "just build") - ("test" . "just test") - ("release" . "just release"))) - (rollback . ()) - (debug . ()))) - (alerts . ()) - (contacts . ()))) + ((build + (steps + (("compile" . "deno task build") + ("verify" . "Check src/HttpServer.res.js exists"))) + (success-criteria . "No compiler errors, .res.js file generated")) + + (test + (steps + (("unit" . "deno test tests/") + ("integration" . "deno test --allow-net tests/integration/"))) + (success-criteria . "All tests pass")) + + (release + (steps + (("version" . "Update version in deno.json and STATE.scm") + ("changelog" . "Update ROADMAP.adoc with completed features") + ("tag" . "git tag v$VERSION") + ("push" . "git push origin main --tags"))) + (success-criteria . "Tag pushed, version updated")) + + (dev-server + (steps + (("watch" . "deno task dev") + ("example" . "deno run --allow-net examples/basic.res.js"))) + (notes . "Watch mode rebuilds on file changes")) + + (clean + (steps + (("artifacts" . "deno task clean") + ("cache" . "rm -rf lib/"))) + (notes . "Removes compiled JavaScript and build cache")))) + + (common-issues + ((issue . "Compiler error: Unbound module Fetch") + (cause . "Missing @rescript/core dependency") + (fix . "Add @rescript/core to bs-dependencies in rescript.json")) + + ((issue . "Runtime error: Deno is not defined") + (cause . "Running with Node.js instead of Deno") + (fix . "Use deno run instead of node")) + + ((issue . "Type error in handler") + (cause . "Handler not returning promise") + (fix . "Ensure handler is async or returns promise explicitly"))) + + (alerts + ((security . "Monitor Deno security advisories") + (dependencies . "Check @rescript/core updates monthly"))) + + (contacts + ((maintainer . "hyperpolymath") + (issues . "github.com/hyperpolymath/rescript-http-server/issues") + (ecosystem . "github.com/hyperpolymath/rescript-full-stack"))))) diff --git a/.machine_readable/STATE.scm b/.machine_readable/STATE.scm index 1f57360..1c05338 100644 --- a/.machine_readable/STATE.scm +++ b/.machine_readable/STATE.scm @@ -1,39 +1,74 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; STATE.scm - Project state for rsr-template-repo +;; STATE.scm - Project state for rescript-http-server ;; Media-Type: application/vnd.state+scm (state (metadata - (version "0.0.1") + (version "0.1.0") (schema-version "1.0") - (created "2026-01-03") - (updated "2026-01-03") - (project "rsr-template-repo") - (repo "github.com/hyperpolymath/rsr-template-repo")) + (created "2025-01-04") + (updated "2025-01-04") + (project "rescript-http-server") + (repo "github.com/hyperpolymath/rescript-http-server")) (project-context - (name "rsr-template-repo") - (tagline "") - (tech-stack ())) + (name "rescript-http-server") + (tagline "Type-safe HTTP server bindings for ReScript using Deno.serve") + (tech-stack + ("ReScript" "11+") + ("Deno" "1.40+") + ("@rescript/core" "latest"))) (current-position - (phase "initial") - (overall-completion 0) - (components ()) - (working-features ())) + (phase "alpha") + (overall-completion 40) + (components + (("HttpServer.res" . "complete") + ("HttpServer.resi" . "complete") + ("deno-bindings" . "complete") + ("routing" . "complete") + ("middleware" . "complete") + ("tests" . "not-started") + ("path-params" . "not-started"))) + (working-features + ("Deno.serve FFI bindings") + ("Request method/path/headers/body access") + ("Response builders (text, JSON, HTML)") + ("Error responses (400, 401, 403, 404, 500)") + ("Redirects") + ("Pattern-based routing with wildcards") + ("Middleware composition") + ("CORS middleware") + ("Logging middleware"))) (route-to-mvp - (milestones ())) + (milestones + (("v0.1.0" . "Foundation - complete") + ("v0.2.0" . "Enhanced routing - path params, groups") + ("v0.3.0" . "Advanced middleware - rate limiting, compression") + ("v0.4.0" . "Body handling - multipart, streaming") + ("v0.5.0" . "Security - CSRF, JWT, auth") + ("v1.0.0" . "Production ready - tests, benchmarks, docs")))) (blockers-and-issues (critical) (high) - (medium) - (low)) + (medium + ("Path parameter extraction not implemented") + ("No test suite")) + (low + ("No multipart form parsing") + ("No streaming support"))) (critical-next-actions - (immediate) - (this-week) - (this-month)) + (immediate + ("Add path parameter extraction")) + (this-week + ("Create test suite") + ("Add rate limiting middleware")) + (this-month + ("Implement body size limits") + ("Add compression middleware"))) - (session-history ())) + (session-history + (("2025-01-04" . "Initial implementation with core HTTP server functionality")))) diff --git a/README.adoc b/README.adoc index 8b13789..44d701e 100644 --- a/README.adoc +++ b/README.adoc @@ -1 +1,438 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath += ReScript HTTP Server +:toc: macro +:toc-title: Contents +:toclevels: 3 +:icons: font +:source-highlighter: rouge +:experimental: +Type-safe HTTP server bindings for ReScript using Deno.serve. + +image:https://img.shields.io/badge/ReScript-v11-e84f4f[ReScript] +image:https://img.shields.io/badge/Runtime-Deno-000000[Deno] +image:https://img.shields.io/badge/License-AGPL--3.0--or--later-blue[License] + +toc::[] + +== Overview + +*rescript-http-server* provides idiomatic ReScript bindings to https://deno.land/api?s=Deno.serve[Deno.serve], enabling type-safe HTTP server development without leaving the ReScript ecosystem. + +Part of the https://github.com/hyperpolymath/rescript-full-stack[ReScript Full Stack] ecosystem. + +=== Key Features + +* *Type-safe routing* - Pattern-based routing with compile-time checks +* *Middleware support* - Composable middleware chain (CORS, logging, custom) +* *Request helpers* - Type-safe access to method, path, headers, body +* *Response builders* - Fluent API for JSON, HTML, redirects, errors +* *Zero runtime overhead* - Compiles to clean JavaScript with no wrapper cost +* *Deno-native* - Built for Deno's secure, modern runtime + +== Installation + +=== Prerequisites + +* https://deno.land[Deno] v1.40+ +* https://rescript-lang.org[ReScript] v11+ +* https://github.com/rescript-association/rescript-core[@rescript/core] + +=== Setup + +Add to your `rescript.json`: + +[source,json] +---- +{ + "bs-dependencies": [ + "@rescript/core", + "@hyperpolymath/rescript-http-server" + ] +} +---- + +== Quick Start + +=== Basic Server + +[source,rescript] +---- +open HttpServer + +let handler = async request => { + switch (method(request), path(request)) { + | (Get, "/") => respond("Hello, World!") + | (Get, "/health") => respondJson(JSON.Encode.object([("status", JSON.Encode.string("ok"))])) + | _ => notFound() + } +} + +let server = serve({port: 8000, onListen: info => { + Console.log(`Server running at http://${info["hostname"]}:${Int.toString(info["port"])}`) +}}, handler) +---- + +=== Using the Router + +[source,rescript] +---- +open HttpServer + +let routes = [ + get("/", async _ => respond("Home")), + get("/api/users", async _ => respondJson(usersData)), + post("/api/users", async req => { + let body = await json(req) + // Process body... + respondJson(~status=201, newUser) + }), + get("/api/*", async _ => respondJson(apiIndex)), +] + +let server = serve({port: 8000}, router(routes)) +---- + +=== With Middleware + +[source,rescript] +---- +open HttpServer + +let handler = router([ + get("/", async _ => respond("Hello")), + get("/data", async _ => respondJson(data)), +]) + +// Apply middleware chain +let withLogging = withMiddleware(handler, logging()) +let withCors = withMiddleware(withLogging, cors(~origin="https://example.com")) + +let server = serve({port: 8000}, withCors) +---- + +== API Reference + +=== Server Functions + +[cols="1,2,1"] +|=== +|Function |Description |Returns + +|`serve(options, handler)` +|Start HTTP server with options +|`server` + +|`serveHandler(handler)` +|Start server with defaults (port 8000) +|`server` + +|`shutdown(server)` +|Gracefully stop server +|`promise` + +|`finished(server)` +|Wait for server to finish +|`promise` + +|`addr(server)` +|Get listening address +|`listenInfo` +|=== + +=== Request Helpers + +[cols="1,2,1"] +|=== +|Function |Description |Returns + +|`method(req)` +|HTTP method as variant +|`method` + +|`path(req)` +|URL pathname +|`string` + +|`searchParams(req)` +|Query parameters +|`URLSearchParams.t` + +|`header(req, name)` +|Get header value +|`option` + +|`text(req)` +|Read body as text +|`promise` + +|`json(req)` +|Parse body as JSON +|`promise` + +|`requestUrl(req)` +|Full URL string +|`string` + +|`requestHeaders(req)` +|All headers +|`Fetch.Headers.t` +|=== + +=== Response Builders + +[cols="1,2"] +|=== +|Function |Description + +|`respond(~status?, ~headers?, body)` +|Plain text response + +|`respondJson(~status?, ~headers?, data)` +|JSON response with correct Content-Type + +|`respondHtml(~status?, ~headers?, html)` +|HTML response + +|`redirect(~status?, location)` +|Redirect response (default 302) + +|`notFound(~body?)` +|404 Not Found + +|`badRequest(~body?)` +|400 Bad Request + +|`unauthorized(~body?)` +|401 Unauthorized + +|`forbidden(~body?)` +|403 Forbidden + +|`serverError(~body?)` +|500 Internal Server Error +|=== + +=== Routing + +[cols="1,2"] +|=== +|Function |Description + +|`router(routes)` +|Create handler from route array + +|`get(pattern, handler)` +|Define GET route + +|`post(pattern, handler)` +|Define POST route + +|`put(pattern, handler)` +|Define PUT route + +|`patch(pattern, handler)` +|Define PATCH route + +|`delete(pattern, handler)` +|Define DELETE route + +|`matchPath(pattern, path)` +|Check if path matches pattern +|=== + +Pattern matching supports: + +* Exact match: `"/users"` matches `/users` +* Wildcard suffix: `"/api/*"` matches `/api/anything` + +=== Middleware + +[cols="1,2"] +|=== +|Function |Description + +|`withMiddleware(handler, middleware)` +|Wrap handler with middleware + +|`cors(~origin?, ~methods?, ~headers?)` +|CORS middleware with configurable options + +|`logging()` +|Request logging middleware +|=== + +== Types + +[source,rescript] +---- +type method = Get | Post | Put | Patch | Delete | Head | Options + +type handler = request => promise + +type middleware = (request, handler) => promise + +type route = { + method: method, + pattern: string, + handler: request => promise, +} + +type serveOptions = { + port?: int, + hostname?: string, + onListen?: {"port": int, "hostname": string} => unit, + onError?: Error.t => response, +} + +type listenInfo = { + port: int, + hostname: string, +} +---- + +== Examples + +=== JSON API Server + +[source,rescript] +---- +open HttpServer + +type user = {id: int, name: string, email: string} + +let users = ref([ + {id: 1, name: "Alice", email: "alice@example.com"}, + {id: 2, name: "Bob", email: "bob@example.com"}, +]) + +let encodeUser = user => JSON.Encode.object([ + ("id", JSON.Encode.int(user.id)), + ("name", JSON.Encode.string(user.name)), + ("email", JSON.Encode.string(user.email)), +]) + +let routes = [ + get("/api/users", async _ => { + respondJson(JSON.Encode.array(users.contents, encodeUser)) + }), + + get("/api/users/*", async req => { + let idStr = path(req)->String.sliceToEnd(~start=12) + switch Int.fromString(idStr) { + | Some(id) => + switch users.contents->Array.find(u => u.id == id) { + | Some(user) => respondJson(encodeUser(user)) + | None => notFound(~body="User not found") + } + | None => badRequest(~body="Invalid user ID") + } + }), + + post("/api/users", async req => { + let body = await json(req) + // Validate and create user... + respondJson(~status=201, body) + }), +] + +let handler = router(routes) + ->withMiddleware(logging()) + ->withMiddleware(cors()) + +let _ = serve({port: 8000, onListen: _ => Console.log("API ready")}, handler) +---- + +=== Static File Server + +[source,rescript] +---- +open HttpServer + +let serveFile = async path => { + try { + let content = await Deno.readTextFile(path) + let contentType = switch { + | path->String.endsWith(".html") => "text/html" + | path->String.endsWith(".css") => "text/css" + | path->String.endsWith(".js") => "application/javascript" + | _ => "text/plain" + } + respondHtml(~headers=Dict.fromArray([("Content-Type", contentType)]), content) + } catch { + | _ => notFound() + } +} + +let handler = async req => { + let reqPath = path(req) + let filePath = switch reqPath { + | "/" => "./public/index.html" + | p => "./public" ++ p + } + await serveFile(filePath) +} + +let _ = serve({port: 3000}, handler) +---- + +== Ecosystem Integration + +This package is part of the https://github.com/hyperpolymath/rescript-full-stack[ReScript Full Stack] ecosystem: + +[cols="1,2"] +|=== +|Package |Purpose + +|https://github.com/hyperpolymath/rescript-tea[rescript-tea] +|Elm Architecture for frontend + +|https://github.com/hyperpolymath/rescript-postgres[rescript-postgres] +|Type-safe PostgreSQL client + +|https://github.com/hyperpolymath/rescript-websocket[rescript-websocket] +|WebSocket client/server + +|https://github.com/hyperpolymath/rescript-redis[rescript-redis] +|Redis client with Streams + +|https://github.com/hyperpolymath/rescript-env[rescript-env] +|Environment variable access +|=== + +== Development + +[source,bash] +---- +# Build +deno task build + +# Watch mode +deno task dev + +# Clean +deno task clean +---- + +== Design Principles + +Following the https://github.com/hyperpolymath/rescript-full-stack[ReScript Full Stack] design philosophy: + +1. *No global state* - Configuration passed explicitly +2. *Interface files* - `.resi` defines public API boundary +3. *Minimal dependencies* - Only `@rescript/core` required +4. *Configuration over convention* - Explicit options, no magic +5. *Async-first* - All I/O returns promises + +== License + +SPDX-License-Identifier: AGPL-3.0-or-later + +Copyright (C) 2025 Hyperpolymath + +See link:LICENSE.txt[LICENSE.txt] for full terms. + +== Contributing + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for guidelines. diff --git a/ROADMAP.adoc b/ROADMAP.adoc index feb9954..da40958 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -1,22 +1,150 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -= Rsr Template Repo Roadmap +// SPDX-FileCopyrightText: 2025 Hyperpolymath += ReScript HTTP Server Roadmap +:toc: +:toclevels: 2 == Current Status -Initial development phase. +*v0.1.0* - Initial release with core HTTP server functionality. + +== Version History + +=== v0.1.0 - Foundation (Current) + +[cols="1,3,1"] +|=== +|Status |Feature |Notes + +|[x] +|Deno.serve FFI bindings +|Core server functionality + +|[x] +|Request helpers (method, path, headers, body) +|Type-safe request access + +|[x] +|Response builders (text, JSON, HTML, redirects) +|Fluent response API + +|[x] +|Simple pattern-based routing +|Exact and wildcard matching + +|[x] +|Middleware infrastructure +|Composable middleware chain + +|[x] +|CORS middleware +|Configurable origins/methods + +|[x] +|Logging middleware +|Request timing +|=== == Milestones -=== v0.1.0 - Foundation -* [ ] Core functionality -* [ ] Basic documentation -* [ ] CI/CD pipeline +=== v0.2.0 - Enhanced Routing -=== v1.0.0 - Stable Release -* [ ] Full feature set -* [ ] Comprehensive tests -* [ ] Production ready +* [ ] Path parameter extraction (`/users/:id`) +* [ ] Query parameter parsing helpers +* [ ] Route groups with shared middleware +* [ ] Route prefixing +* [ ] Method-agnostic route matching + +=== v0.3.0 - Advanced Middleware + +* [ ] Rate limiting middleware +* [ ] Request validation middleware +* [ ] Compression middleware (gzip, brotli) +* [ ] ETag/caching middleware +* [ ] Error boundary middleware +* [ ] Timeout middleware + +=== v0.4.0 - Body Handling + +* [ ] Multipart form data parsing +* [ ] File upload handling +* [ ] Streaming request/response bodies +* [ ] Body size limits +* [ ] Content-Type negotiation + +=== v0.5.0 - Security + +* [ ] CSRF protection middleware +* [ ] Helmet-style security headers +* [ ] Request signing/verification +* [ ] Basic auth middleware +* [ ] JWT validation middleware + +=== v1.0.0 - Production Ready + +* [ ] Comprehensive test suite +* [ ] Performance benchmarks +* [ ] Full API documentation +* [ ] Migration guide from Express/Oak +* [ ] Production deployment guide == Future Directions -_To be determined based on community feedback._ +=== HTTP/2 Support + +When Deno stabilizes HTTP/2 server APIs: + +* Server push +* Stream multiplexing +* Header compression + +=== WebSocket Integration + +Seamless upgrade from HTTP: + +* WebSocket route handlers +* Shared middleware with HTTP routes +* Integration with https://github.com/hyperpolymath/rescript-websocket[rescript-websocket] + +=== OpenAPI/Swagger + +Type-safe API documentation: + +* Route introspection +* OpenAPI schema generation +* Swagger UI integration + +=== Observability + +Production monitoring: + +* OpenTelemetry integration +* Distributed tracing +* Metrics collection +* Health check endpoints + +== Non-Goals + +These are explicitly out of scope: + +* *Template engines* - Use rescript-tea for views +* *ORM integration* - Use rescript-postgres directly +* *Session management* - Stateless by design; use external stores +* *Static file serving* - Deno's native `serveDir` is sufficient + +== Contributing + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for how to propose features. + +Priority is given to: + +1. Bug fixes +2. Performance improvements +3. Features aligned with rescript-full-stack ecosystem +4. Community-requested features with clear use cases + +== Related Projects + +* https://github.com/hyperpolymath/rescript-full-stack[rescript-full-stack] - Ecosystem overview +* https://github.com/hyperpolymath/rescript-websocket[rescript-websocket] - WebSocket support +* https://github.com/hyperpolymath/rescript-postgres[rescript-postgres] - Database access diff --git a/STATE.adoc b/STATE.adoc index e69de29..222ed2a 100644 --- a/STATE.adoc +++ b/STATE.adoc @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath += ReScript HTTP Server - Project State +:toc: + +== Overview + +[cols="1,2"] +|=== +|Project |rescript-http-server +|Version |0.1.0 +|Status |Alpha - Core functionality complete +|Last Updated |2025-01-04 +|=== + +== Current Phase + +*Foundation Complete* - Core HTTP server bindings are implemented and functional. + +== Component Status + +[cols="2,1,2"] +|=== +|Component |Status |Notes + +|HttpServer.res +|Complete +|Core implementation + +|HttpServer.resi +|Complete +|Public API interface + +|Deno.serve bindings +|Complete +|FFI to Deno runtime + +|Request helpers +|Complete +|method, path, headers, body + +|Response builders +|Complete +|text, JSON, HTML, errors + +|Router +|Complete +|Pattern-based routing + +|Middleware +|Complete +|CORS, logging, custom + +|Tests +|Not Started +|Needed for v0.2.0 + +|Documentation +|Complete +|README with examples +|=== + +== Working Features + +* Start HTTP server on configurable port/hostname +* Handle GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS +* Parse request path, query params, headers, body +* Build text, JSON, HTML responses +* Standard error responses (400, 401, 403, 404, 500) +* Redirects (301, 302, etc.) +* Pattern-based routing with wildcards +* Middleware composition +* CORS with configurable origins +* Request logging + +== Known Limitations + +* Path parameters (`:id`) not yet supported +* No multipart form parsing +* No streaming body support +* No built-in rate limiting +* No compression + +== Dependencies + +[cols="1,1,2"] +|=== +|Dependency |Version |Purpose + +|@rescript/core +|latest +|Core ReScript utilities + +|Deno +|1.40+ +|Runtime environment + +|ReScript +|11+ +|Compiler +|=== + +== Next Actions + +1. Add path parameter extraction +2. Create test suite +3. Add rate limiting middleware +4. Document deployment patterns + +== Session Log + +=== 2025-01-04 + +* Created HttpServer.res with full Deno.serve bindings +* Created HttpServer.resi interface +* Added routing system with pattern matching +* Added middleware infrastructure +* Implemented CORS and logging middleware +* Created comprehensive README documentation