diff --git a/.gemini/settings.json b/.gemini/settings.json
deleted file mode 100644
index 9bd4773..0000000
--- a/.gemini/settings.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "version": "1.0",
- "project": {
- "name": "BanglaCode",
- "type": "Programming Language Interpreter",
- "language": "Go",
- "runtime": "Go ≥1.20"
- },
- "context": {
- "fileName": ["GEMINI.md", "SYNTAX.md"],
- "hierarchical": true
- },
- "tools": {
- "enabled": ["codeSearch", "fileOperations", "shellCommands"]
- }
-}
diff --git a/Documentation/app/docs/http-routing/page.tsx b/Documentation/app/docs/http-routing/page.tsx
index 6226d15..3605e86 100644
--- a/Documentation/app/docs/http-routing/page.tsx
+++ b/Documentation/app/docs/http-routing/page.tsx
@@ -426,6 +426,229 @@ pathao authRouter; // Always export your router`}
+ Path Parameters
+ Use :name segments in route paths. Values are available in req["params"] as a MAP.
+
+{`dhoro app = router_banao();
+
+// Single param: /users/123
+app.ana("/users/:id", kaj(req, res) {
+ dhoro id = req["params"]["id"];
+ json_uttor(res, {"user_id": id});
+});
+
+// Multiple params: /posts/42/comments/7
+app.ana("/posts/:pid/comments/:cid", kaj(req, res) {
+ dhoro pid = req["params"]["pid"];
+ dhoro cid = req["params"]["cid"];
+ json_uttor(res, {"post": pid, "comment": cid});
+});`}
+
+
+
+
+ Query String Parsing
+ req["query"] is a parsed MAP. Use req["query_raw"] for the raw string.
+
+{`// GET /search?q=hello&page=2
+app.ana("/search", kaj(req, res) {
+ dhoro term = req["query"]["q"]; // "hello"
+ dhoro page = req["query"]["page"]; // "2"
+ json_uttor(res, {"term": term, "page": page});
+});`}
+
+
+
+
+ Auto JSON Body Parsing
+ When the request has Content-Type: application/json, req["json"] is auto-parsed. Otherwise it is khali.
+
+{`app.pathano("/users", kaj(req, res) {
+ dhoro user = req["json"]; // auto-parsed MAP — no json_poro() needed
+ dekho("Name:", user["name"]);
+ json_uttor(res, {"created": sotti}, 201);
+});`}
+
+
+
+
+ Middleware (majhe - মাঝে)
+ Runs before every route handler. Call agorao() (আগাও = go forward) to pass to the next layer.
+
+{`dhoro app = router_banao();
+
+// Logging middleware
+app.majhe(kaj(req, res, agorao) {
+ dekho(req["method"], req["path"]);
+ agorao(); // must call to continue!
+});
+
+// Auth middleware
+app.majhe(kaj(req, res, agorao) {
+ jodi (req["headers"]["Authorization"] == khali) {
+ json_uttor(res, {"error": "Unauthorized"}, 401);
+ ferao; // stop here — don't call agorao()
+ }
+ agorao();
+});
+
+app.ana("/", kaj(req, res) {
+ uttor(res, "Protected page");
+});`}
+
+
+
+
+ CORS (cors_chharpao - ছাড়পাও)
+ Enables Cross-Origin Resource Sharing. Call before defining routes.
+
+{`dhoro app = router_banao();
+
+cors_chharpao(app); // allow all origins (default)
+
+// Custom options
+cors_chharpao(app, {
+ "origin": "https://myapp.com",
+ "methods": "GET,POST,PUT,DELETE"
+});`}
+
+
+
+
+ Static File Serving (file_dao - ফাইল দাও)
+
+{`dhoro app = router_banao();
+file_dao(app, "/public", "./static_files");
+// GET /public/style.css → serves ./static_files/style.css`}
+
+
+
+
+ Cookie Handling
+ Read cookies from req["kukis"]. Set cookies with kuki_rakho() (কুকি রাখো).
+
+{`app.ana("/profile", kaj(req, res) {
+ dhoro sessionToken = req["kukis"]["session"];
+ json_uttor(res, {"token": sessionToken});
+});
+
+app.pathano("/login", kaj(req, res) {
+ // Basic cookie
+ kuki_rakho(res, "session", "token123");
+
+ // With options
+ kuki_rakho(res, "session", "token123", {
+ "httpOnly": sotti,
+ "maxAge": 86400,
+ "sameSite": "Lax",
+ "secure": sotti
+ });
+ json_uttor(res, {"ok": sotti});
+});`}
+
+
+
+
+ Redirect (ghurao - ঘোরাও)
+
+{`app.ana("/old-page", kaj(req, res) {
+ ghurao(res, "/new-page"); // 302 Found
+});
+
+app.ana("/moved", kaj(req, res) {
+ ghurao(res, "/permanent", 301); // 301 Moved Permanently
+});`}
+
+
+
+
+ HTML File Response (html_uttor - HTML উত্তর)
+
+{`app.ana("/", kaj(req, res) {
+ html_uttor(res, "./views/index.html");
+});`}
+
+
+
+
+ Error Middleware (bhul_sambhalo - ভুল সামলাও)
+ Catches errors returned by route handlers. Register after all routes.
+
+{`bhul_sambhalo(app, kaj(err, req, res) {
+ json_uttor(res, {"error": err["message"]}, 500);
+});`}
+
+
+
+
+ Performance Features
+
+{`dhoro app = router_banao();
+
+goti_shima(app, 100, 60); // rate limit: 100 req/min per IP (গতি সীমা)
+sankochon_chalu(app); // gzip compression (সংকোচন চালু)
+somoy_shima(app, 30); // 30-second timeout (সময় সীমা)
+akaar_shima(app, 1048576); // 1 MB body limit (আকার সীমা)
+log_chalu(app); // request logging (লগ চালু)`}
+
+
+
+
+ Sub-router Mounting (bebohar - ব্যবহার)
+
+{`dhoro userRoutes = router_banao();
+userRoutes.ana("/users", kaj(req, res) { json_uttor(res, {"users": []}); });
+userRoutes.pathano("/users", kaj(req, res) { json_uttor(res, {}, 201); });
+
+dhoro app = router_banao();
+app.bebohar("/api", userRoutes);
+// Now: GET /api/users, POST /api/users`}
+
+
+
+
+ Full Production Example
+
+{`dhoro app = router_banao();
+
+cors_chharpao(app);
+log_chalu(app);
+sankochon_chalu(app);
+somoy_shima(app, 30);
+akaar_shima(app, 1048576);
+goti_shima(app, 100, 60);
+
+app.majhe(kaj(req, res, agorao) {
+ dekho(req["method"], req["path"]);
+ agorao();
+});
+
+file_dao(app, "/public", "./static");
+
+app.ana("/users/:id", kaj(req, res) {
+ dhoro id = req["params"]["id"];
+ json_uttor(res, {"id": id});
+});
+
+app.pathano("/users", kaj(req, res) {
+ dhoro user = req["json"];
+ json_uttor(res, {"created": sotti}, 201);
+});
+
+app.ana("/search", kaj(req, res) {
+ dhoro q = req["query"]["q"];
+ json_uttor(res, {"results": []});
+});
+
+bhul_sambhalo(app, kaj(err, req, res) {
+ json_uttor(res, {"error": err["message"]}, 500);
+});
+
+server_chalu(3000, app);`}
+
+
+
+
Related Topics
- HTTP Server Basics
diff --git a/Documentation/app/docs/http-server/page.tsx b/Documentation/app/docs/http-server/page.tsx
index 4e90bc4..78349fb 100644
--- a/Documentation/app/docs/http-server/page.tsx
+++ b/Documentation/app/docs/http-server/page.tsx
@@ -314,14 +314,127 @@ dhoro data = json_poro(response.body);
dekho(data);`}
/>
+ Enhanced HTTP Client (anun)
+ anun() now supports all HTTP methods via an optional options map. Backward-compatible: one-argument GET still works.
+
+{`// GET (unchanged)
+dhoro res = anun("https://api.example.com/users");
+
+// POST with JSON body
+dhoro res = anun("https://api.example.com/users", {
+ "method": "POST",
+ "body": json_banao({"name": "Ankan", "email": "a@b.com"}),
+ "headers": {"Content-Type": "application/json"}
+});
+
+// PUT
+dhoro res = anun("https://api.example.com/users/1", {
+ "method": "PUT",
+ "body": json_banao({"name": "Updated"})
+});
+
+// DELETE
+dhoro res = anun("https://api.example.com/users/1", {
+ "method": "DELETE"
+});
+
+dekho("Status:", res["status"]);
+dhoro data = json_poro(res["body"]);`}
+
+
+ Async HTTP Client (anun_async)
+
+{`proyash kaj createUser(userData) {
+ dhoro res = opekha anun_async("https://api.example.com/users", {
+ "method": "POST",
+ "body": json_banao(userData),
+ "headers": {"Content-Type": "application/json"}
+ });
+ ferao json_poro(res["body"]);
+}
+
+dhoro user = opekha createUser({"name": "Ankan"});
+dekho("Created:", user);`}
+
+
+ Request Object Reference
+ All fields available on req inside route handlers:
+
+
+
+
+ | Field |
+ Type |
+ Description |
+
+
+
+ {[
+ ["req[\"method\"]", "STRING", "HTTP method: GET, POST, PUT, ..."],
+ ["req[\"path\"]", "STRING", "Request path: /users/123"],
+ ["req[\"ip\"]", "STRING", "Client IP address"],
+ ["req[\"headers\"]", "MAP", "All request headers"],
+ ["req[\"body\"]", "STRING", "Raw request body"],
+ ["req[\"json\"]", "MAP/NULL", "Auto-parsed JSON body (when Content-Type: application/json)"],
+ ["req[\"form\"]", "MAP/NULL", "URL-encoded form data (when Content-Type: application/x-www-form-urlencoded)"],
+ ["req[\"params\"]", "MAP", "Path params: route /users/:id → req[\"params\"][\"id\"]"],
+ ["req[\"query\"]", "MAP", "Parsed query string: ?q=hi → req[\"query\"][\"q\"]"],
+ ["req[\"query_raw\"]", "STRING", "Raw query string: \"q=hi&page=2\""],
+ ["req[\"kukis\"]", "MAP", "Parsed cookies: req[\"kukis\"][\"session\"]"],
+ ].map(([field, type, desc]) => (
+
+ | {field} |
+ {type} |
+ {desc} |
+
+ ))}
+
+
+
+
+ Performance Helpers Reference
+
+
+
+
+ | Function |
+ Bengali |
+ Description |
+
+
+
+ {[
+ ["goti_shima(app, max, window)", "গতি সীমা", "Rate limit: max requests per window seconds per IP"],
+ ["sankochon_chalu(app)", "সংকোচন চালু", "Enable gzip compression"],
+ ["somoy_shima(app, secs)", "সময় সীমা", "Request timeout in seconds"],
+ ["akaar_shima(app, bytes)", "আকার সীমা", "Max request body size in bytes"],
+ ["log_chalu(app)", "লগ চালু", "Enable request logging"],
+ ["cors_chharpao(app, opts?)", "ছাড়পাও", "Enable CORS"],
+ ["file_dao(app, url, dir)", "ফাইল দাও", "Serve static files"],
+ ["ghurao(res, url, status?)", "ঘোরাও", "HTTP redirect (default 302)"],
+ ["kuki_rakho(res, name, val, opts?)", "কুকি রাখো", "Set response cookie"],
+ ["html_uttor(res, filepath)", "HTML উত্তর", "Serve HTML file"],
+ ["bhul_sambhalo(app, handler)", "ভুল সামলাও", "Global error middleware"],
+ ].map(([fn, bn, desc]) => (
+
+ | {fn} |
+ {bn} |
+ {desc} |
+
+ ))}
+
+
+
+
Best Practices
- Always validate input - Never trust client data
- Use appropriate status codes - 200 for success, 404 for not found, etc.
- Set correct content types - Especially for JSON and HTML
- - Handle errors gracefully - Wrap handlers in try-catch
+ - Handle errors gracefully - Use
bhul_sambhalo() for centralized error handling
- Use JSON for APIs - It's the standard for data exchange
+ - Enable production features -
cors_chharpao, goti_shima, sankochon_chalu, somoy_shima for production deployments
diff --git a/Documentation/package.json b/Documentation/package.json
index ffdb457..1088ede 100644
--- a/Documentation/package.json
+++ b/Documentation/package.json
@@ -1,6 +1,6 @@
{
"name": "documentation",
- "version": "9.2.1",
+ "version": "9.3.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
diff --git a/Extension/package.json b/Extension/package.json
index 76b48e5..bc37ced 100644
--- a/Extension/package.json
+++ b/Extension/package.json
@@ -2,7 +2,7 @@
"name": "banglacode",
"displayName": "BanglaCode",
"description": "Language support for BanglaCode (.bang, .bangla, .bong) - Bengali Programming Language created by Ankan from West Bengal, India",
- "version": "9.2.1",
+ "version": "9.3.0",
"publisher": "AnkanSaha",
"author": {
"name": "AnkanSaha"
diff --git a/Extension/snippets/banglacode.json b/Extension/snippets/banglacode.json
index b13bfd7..a7ed3bd 100644
--- a/Extension/snippets/banglacode.json
+++ b/Extension/snippets/banglacode.json
@@ -1591,6 +1591,173 @@
],
"description": "Mount sub-router at path (রাউটার ব্যবহার)"
},
+ "Middleware (majhe)": {
+ "prefix": "majhe",
+ "body": [
+ "${1:app}.majhe(kaj(req, res, agorao) {",
+ "\t$2",
+ "\tagorao();",
+ "});"
+ ],
+ "description": "Add middleware to router - agorao() calls next (মাঝে)"
+ },
+ "Route with Path Params": {
+ "prefix": "route-params",
+ "body": [
+ "${1:app}.ana(\"/${2:resource}/:${3:id}\", kaj(req, res) {",
+ "\tdhoro ${3:id} = req[\"params\"][\"${3:id}\"];",
+ "\t$4",
+ "});"
+ ],
+ "description": "Route with path parameter (:id)"
+ },
+ "CORS Enable": {
+ "prefix": "cors",
+ "body": [
+ "cors_chharpao(${1:app});"
+ ],
+ "description": "Enable CORS with defaults (ছাড়পাও)"
+ },
+ "CORS with Options": {
+ "prefix": "cors-opts",
+ "body": [
+ "cors_chharpao(${1:app}, {",
+ "\t\"origin\": \"${2:*}\",",
+ "\t\"methods\": \"${3:GET,POST,PUT,DELETE,PATCH}\"",
+ "});"
+ ],
+ "description": "Enable CORS with custom options"
+ },
+ "Static Files": {
+ "prefix": "file-dao",
+ "body": [
+ "file_dao(${1:app}, \"${2:/public}\", \"${3:./static}\");"
+ ],
+ "description": "Serve static files from directory (ফাইল দাও)"
+ },
+ "Redirect": {
+ "prefix": "ghurao",
+ "body": [
+ "ghurao(res, \"${1:/new-url}\");"
+ ],
+ "description": "HTTP redirect (ঘোরাও)"
+ },
+ "Set Cookie": {
+ "prefix": "kuki-rakho",
+ "body": [
+ "kuki_rakho(res, \"${1:name}\", ${2:value});"
+ ],
+ "description": "Set cookie on response (কুকি রাখো)"
+ },
+ "Set Cookie with Options": {
+ "prefix": "kuki-opts",
+ "body": [
+ "kuki_rakho(res, \"${1:name}\", ${2:value}, {",
+ "\t\"httpOnly\": sotti,",
+ "\t\"maxAge\": ${3:3600},",
+ "\t\"sameSite\": \"Lax\"",
+ "});"
+ ],
+ "description": "Set cookie with security options"
+ },
+ "HTML Response": {
+ "prefix": "html-uttor",
+ "body": [
+ "html_uttor(res, \"${1:./views/index.html}\");"
+ ],
+ "description": "Serve HTML file as response (HTML উত্তর)"
+ },
+ "Rate Limit": {
+ "prefix": "goti-shima",
+ "body": [
+ "goti_shima(${1:app}, ${2:100}, ${3:60});"
+ ],
+ "description": "Per-IP rate limit: max requests per window seconds (গতি সীমা)"
+ },
+ "Gzip Compression": {
+ "prefix": "sankochon",
+ "body": [
+ "sankochon_chalu(${1:app});"
+ ],
+ "description": "Enable gzip compression (সংকোচন চালু)"
+ },
+ "Request Timeout": {
+ "prefix": "somoy-shima",
+ "body": [
+ "somoy_shima(${1:app}, ${2:30});"
+ ],
+ "description": "Set request handler timeout in seconds (সময় সীমা)"
+ },
+ "Body Size Limit": {
+ "prefix": "akaar-shima",
+ "body": [
+ "akaar_shima(${1:app}, ${2:1048576});"
+ ],
+ "description": "Set max request body size in bytes (আকার সীমা)"
+ },
+ "Error Handler": {
+ "prefix": "bhul-sambhalo",
+ "body": [
+ "bhul_sambhalo(${1:app}, kaj(err, req, res) {",
+ "\tjson_uttor(res, {\"error\": err[\"message\"]}, 500);",
+ "});"
+ ],
+ "description": "Register global error handler (ভুল সামলাও)"
+ },
+ "HTTP POST Client": {
+ "prefix": "anun-post",
+ "body": [
+ "dhoro ${1:res} = anun(\"${2:https://api.example.com/endpoint}\", {",
+ "\t\"method\": \"POST\",",
+ "\t\"body\": json_banao($3),",
+ "\t\"headers\": {\"Content-Type\": \"application/json\"}",
+ "});",
+ "dhoro ${4:data} = json_poro(${1:res}[\"body\"]);"
+ ],
+ "description": "HTTP POST request with JSON body"
+ },
+ "Production Server Template": {
+ "prefix": "server-production",
+ "body": [
+ "dhoro app = router_banao();",
+ "",
+ "// Production middleware",
+ "cors_chharpao(app);",
+ "log_chalu(app);",
+ "sankochon_chalu(app);",
+ "somoy_shima(app, 30);",
+ "akaar_shima(app, 1048576);",
+ "goti_shima(app, ${1:100}, 60);",
+ "",
+ "// Auth middleware",
+ "app.majhe(kaj(req, res, agorao) {",
+ "\t// dhoro token = req[\"headers\"][\"Authorization\"];",
+ "\tagorao();",
+ "});",
+ "",
+ "// Static files",
+ "file_dao(app, \"/public\", \"./static\");",
+ "",
+ "// Routes",
+ "app.ana(\"/${2:resource}/:id\", kaj(req, res) {",
+ "\tdhoro id = req[\"params\"][\"id\"];",
+ "\tjson_uttor(res, {\"id\": id});",
+ "});",
+ "",
+ "app.pathano(\"/${2:resource}\", kaj(req, res) {",
+ "\tdhoro body = req[\"json\"];",
+ "\tjson_uttor(res, {\"created\": sotti}, 201);",
+ "});",
+ "",
+ "// Error handler",
+ "bhul_sambhalo(app, kaj(err, req, res) {",
+ "\tjson_uttor(res, {\"error\": err[\"message\"]}, 500);",
+ "});",
+ "",
+ "server_chalu(${3:3000}, app);"
+ ],
+ "description": "Full production-ready Express.js-style server template"
+ },
"Router Full App": {
"prefix": "router-app",
"body": [
diff --git a/Extension/syntaxes/banglacode.tmLanguage.json b/Extension/syntaxes/banglacode.tmLanguage.json
index 781011d..a1ca008 100644
--- a/Extension/syntaxes/banglacode.tmLanguage.json
+++ b/Extension/syntaxes/banglacode.tmLanguage.json
@@ -281,7 +281,7 @@
},
{
"name": "support.function.js",
- "match": "\\b(server_chalu|router_banao|anun|uttor|json_uttor)\\b"
+ "match": "\\b(server_chalu|router_banao|anun|anun_async|uttor|json_uttor|cors_chharpao|file_dao|ghurao|kuki_rakho|html_uttor|log_chalu|goti_shima|sankochon_chalu|somoy_shima|akaar_shima|bhul_sambhalo)\\b"
},
{
"name": "support.function.builtin.network.js",
diff --git a/GEMINI.md b/GEMINI.md
deleted file mode 100644
index e5a659e..0000000
--- a/GEMINI.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# GEMINI.md
-
-This file provides guidance to Gemini Code Assist when working with code in this repository.
-
-## Project Overview
-
-**BanglaCode** - Educational Programming Language in Bengali
-
-- **Language**: Go ≥1.20
-- **Type**: Tree-walking interpreter for Bengali-syntax programming
-- **Platform**: Cross-platform (Linux, macOS, Windows)
-- **Purpose**: Make programming accessible to 300+ million Bengali speakers
-
-## Commands
-
-```bash
-# Build & Run
-go build -o banglacode main.go
-./banglacode examples/hello.bang
-./banglacode # REPL
-
-# Test
-go test ./...
-go fmt ./...
-go vet ./...
-
-# Cross-compile
-GOOS=windows GOARCH=amd64 go build -o banglacode.exe .
-GOOS=darwin GOARCH=arm64 go build -o banglacode .
-```
-
-## Core Rules (NON-NEGOTIABLE)
-
-1. **Bengali keywords**: Use Banglish (Bengali in English script)
-2. **ALWAYS test**: Run test files after changes
-3. **ALWAYS build**: `go build` after code changes
-4. **Maintain compatibility**: Don't break existing BanglaCode programs
-5. **Clear errors**: Bengali-friendly error messages
-6. **Update docs**: README, SYNTAX.md, Documentation/
-
-## Architecture
-
-### Interpreter Pipeline
-```
-Source Code → Lexer → Parser → AST → Evaluator → Result
-```
-
-### Structure
-```
-src/
-├── lexer/ # Tokenization (29 Bengali keywords)
-├── parser/ # Pratt parsing, AST building
-├── ast/ # Node definitions
-├── object/ # Runtime types, Environment
-├── evaluator/ # Tree-walking interpreter
-│ ├── evaluator.go # Main Eval() switch
-│ ├── builtins.go # 135+ built-in functions
-│ ├── async.go # Async/await, promises
-│ ├── classes.go # OOP support
-│ ├── modules.go # Import/export
-│ └── errors.go # Try/catch/finally
-└── repl/ # Interactive shell
-
-examples/ # BanglaCode example programs
-test/ # Test files
-Extension/ # VS Code extension
-```
-
-## Go Standards
-
-### Error Handling
-```go
-// ✅ GOOD
-if err != nil {
- return newError("ভুল: %s", err.Error())
-}
-
-// ❌ BAD
-if err != nil {
- return nil
-}
-```
-
-### Type Safety
-```go
-// ✅ Use object types
-type Integer struct {
- Value int64
-}
-
-// ✅ Type assertions
-intObj, ok := obj.(*object.Integer)
-if !ok {
- return newError("সংখ্যা প্রত্যাশিত")
-}
-```
-
-## Key Patterns
-
-### Bengali Keywords
-```go
-// Token definitions in lexer/token.go
-var keywords = map[string]TokenType{
- "dhoro": LET, // let/var
- "jodi": IF, // if
- "nahole": ELSE, // else
- "kaj": FUNCTION, // function
- "firao": RETURN, // return
- "proyash": ASYNC, // async
- "opekha": AWAIT, // await
- // ... 29 keywords total
-}
-```
-
-### Built-in Functions
-```go
-// evaluator/builtins.go
-var builtins = map[string]*object.Builtin{
- "dekho": &object.Builtin{Fn: dekhoPrint}, // print
- "dorghyo": &object.Builtin{Fn: dorghyoLength}, // length
- "dhokao": &object.Builtin{Fn: dhokaoPush}, // push
- // ... 135+ functions
-}
-```
-
-### Error Messages
-```go
-// ✅ Bengali-friendly errors
-return newError("'%s' চিহ্ন অপারেটর সমর্থন করে না: %s", operator, left.Type())
-
-// ✅ Context in errors
-return newError("%d লাইনে: অজানা ফাংশন '%s'", node.Line, fn.Name)
-```
-
-## Documentation
-
-Update when features change:
-- README.md - Overview, installation, usage
-- SYNTAX.md - Language syntax reference
-- ARCHITECTURE.md - Interpreter design
-- Documentation/ - Website docs
-- Extension/ - VS Code extension features
-
-## Testing
-
-- Unit tests for lexer, parser, evaluator
-- Integration tests for full programs
-- Example programs in `examples/`
-- REPL testing
-- Cross-platform testing
-
-## Bengali Language Features
-
-### Keywords (29 total)
-- Variables: `dhoro`, `sthir`, `protiti`
-- Control: `jodi`, `nahole`, `jokhon`, `bhango`
-- Functions: `kaj`, `firao`, `proyash`, `opekha`
-- OOP: `dol`, `notun`, `ei`, `super`
-- Modules: `ano`, `theke`, `pathao`, `hisabe`
-- Error: `chesta`, `dhoro_bhul`, `shesh`, `chhar`
-
-### Built-in Functions (135+)
-- I/O: `dekho`, `input`, `file_lekho`, `file_poro`
-- String: `dorghyo`, `boro_hater`, `choto_hater`
-- Array: `dhokao`, `chhino`, `jog_koro`, `filter_koro`
-- Math: `gononaa`, `bolod`, `muladhon`, `ghuriye`
-- HTTP: `http_server`, `http_get`, `http_post`
-- Database: `postgres_connect`, `mysql_connect`, `mongo_connect`
-- Async: `ghumaao`, `somoy_dekhao`, `proyash_solve`
-
-## Definition of "Done"
-
-- ✅ `go build` passes
-- ✅ `go test ./...` passes
-- ✅ Existing BanglaCode programs work
-- ✅ Documentation updated
-- ✅ Error messages in Bengali
-- ✅ Cross-platform tested
diff --git a/SYNTAX.md b/SYNTAX.md
index 6aaf3ec..adc1463 100644
--- a/SYNTAX.md
+++ b/SYNTAX.md
@@ -700,20 +700,63 @@ kaj handleRequest(req, res) {
server_chalu(3000, handleRequest);
```
-### HTTP Client
+### HTTP Client (anun - আনুন)
-Make HTTP requests:
+`anun()` supports GET (1 argument) and any method via options map (2 arguments):
```banglacode
+// GET (backward compatible)
dhoro response = anun("https://api.example.com/data");
dekho("Status:", response["status"]);
-dekho("Body:", response["body"]);
-
-// Parse JSON response
dhoro data = json_poro(response["body"]);
-dekho("Parsed data:", data);
+
+// POST with JSON body
+dhoro response = anun("https://api.example.com/users", {
+ "method": "POST",
+ "body": json_banao({"name": "Ankan"}),
+ "headers": {"Content-Type": "application/json"}
+});
+
+// PUT / DELETE
+dhoro r2 = anun("https://api.example.com/users/1", {"method": "PUT", "body": json_banao({})});
+dhoro r3 = anun("https://api.example.com/users/1", {"method": "DELETE"});
+
+// Async client (anun_async - returns Promise)
+proyash kaj fetchUser(id) {
+ dhoro res = opekha anun_async("https://api.example.com/users/" + id);
+ ferao json_poro(res["body"]);
+}
```
+### New Middleware & Utility Functions
+
+| Function | Bengali | Description |
+|----------|---------|-------------|
+| `app.majhe(handler)` | মাঝে | Add middleware (handler: `kaj(req, res, agorao)`) |
+| `cors_chharpao(app, opts?)` | ছাড়পাও | Enable CORS |
+| `file_dao(app, url, dir)` | ফাইল দাও | Serve static files |
+| `ghurao(res, url, status?)` | ঘোরাও | HTTP redirect (default 302) |
+| `kuki_rakho(res, name, val, opts?)` | কুকি রাখো | Set response cookie |
+| `html_uttor(res, filepath)` | HTML উত্তর | Serve HTML file |
+| `log_chalu(app)` | লগ চালু | Enable request logging |
+| `goti_shima(app, max, sec)` | গতি সীমা | Per-IP rate limiting |
+| `sankochon_chalu(app)` | সংকোচন চালু | Enable gzip compression |
+| `somoy_shima(app, secs)` | সময় সীমা | Request timeout |
+| `akaar_shima(app, bytes)` | আকার সীমা | Body size limit |
+| `bhul_sambhalo(app, handler)` | ভুল সামলাও | Error middleware |
+
+### New Request Object Fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `req["params"]` | MAP | Path params from `:name` segments |
+| `req["query"]` | MAP | Parsed query string (`?key=val`) |
+| `req["query_raw"]` | STRING | Raw query string |
+| `req["json"]` | MAP/NULL | Auto-parsed JSON body |
+| `req["form"]` | MAP/NULL | URL-encoded form data |
+| `req["kukis"]` | MAP | Parsed cookies |
+| `req["ip"]` | STRING | Client IP address |
+
## JSON Functions
BanglaCode provides built-in functions for working with JSON data.
diff --git a/VERSION b/VERSION
index 45acc9e..b13d146 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.2.1
+9.3.0
diff --git a/main.go b/main.go
index b479b14..b0f9f20 100644
--- a/main.go
+++ b/main.go
@@ -82,10 +82,10 @@ func printHelp() {
func printVersion() {
fmt.Println("\033[1;36m╔════════════════════════════════════════════════════════╗")
- fmt.Println("║ BanglaCode v9.2.1 ║")
+ fmt.Println("║ BanglaCode v9.3.0 ║")
fmt.Println("║ A Programming Language in Bengali (Banglish) ║")
fmt.Println("╠════════════════════════════════════════════════════════╣\033[0m")
- fmt.Println("\033[1;36m║\033[0m 📦 \033[1mVersion:\033[0m \033[1;32m9.2.1\033[0m \033[1;36m║\033[0m")
+ fmt.Println("\033[1;36m║\033[0m 📦 \033[1mVersion:\033[0m \033[1;32m9.3.0\033[0m \033[1;36m║\033[0m")
fmt.Println("\033[1;36m║\033[0m 👨💻 \033[1mAuthor:\033[0m \033[1;35mAnkan Saha\033[0m \033[1;36m║\033[0m")
fmt.Println("\033[1;36m║\033[0m 🌍 \033[1mFrom:\033[0m \033[1;37mWest Bengal, India\033[0m \033[1;36m║\033[0m")
fmt.Println("\033[1;36m║\033[0m 🔗 \033[1mGitHub:\033[0m \033[1;34mhttps://github.com/nexoral/BanglaCode\033[0m \033[1;36m║\033[0m")
diff --git a/src/evaluator/builtins/builtins_http.go b/src/evaluator/builtins/builtins_http.go
index 247d90b..b026f77 100644
--- a/src/evaluator/builtins/builtins_http.go
+++ b/src/evaluator/builtins/builtins_http.go
@@ -6,10 +6,12 @@ import (
"fmt"
"io"
"net/http"
+ "strings"
)
func init() {
- // HTTP Server - server_chalu (সার্ভার চালু - start server)
+ // server_chalu (সার্ভার চালু - start server)
+ // Accepts a Router (MAP with __router_id__) or a plain function handler.
Builtins["server_chalu"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) != 2 {
@@ -18,162 +20,114 @@ func init() {
if args[0].Type() != object.NUMBER_OBJ {
return newError("first argument to `server_chalu` must be NUMBER (port), got %s", args[0].Type())
}
-
port := int(args[0].(*object.Number).Value)
- // Check if second argument is a Router (MAP with __router_id__) or Function
+ // Router mode
if args[1].Type() == object.MAP_OBJ {
- // Router-based server
routerMap := args[1].(*object.Map)
-
- if routerIDObj, ok := routerMap.Pairs["__router_id__"]; ok {
- if routerID, ok := routerIDObj.(*object.String); ok {
- if router, found := getRouter(routerID.Value); found {
- fmt.Printf("🚀 Server cholche http://localhost:%d e (Router mode)\n", port)
- err := http.ListenAndServe(fmt.Sprintf(":%d", port), router)
- if err != nil {
- return newError("server error: %s", err.Error())
- }
- return object.NULL
- }
- }
+ ridObj, ok := routerMap.Pairs["__router_id__"]
+ if !ok {
+ return newError("second argument to `server_chalu` is not a valid router")
+ }
+ rid, ok := ridObj.(*object.String)
+ if !ok {
+ return newError("invalid router ID in `server_chalu`")
+ }
+ router, found := getRouter(rid.Value)
+ if !found {
+ return newError("router not found — was it created with router_banao()?")
}
- return newError("invalid router object")
+ fmt.Printf("🚀 Server cholche http://localhost:%d e (Router mode)\n", port)
+ if err := http.ListenAndServe(fmt.Sprintf(":%d", port), router); err != nil {
+ return newError("server error: %s", err.Error())
+ }
+ return object.NULL
}
- // Function-based server (backward compatible)
+ // Function-based mode (backward compatible)
if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to `server_chalu` must be FUNCTION (handler) or ROUTER, got %s", args[1].Type())
+ return newError("second argument to `server_chalu` must be FUNCTION or ROUTER, got %s", args[1].Type())
}
-
handler := args[1].(*object.Function)
-
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- reqMap := &object.Map{Pairs: make(map[string]object.Object)}
- reqMap.Pairs["method"] = &object.String{Value: r.Method}
- reqMap.Pairs["path"] = &object.String{Value: r.URL.Path}
- reqMap.Pairs["query"] = &object.String{Value: r.URL.RawQuery}
-
- headersMap := &object.Map{Pairs: make(map[string]object.Object)}
- for k, v := range r.Header {
- if len(v) > 0 {
- headersMap.Pairs[k] = &object.String{Value: v[0]}
- }
- }
- reqMap.Pairs["headers"] = headersMap
-
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
- reqMap.Pairs["body"] = &object.String{Value: string(body)}
-
- resMap := &object.Map{Pairs: make(map[string]object.Object)}
- resMap.Pairs["status"] = &object.Number{Value: 200}
- resMap.Pairs["body"] = &object.String{Value: ""}
- resMap.Pairs["headers"] = &object.Map{Pairs: make(map[string]object.Object)}
-
- var result object.Object
+ reqMap := buildRequestMap(r, body, nil)
+ resMap := buildResponseMap()
if EvalFunc != nil {
- result = EvalFunc(handler, []object.Object{reqMap, resMap})
- }
-
- if statusObj, ok := resMap.Pairs["status"]; ok {
- if status, ok := statusObj.(*object.Number); ok {
- w.WriteHeader(int(status.Value))
- }
- }
-
- if headersObj, ok := resMap.Pairs["headers"]; ok {
- if headers, ok := headersObj.(*object.Map); ok {
- for k, v := range headers.Pairs {
- w.Header().Set(k, v.Inspect())
- }
- }
- }
-
- if bodyObj, ok := resMap.Pairs["body"]; ok {
- fmt.Fprint(w, bodyObj.Inspect())
- } else if result != nil && result != object.NULL {
- fmt.Fprint(w, result.Inspect())
+ EvalFunc(handler, []object.Object{reqMap, resMap})
}
+ writeHTTPResponse(w, resMap, false)
})
-
fmt.Printf("🚀 Server cholche http://localhost:%d e\n", port)
- err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
- if err != nil {
+ if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
return newError("server error: %s", err.Error())
}
return object.NULL
},
}
- // HTTP GET - anun (আনুন - fetch/bring)
+ // anun (আনুন - HTTP client, backward compatible + extended with options)
+ // anun(url) → GET
+ // anun(url, {method, body, headers}) → any method
Builtins["anun"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
- if len(args) != 1 {
- return newError("wrong number of arguments. got=%d, want=1", len(args))
+ if len(args) < 1 || len(args) > 2 {
+ return newError("wrong number of arguments. got=%d, want=1-2", len(args))
}
if args[0].Type() != object.STRING_OBJ {
- return newError("argument to `anun` must be STRING, got %s", args[0].Type())
+ return newError("first argument to `anun` must be STRING (url), got %s", args[0].Type())
}
- url := args[0].(*object.String).Value
-
- resp, err := http.Get(url)
+ resp, err := doHTTPRequest(args)
if err != nil {
return newError("HTTP error: %s", err.Error())
}
defer resp.Body.Close()
-
body, err := io.ReadAll(resp.Body)
if err != nil {
return newError("error reading response: %s", err.Error())
}
-
result := &object.Map{Pairs: make(map[string]object.Object)}
result.Pairs["status"] = &object.Number{Value: float64(resp.StatusCode)}
result.Pairs["body"] = &object.String{Value: string(body)}
-
return result
},
}
- // Async HTTP GET - anun_async (আনুন_async)
+ // anun_async (আনুন async - async HTTP client)
Builtins["anun_async"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
- if len(args) != 1 {
- return newError("wrong number of arguments. got=%d, want=1", len(args))
+ if len(args) < 1 || len(args) > 2 {
+ return newError("wrong number of arguments. got=%d, want=1-2", len(args))
}
if args[0].Type() != object.STRING_OBJ {
- return newError("argument to `anun_async` must be STRING, got %s", args[0].Type())
+ return newError("first argument to `anun_async` must be STRING (url), got %s", args[0].Type())
}
-
- url := args[0].(*object.String).Value
promise := object.CreatePromise()
-
+ // Capture args slice for goroutine
+ capturedArgs := args
go func() {
- resp, err := http.Get(url)
+ resp, err := doHTTPRequest(capturedArgs)
if err != nil {
object.RejectPromise(promise, newError("HTTP error: %s", err.Error()))
return
}
defer resp.Body.Close()
-
body, err := io.ReadAll(resp.Body)
if err != nil {
object.RejectPromise(promise, newError("error reading response: %s", err.Error()))
return
}
-
result := &object.Map{Pairs: make(map[string]object.Object)}
result.Pairs["status"] = &object.Number{Value: float64(resp.StatusCode)}
result.Pairs["body"] = &object.String{Value: string(body)}
-
object.ResolvePromise(promise, result)
}()
-
return promise
},
}
- // JSON Parse - json_poro (JSON পড়ো - read JSON)
+ // json_poro (JSON পড়ো - parse JSON string)
Builtins["json_poro"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
@@ -182,12 +136,11 @@ func init() {
if args[0].Type() != object.STRING_OBJ {
return newError("argument to `json_poro` must be STRING, got %s", args[0].Type())
}
- jsonStr := args[0].(*object.String).Value
- return parseJSON(jsonStr)
+ return parseJSON(args[0].(*object.String).Value)
},
}
- // JSON Stringify - json_banao (JSON বানাও - make JSON)
+ // json_banao (JSON বানাও - stringify to JSON)
Builtins["json_banao"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
@@ -197,80 +150,104 @@ func init() {
},
}
- // Simple HTTP response helper - uttor (উত্তর - reply/response)
+ // uttor (উত্তর - set response body, status, content-type)
Builtins["uttor"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) < 2 || len(args) > 4 {
- return newError("wrong number of arguments. got=%d, want=2-4 (res, body, [status], [contentType])", len(args))
+ return newError("wrong number of arguments. got=%d, want=2-4", len(args))
}
if args[0].Type() != object.MAP_OBJ {
return newError("first argument to `uttor` must be response MAP, got %s", args[0].Type())
}
resMap := args[0].(*object.Map)
-
- // Set body
resMap.Pairs["body"] = args[1]
-
- // Set status (optional, default 200)
if len(args) >= 3 {
if args[2].Type() != object.NUMBER_OBJ {
return newError("third argument to `uttor` must be NUMBER (status), got %s", args[2].Type())
}
resMap.Pairs["status"] = args[2]
}
-
- // Set content-type (optional)
if len(args) >= 4 {
if args[3].Type() != object.STRING_OBJ {
return newError("fourth argument to `uttor` must be STRING (contentType), got %s", args[3].Type())
}
- if headersObj, ok := resMap.Pairs["headers"]; ok {
- if headers, ok := headersObj.(*object.Map); ok {
- headers.Pairs["Content-Type"] = args[3]
- }
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ h.Pairs["Content-Type"] = args[3]
}
}
-
return resMap
},
}
- // JSON response helper - json_uttor (JSON উত্তর - JSON reply)
+ // json_uttor (JSON উত্তর - send JSON response)
Builtins["json_uttor"] = &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) < 2 || len(args) > 3 {
- return newError("wrong number of arguments. got=%d, want=2-3 (res, data, [status])", len(args))
+ return newError("wrong number of arguments. got=%d, want=2-3", len(args))
}
if args[0].Type() != object.MAP_OBJ {
return newError("first argument to `json_uttor` must be response MAP, got %s", args[0].Type())
}
resMap := args[0].(*object.Map)
-
- // Convert data to JSON string
- jsonStr := stringifyJSON(args[1])
- resMap.Pairs["body"] = &object.String{Value: jsonStr}
-
- // Set status (optional, default 200)
+ resMap.Pairs["body"] = &object.String{Value: stringifyJSON(args[1])}
if len(args) >= 3 {
if args[2].Type() != object.NUMBER_OBJ {
return newError("third argument to `json_uttor` must be NUMBER (status), got %s", args[2].Type())
}
resMap.Pairs["status"] = args[2]
}
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ h.Pairs["Content-Type"] = &object.String{Value: "application/json; charset=utf-8"}
+ }
+ return resMap
+ },
+ }
+}
- // Set content-type to JSON
- if headersObj, ok := resMap.Pairs["headers"]; ok {
- if headers, ok := headersObj.(*object.Map); ok {
- headers.Pairs["Content-Type"] = &object.String{Value: "application/json; charset=utf-8"}
+// doHTTPRequest builds and executes an HTTP request from BanglaCode args.
+// args[0] = url STRING
+// args[1] = options MAP (optional): method, body, headers
+func doHTTPRequest(args []object.Object) (*http.Response, error) {
+ rawURL := args[0].(*object.String).Value
+ method := "GET"
+ bodyStr := ""
+ extraHeaders := map[string]string{}
+
+ if len(args) == 2 && args[1].Type() == object.MAP_OBJ {
+ opts := args[1].(*object.Map)
+ if m, ok := opts.Pairs["method"].(*object.String); ok {
+ method = strings.ToUpper(m.Value)
+ }
+ if b, ok := opts.Pairs["body"].(*object.String); ok {
+ bodyStr = b.Value
+ }
+ if h, ok := opts.Pairs["headers"].(*object.Map); ok {
+ for k, v := range h.Pairs {
+ if vs, ok := v.(*object.String); ok {
+ extraHeaders[k] = vs.Value
}
}
+ }
+ }
- return resMap
- },
+ var bodyReader io.Reader
+ if bodyStr != "" {
+ bodyReader = strings.NewReader(bodyStr)
}
+
+ req, err := http.NewRequest(method, rawURL, bodyReader)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range extraHeaders {
+ req.Header.Set(k, v)
+ }
+
+ client := &http.Client{}
+ return client.Do(req)
}
-// parseJSON converts a JSON string to BanglaCode objects
+// parseJSON converts a JSON string to BanglaCode objects.
func parseJSON(jsonStr string) object.Object {
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
@@ -279,7 +256,7 @@ func parseJSON(jsonStr string) object.Object {
return JsonToObject(data)
}
-// JsonToObject recursively converts Go values to BanglaCode objects
+// JsonToObject recursively converts Go values to BanglaCode objects.
func JsonToObject(data interface{}) object.Object {
switch v := data.(type) {
case nil:
@@ -300,17 +277,17 @@ func JsonToObject(data interface{}) object.Object {
}
return &object.Array{Elements: elements}
case map[string]interface{}:
- pairs := make(map[string]object.Object)
+ pairs := make(map[string]object.Object, len(v))
for key, val := range v {
pairs[key] = JsonToObject(val)
}
return &object.Map{Pairs: pairs}
default:
- return newError("unsupported JSON type")
+ return newError("unsupported JSON type: %T", data)
}
}
-// stringifyJSON converts a BanglaCode object to JSON string
+// stringifyJSON converts a BanglaCode object to a JSON string.
func stringifyJSON(obj object.Object) string {
data := objectToJSON(obj)
bytes, err := json.Marshal(data)
@@ -320,7 +297,7 @@ func stringifyJSON(obj object.Object) string {
return string(bytes)
}
-// objectToJSON recursively converts BanglaCode objects to Go values
+// objectToJSON recursively converts BanglaCode objects to Go values for JSON marshalling.
func objectToJSON(obj object.Object) interface{} {
switch v := obj.(type) {
case *object.Null:
@@ -338,7 +315,7 @@ func objectToJSON(obj object.Object) interface{} {
}
return arr
case *object.Map:
- m := make(map[string]interface{})
+ m := make(map[string]interface{}, len(v.Pairs))
for key, val := range v.Pairs {
m[key] = objectToJSON(val)
}
diff --git a/src/evaluator/builtins/builtins_http_advanced.go b/src/evaluator/builtins/builtins_http_advanced.go
new file mode 100644
index 0000000..8a72045
--- /dev/null
+++ b/src/evaluator/builtins/builtins_http_advanced.go
@@ -0,0 +1,135 @@
+package builtins
+
+import (
+ "BanglaCode/src/object"
+ "time"
+)
+
+func init() {
+ // goti_shima (গতি সীমা - per-IP sliding-window rate limiter)
+ // goti_shima(app, maxRequests, windowSeconds)
+ // Example: goti_shima(app, 100, 60) → max 100 req per 60 seconds per IP
+ Builtins["goti_shima"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 3 {
+ return newError("wrong number of arguments. got=%d, want=3 (app, maxReq, windowSec)", len(args))
+ }
+ router, err := extractRouter("goti_shima", args[0])
+ if err != nil {
+ return err
+ }
+ if args[1].Type() != object.NUMBER_OBJ {
+ return newError("second argument to `goti_shima` must be NUMBER (max requests), got %s", args[1].Type())
+ }
+ if args[2].Type() != object.NUMBER_OBJ {
+ return newError("third argument to `goti_shima` must be NUMBER (window seconds), got %s", args[2].Type())
+ }
+ maxReq := int(args[1].(*object.Number).Value)
+ windowSec := int(args[2].(*object.Number).Value)
+ if maxReq <= 0 || windowSec <= 0 {
+ return newError("goti_shima: maxReq and windowSec must be positive numbers")
+ }
+ router.mu.Lock()
+ router.rateLimiter = &RateLimiter{
+ max: maxReq,
+ window: time.Duration(windowSec) * time.Second,
+ hits: make(map[string][]time.Time),
+ }
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // sankochon_chalu (সংকোচন চালু - enable gzip compression)
+ // sankochon_chalu(app)
+ // Compresses responses when client sends Accept-Encoding: gzip
+ Builtins["sankochon_chalu"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 1 {
+ return newError("wrong number of arguments. got=%d, want=1 (app)", len(args))
+ }
+ router, err := extractRouter("sankochon_chalu", args[0])
+ if err != nil {
+ return err
+ }
+ router.mu.Lock()
+ router.gzipEnabled = true
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // somoy_shima (সময় সীমা - request timeout)
+ // somoy_shima(app, seconds)
+ // Example: somoy_shima(app, 30) → 30-second handler timeout
+ Builtins["somoy_shima"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("wrong number of arguments. got=%d, want=2 (app, seconds)", len(args))
+ }
+ router, err := extractRouter("somoy_shima", args[0])
+ if err != nil {
+ return err
+ }
+ if args[1].Type() != object.NUMBER_OBJ {
+ return newError("second argument to `somoy_shima` must be NUMBER (seconds), got %s", args[1].Type())
+ }
+ secs := args[1].(*object.Number).Value
+ if secs <= 0 {
+ return newError("somoy_shima: seconds must be a positive number")
+ }
+ router.mu.Lock()
+ router.timeout = time.Duration(secs * float64(time.Second))
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // akaar_shima (আকার সীমা - request body size limit)
+ // akaar_shima(app, bytes)
+ // Example: akaar_shima(app, 1048576) → 1 MB body limit
+ Builtins["akaar_shima"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("wrong number of arguments. got=%d, want=2 (app, bytes)", len(args))
+ }
+ router, err := extractRouter("akaar_shima", args[0])
+ if err != nil {
+ return err
+ }
+ if args[1].Type() != object.NUMBER_OBJ {
+ return newError("second argument to `akaar_shima` must be NUMBER (bytes), got %s", args[1].Type())
+ }
+ bytes := int64(args[1].(*object.Number).Value)
+ if bytes <= 0 {
+ return newError("akaar_shima: bytes must be a positive number")
+ }
+ router.mu.Lock()
+ router.maxBodyBytes = bytes
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // bhul_sambhalo (ভুল সামলাও - error middleware handler)
+ // bhul_sambhalo(app, kaj(err, req, res) { ... })
+ // Called when a route handler returns an ERROR object
+ Builtins["bhul_sambhalo"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("wrong number of arguments. got=%d, want=2 (app, handler)", len(args))
+ }
+ router, err := extractRouter("bhul_sambhalo", args[0])
+ if err != nil {
+ return err
+ }
+ if args[1].Type() != object.FUNCTION_OBJ && args[1].Type() != object.BUILTIN_OBJ {
+ return newError("second argument to `bhul_sambhalo` must be FUNCTION (handler), got %s", args[1].Type())
+ }
+ router.mu.Lock()
+ router.errorHandler = args[1]
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+}
diff --git a/src/evaluator/builtins/builtins_http_router.go b/src/evaluator/builtins/builtins_http_router.go
index cc7b175..e0456e4 100644
--- a/src/evaluator/builtins/builtins_http_router.go
+++ b/src/evaluator/builtins/builtins_http_router.go
@@ -2,361 +2,431 @@ package builtins
import (
"BanglaCode/src/object"
+ "compress/gzip"
+ "context"
"fmt"
"io"
+ "net"
"net/http"
+ "net/url"
+ "regexp"
"strings"
"sync"
+ "time"
)
-// Router represents a modular HTTP router (similar to Express.js Router)
-// Supports all 7 common HTTP methods with Banglish method names
+// Route holds a compiled route pattern with its handler.
+type Route struct {
+ pattern string
+ params []string // param names in order: /users/:id → ["id"]
+ re *regexp.Regexp // precompiled once at AddRoute time
+ handler object.Object
+}
+
+// CORSOptions holds CORS configuration.
+type CORSOptions struct {
+ Origin string
+ Methods string
+ Headers string
+ MaxAge string
+}
+
+// FileRoute maps a URL prefix to a static file handler.
+type FileRoute struct {
+ prefix string
+ handler http.Handler
+}
+
+// RateLimiter implements a per-IP sliding-window rate limiter.
+type RateLimiter struct {
+ max int
+ window time.Duration
+ mu sync.Mutex
+ hits map[string][]time.Time
+}
+
+// Allow reports whether the given IP may proceed; trims expired hits.
+func (rl *RateLimiter) Allow(ip string) bool {
+ rl.mu.Lock()
+ defer rl.mu.Unlock()
+ now := time.Now()
+ cutoff := now.Add(-rl.window)
+ prev := rl.hits[ip]
+ valid := prev[:0]
+ for _, t := range prev {
+ if t.After(cutoff) {
+ valid = append(valid, t)
+ }
+ }
+ if len(valid) >= rl.max {
+ rl.hits[ip] = valid
+ return false
+ }
+ rl.hits[ip] = append(valid, now)
+ return true
+}
+
+// Router is a modular HTTP router with Express.js-level features.
type Router struct {
- basePath string
- routes map[string]map[string]*object.Function // method -> path -> handler
- mu sync.RWMutex
+ basePath string
+ routes map[string][]Route // HTTP method → ordered route slice
+ middlewares []object.Object // run before every route handler
+ fileRoutes []FileRoute
+ errorHandler object.Object // bhul_sambhalo handler
+ corsEnabled bool
+ corsOptions CORSOptions
+ gzipEnabled bool
+ logEnabled bool
+ timeout time.Duration
+ maxBodyBytes int64
+ rateLimiter *RateLimiter
+ mu sync.RWMutex
}
-// NewRouter creates a new router instance with support for all HTTP methods
+// NewRouter creates a Router with all HTTP methods pre-initialized.
func NewRouter(basePath string) *Router {
return &Router{
basePath: basePath,
- routes: map[string]map[string]*object.Function{
- "GET": make(map[string]*object.Function),
- "POST": make(map[string]*object.Function),
- "PUT": make(map[string]*object.Function),
- "DELETE": make(map[string]*object.Function),
- "PATCH": make(map[string]*object.Function),
- "HEAD": make(map[string]*object.Function),
- "OPTIONS": make(map[string]*object.Function),
+ routes: map[string][]Route{
+ "GET": {}, "POST": {}, "PUT": {}, "DELETE": {},
+ "PATCH": {}, "HEAD": {}, "OPTIONS": {},
},
}
}
-// AddRoute adds a route to the router
-func (r *Router) AddRoute(method, path string, handler *object.Function) {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- // Normalize path
- if !strings.HasPrefix(path, "/") {
- path = "/" + path
+// compilePattern converts a path pattern into a precompiled regex and param list.
+// Example: /users/:id/posts/:pid → ^/users/([^/]+)/posts/([^/]+)$, ["id","pid"]
+func compilePattern(pattern string) (*regexp.Regexp, []string) {
+ parts := strings.Split(pattern, "/")
+ var paramNames []string
+ var regexParts []string
+ for _, part := range parts {
+ if strings.HasPrefix(part, ":") {
+ paramNames = append(paramNames, part[1:])
+ regexParts = append(regexParts, "([^/]+)")
+ } else {
+ regexParts = append(regexParts, regexp.QuoteMeta(part))
+ }
}
+ re := regexp.MustCompile("^" + strings.Join(regexParts, "/") + "$")
+ return re, paramNames
+}
- // Store route
- if r.routes[method] == nil {
- r.routes[method] = make(map[string]*object.Function)
+// AddRoute registers a route; compiles the pattern once.
+func (r *Router) AddRoute(method, pattern string, handler object.Object) {
+ if !strings.HasPrefix(pattern, "/") {
+ pattern = "/" + pattern
}
- r.routes[method][path] = handler
+ re, params := compilePattern(pattern)
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.routes[method] = append(r.routes[method], Route{
+ pattern: pattern, params: params, re: re, handler: handler,
+ })
}
-// GetHandler finds a handler for the given method and path
-func (r *Router) GetHandler(method, path string) (*object.Function, bool) {
+// FindRoute returns the first matching route and extracted path params.
+func (r *Router) FindRoute(method, path string) (*Route, map[string]string, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
-
- // Remove base path if present
- if r.basePath != "" && strings.HasPrefix(path, r.basePath) {
- path = strings.TrimPrefix(path, r.basePath)
- if path == "" {
- path = "/"
+ for i := range r.routes[method] {
+ route := &r.routes[method][i]
+ matches := route.re.FindStringSubmatch(path)
+ if matches == nil {
+ continue
+ }
+ params := make(map[string]string, len(route.params))
+ for j, name := range route.params {
+ params[name] = matches[j+1]
}
+ return route, params, true
}
-
- handler, ok := r.routes[method][path]
- return handler, ok
+ return nil, nil, false
}
-// MountSubRouter mounts a sub-router at a specific path
-func (r *Router) MountSubRouter(mountPath string, subRouter *Router) {
+// AddMiddleware appends a middleware to the router's chain.
+func (r *Router) AddMiddleware(handler object.Object) {
r.mu.Lock()
defer r.mu.Unlock()
+ r.middlewares = append(r.middlewares, handler)
+}
- // Normalize mount path
+// MountSubRouter copies all routes from subRouter prefixed with mountPath.
+func (r *Router) MountSubRouter(mountPath string, subRouter *Router) {
if !strings.HasPrefix(mountPath, "/") {
mountPath = "/" + mountPath
}
- if strings.HasSuffix(mountPath, "/") && mountPath != "/" {
- mountPath = strings.TrimSuffix(mountPath, "/")
- }
+ mountPath = strings.TrimSuffix(mountPath, "/")
- // Update sub-router's base path
- subRouter.basePath = r.basePath + mountPath
+ subRouter.mu.RLock()
+ defer subRouter.mu.RUnlock()
+ r.mu.Lock()
+ defer r.mu.Unlock()
- // Copy all routes from sub-router with updated paths
for method, routes := range subRouter.routes {
- for path, handler := range routes {
- fullPath := mountPath + path
- if r.routes[method] == nil {
- r.routes[method] = make(map[string]*object.Function)
- }
- r.routes[method][fullPath] = handler
+ for _, route := range routes {
+ fullPattern := mountPath + route.pattern
+ re, params := compilePattern(fullPattern)
+ r.routes[method] = append(r.routes[method], Route{
+ pattern: fullPattern, params: params, re: re, handler: route.handler,
+ })
}
}
}
-// ServeHTTP implements http.Handler interface
+// callHandler invokes either a *object.Function (via EvalFunc) or *object.Builtin directly.
+func callHandler(handler object.Object, args []object.Object) object.Object {
+ switch h := handler.(type) {
+ case *object.Function:
+ if EvalFunc != nil {
+ return EvalFunc(h, args)
+ }
+ case *object.Builtin:
+ return h.Fn(args...)
+ }
+ return object.NULL
+}
+
+// ServeHTTP implements http.Handler with the full Express.js-style pipeline.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- handler, ok := r.GetHandler(req.Method, req.URL.Path)
+ start := time.Now()
- if !ok {
- http.NotFound(w, req)
- return
+ // 1. Read body with optional size limit
+ var bodyReader io.Reader = req.Body
+ if r.maxBodyBytes > 0 {
+ bodyReader = io.LimitReader(req.Body, r.maxBodyBytes)
}
+ body, _ := io.ReadAll(bodyReader)
- // Build request object
- reqMap := &object.Map{Pairs: make(map[string]object.Object)}
- reqMap.Pairs["method"] = &object.String{Value: req.Method}
- reqMap.Pairs["path"] = &object.String{Value: req.URL.Path}
- reqMap.Pairs["query"] = &object.String{Value: req.URL.RawQuery}
+ // 2. CORS headers (before any WriteHeader)
+ if r.corsEnabled {
+ setCORSHeaders(w, r.corsOptions)
+ }
- // Parse headers
- headersMap := &object.Map{Pairs: make(map[string]object.Object)}
- for k, v := range req.Header {
- if len(v) > 0 {
- headersMap.Pairs[k] = &object.String{Value: v[0]}
+ // 3. OPTIONS preflight
+ if req.Method == "OPTIONS" && r.corsEnabled {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ // 4. Static file routes
+ for _, fr := range r.fileRoutes {
+ if strings.HasPrefix(req.URL.Path, fr.prefix) {
+ fr.handler.ServeHTTP(w, req)
+ return
}
}
- reqMap.Pairs["headers"] = headersMap
- // Read body
- body, _ := io.ReadAll(req.Body)
- reqMap.Pairs["body"] = &object.String{Value: string(body)}
+ // 5. Rate limiting
+ if r.rateLimiter != nil {
+ ip := getClientIP(req)
+ if !r.rateLimiter.Allow(ip) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusTooManyRequests)
+ fmt.Fprint(w, `{"error":"গতি সীমা অতিক্রান্ত / Rate limit exceeded"}`)
+ return
+ }
+ }
- // Build response object
- resMap := &object.Map{Pairs: make(map[string]object.Object)}
- resMap.Pairs["status"] = &object.Number{Value: 200}
- resMap.Pairs["body"] = &object.String{Value: ""}
- resMap.Pairs["headers"] = &object.Map{Pairs: make(map[string]object.Object)}
+ // 6. Route matching
+ route, params, ok := r.FindRoute(req.Method, req.URL.Path)
+ if !ok {
+ http.NotFound(w, req)
+ return
+ }
- // Execute handler
- var result object.Object
- if EvalFunc != nil {
- result = EvalFunc(handler, []object.Object{reqMap, resMap})
+ // 7. Build BanglaCode request / response maps
+ reqMap := buildRequestMap(req, body, params)
+ resMap := buildResponseMap()
+
+ // 8. Middleware chain + route handler with optional timeout
+ middlewares := r.middlewares
+ var execute func(idx int)
+ execute = func(idx int) {
+ if idx < len(middlewares) {
+ mw := middlewares[idx]
+ nextFn := &object.Builtin{
+ Fn: func(_ ...object.Object) object.Object {
+ execute(idx + 1)
+ return object.NULL
+ },
+ }
+ callHandler(mw, []object.Object{reqMap, resMap, nextFn})
+ } else {
+ result := callHandler(route.handler, []object.Object{reqMap, resMap})
+ if result != nil && result.Type() == object.ERROR_OBJ && r.errorHandler != nil {
+ callHandler(r.errorHandler, []object.Object{result, reqMap, resMap})
+ }
+ }
}
- // Write response
- if statusObj, ok := resMap.Pairs["status"]; ok {
- if status, ok := statusObj.(*object.Number); ok {
- w.WriteHeader(int(status.Value))
+ if r.timeout > 0 {
+ ctx, cancel := context.WithTimeout(req.Context(), r.timeout)
+ defer cancel()
+ done := make(chan struct{})
+ go func() { execute(0); close(done) }()
+ select {
+ case <-done:
+ case <-ctx.Done():
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusGatewayTimeout)
+ fmt.Fprint(w, `{"error":"Request timeout"}`)
+ return
}
+ } else {
+ execute(0)
}
- if headersObj, ok := resMap.Pairs["headers"]; ok {
- if headers, ok := headersObj.(*object.Map); ok {
- for k, v := range headers.Pairs {
- w.Header().Set(k, v.Inspect())
- }
+ // 9. Logging
+ if r.logEnabled {
+ status := 200
+ if s, ok2 := resMap.Pairs["status"].(*object.Number); ok2 {
+ status = int(s.Value)
}
+ fmt.Printf("🔵 [BanglaCode] %s %s → %d (%v)\n", req.Method, req.URL.Path, status, time.Since(start))
}
- if bodyObj, ok := resMap.Pairs["body"]; ok {
- fmt.Fprint(w, bodyObj.Inspect())
- } else if result != nil && result != object.NULL {
- fmt.Fprint(w, result.Inspect())
+ // 10. Write HTTP response (gzip if requested and enabled)
+ useGzip := r.gzipEnabled && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip")
+ writeHTTPResponse(w, resMap, useGzip)
+}
+
+// setCORSHeaders writes the CORS headers to the response.
+func setCORSHeaders(w http.ResponseWriter, opts CORSOptions) {
+ w.Header().Set("Access-Control-Allow-Origin", opts.Origin)
+ w.Header().Set("Access-Control-Allow-Methods", opts.Methods)
+ w.Header().Set("Access-Control-Allow-Headers", opts.Headers)
+ if opts.MaxAge != "" {
+ w.Header().Set("Access-Control-Max-Age", opts.MaxAge)
}
}
-func init() {
- // router_banao (রাউটার বানাও - create router)
- Builtins["router_banao"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- router := NewRouter("")
-
- // Create a map to represent the router with methods
- routerMap := &object.Map{Pairs: make(map[string]object.Object)}
-
- // Store the actual router instance (we'll use this internally)
- routerMap.Pairs["__internal_router__"] = &object.String{Value: fmt.Sprintf("%p", router)}
-
- // Add ana method (আনা - GET - fetch)
- routerMap.Pairs["ana"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.ana(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.ana() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.ana() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("GET", path, handler)
-
- return routerMap // Return router for chaining
- },
- }
+// getClientIP extracts the real client IP, respecting proxy headers.
+func getClientIP(req *http.Request) string {
+ if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
+ return strings.TrimSpace(strings.SplitN(xff, ",", 2)[0])
+ }
+ if xri := req.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+ ip, _, err := net.SplitHostPort(req.RemoteAddr)
+ if err != nil {
+ return req.RemoteAddr
+ }
+ return ip
+}
- // Add pathano method (পাঠানো - POST - send)
- routerMap.Pairs["pathano"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.pathano(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.pathano() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.pathano() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("POST", path, handler)
-
- return routerMap
- },
- }
+// buildRequestMap constructs the BanglaCode req object with all parsed fields.
+func buildRequestMap(req *http.Request, body []byte, params map[string]string) *object.Map {
+ m := &object.Map{Pairs: make(map[string]object.Object, 10)}
+ m.Pairs["method"] = &object.String{Value: req.Method}
+ m.Pairs["path"] = &object.String{Value: req.URL.Path}
+ m.Pairs["ip"] = &object.String{Value: getClientIP(req)}
- // Add bodlano method (বদলানো - PUT - update/change)
- routerMap.Pairs["bodlano"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.bodlano(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.bodlano() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.bodlano() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("PUT", path, handler)
-
- return routerMap
- },
- }
+ // Headers
+ headersMap := &object.Map{Pairs: make(map[string]object.Object, len(req.Header))}
+ for k, v := range req.Header {
+ if len(v) > 0 {
+ headersMap.Pairs[k] = &object.String{Value: v[0]}
+ }
+ }
+ m.Pairs["headers"] = headersMap
- // Add mujhe_felo method (মুছে ফেলো - DELETE - remove)
- routerMap.Pairs["mujhe_felo"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.mujhe_felo(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.mujhe_felo() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.mujhe_felo() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("DELETE", path, handler)
-
- return routerMap
- },
- }
+ // Raw body
+ m.Pairs["body"] = &object.String{Value: string(body)}
- // Add songshodhon method (সংশোধন - PATCH - modify)
- routerMap.Pairs["songshodhon"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.songshodhon(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.songshodhon() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.songshodhon() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("PATCH", path, handler)
-
- return routerMap
- },
- }
+ // Auto JSON parse
+ ct := req.Header.Get("Content-Type")
+ if strings.Contains(ct, "application/json") && len(body) > 0 {
+ m.Pairs["json"] = parseJSON(string(body))
+ } else {
+ m.Pairs["json"] = object.NULL
+ }
- // Add matha method (মাথা - HEAD - retrieve headers)
- routerMap.Pairs["matha"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.matha(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.matha() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.matha() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("HEAD", path, handler)
-
- return routerMap
- },
+ // URL-encoded form data
+ if strings.Contains(ct, "application/x-www-form-urlencoded") {
+ if formVals, err := url.ParseQuery(string(body)); err == nil {
+ formMap := &object.Map{Pairs: make(map[string]object.Object, len(formVals))}
+ for k, v := range formVals {
+ if len(v) > 0 {
+ formMap.Pairs[k] = &object.String{Value: v[0]}
+ }
}
+ m.Pairs["form"] = formMap
+ } else {
+ m.Pairs["form"] = object.NULL
+ }
+ } else {
+ m.Pairs["form"] = object.NULL
+ }
- // Add nirdharon method (নির্ধারণ - OPTIONS - determine options)
- routerMap.Pairs["nirdharon"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.nirdharon(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.nirdharon() must be STRING (path), got %s", args[0].Type())
- }
- if args[1].Type() != object.FUNCTION_OBJ {
- return newError("second argument to router.nirdharon() must be FUNCTION (handler), got %s", args[1].Type())
- }
-
- path := args[0].(*object.String).Value
- handler := args[1].(*object.Function)
- router.AddRoute("OPTIONS", path, handler)
-
- return routerMap
- },
- }
- // Add bebohār method (ব্যবহার - use/mount sub-router)
- routerMap.Pairs["bebohar"] = &object.Builtin{
- Fn: func(args ...object.Object) object.Object {
- if len(args) != 2 {
- return newError("wrong number of arguments to router.bebohar(). got=%d, want=2", len(args))
- }
- if args[0].Type() != object.STRING_OBJ {
- return newError("first argument to router.bebohar() must be STRING (mount path), got %s", args[0].Type())
- }
- if args[1].Type() != object.MAP_OBJ {
- return newError("second argument to router.bebohar() must be ROUTER (sub-router), got %s", args[1].Type())
- }
-
- mountPath := args[0].(*object.String).Value
- subRouterMap := args[1].(*object.Map)
-
- // Extract the internal router pointer
- if internalObj, ok := subRouterMap.Pairs["__internal_router__"]; ok {
- if _, ok := internalObj.(*object.String); ok {
- // In a real implementation, we'd store routers in a global map
- // For now, we'll recreate the logic
- subRouter := NewRouter("")
-
- // Mount the sub-router
- router.MountSubRouter(mountPath, subRouter)
- }
- }
-
- return routerMap
- },
- }
+ // Path params
+ paramsMap := &object.Map{Pairs: make(map[string]object.Object, len(params))}
+ for k, v := range params {
+ paramsMap.Pairs[k] = &object.String{Value: v}
+ }
+ m.Pairs["params"] = paramsMap
- // Store router in global registry for server_chalu to use
- registerRouter(router)
- routerMap.Pairs["__router_id__"] = &object.String{Value: fmt.Sprintf("%p", router)}
+ // Query string (parsed MAP + raw)
+ queryMap := &object.Map{Pairs: make(map[string]object.Object)}
+ for k, v := range req.URL.Query() {
+ if len(v) > 0 {
+ queryMap.Pairs[k] = &object.String{Value: v[0]}
+ }
+ }
+ m.Pairs["query"] = queryMap
+ m.Pairs["query_raw"] = &object.String{Value: req.URL.RawQuery}
- return routerMap
- },
+ // Cookies
+ kukisMap := &object.Map{Pairs: make(map[string]object.Object)}
+ for _, c := range req.Cookies() {
+ kukisMap.Pairs[c.Name] = &object.String{Value: c.Value}
+ }
+ m.Pairs["kukis"] = kukisMap
+
+ return m
+}
+
+// buildResponseMap creates the initial BanglaCode res object.
+func buildResponseMap() *object.Map {
+ m := &object.Map{Pairs: make(map[string]object.Object, 3)}
+ m.Pairs["status"] = &object.Number{Value: 200}
+ m.Pairs["body"] = &object.String{Value: ""}
+ m.Pairs["headers"] = &object.Map{Pairs: make(map[string]object.Object)}
+ return m
+}
+
+// writeHTTPResponse writes the BanglaCode res map to the HTTP response.
+// Headers are always set before WriteHeader to comply with HTTP/1.1.
+func writeHTTPResponse(w http.ResponseWriter, resMap *object.Map, useGzip bool) {
+ // Set custom headers first
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ for k, v := range h.Pairs {
+ w.Header().Set(k, v.Inspect())
+ }
+ }
+ status := 200
+ if s, ok := resMap.Pairs["status"].(*object.Number); ok {
+ status = int(s.Value)
+ }
+ body := ""
+ if b, ok := resMap.Pairs["body"]; ok {
+ body = b.Inspect()
+ }
+
+ if useGzip {
+ w.Header().Set("Content-Encoding", "gzip")
+ w.WriteHeader(status)
+ gz := gzip.NewWriter(w)
+ defer gz.Close()
+ fmt.Fprint(gz, body)
+ } else {
+ w.WriteHeader(status)
+ fmt.Fprint(w, body)
}
}
-// Global router registry
+// Global router registry — maps pointer string → *Router.
var (
routerRegistry = make(map[string]*Router)
routerRegistryMu sync.RWMutex
@@ -365,8 +435,7 @@ var (
func registerRouter(r *Router) {
routerRegistryMu.Lock()
defer routerRegistryMu.Unlock()
- id := fmt.Sprintf("%p", r)
- routerRegistry[id] = r
+ routerRegistry[fmt.Sprintf("%p", r)] = r
}
func getRouter(id string) (*Router, bool) {
diff --git a/src/evaluator/builtins/builtins_http_router_builtins.go b/src/evaluator/builtins/builtins_http_router_builtins.go
new file mode 100644
index 0000000..7963bd0
--- /dev/null
+++ b/src/evaluator/builtins/builtins_http_router_builtins.go
@@ -0,0 +1,159 @@
+package builtins
+
+import (
+ "BanglaCode/src/object"
+ "fmt"
+)
+
+func init() {
+ // router_banao (রাউটার বানাও - create router)
+ Builtins["router_banao"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ router := NewRouter("")
+ registerRouter(router)
+
+ routerMap := &object.Map{Pairs: make(map[string]object.Object)}
+ routerMap.Pairs["__router_id__"] = &object.String{Value: fmt.Sprintf("%p", router)}
+
+ // ana (আনা - GET - fetch)
+ routerMap.Pairs["ana"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.ana", args); err != nil {
+ return err
+ }
+ router.AddRoute("GET", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // pathano (পাঠানো - POST - send)
+ routerMap.Pairs["pathano"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.pathano", args); err != nil {
+ return err
+ }
+ router.AddRoute("POST", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // bodlano (বদলানো - PUT - update)
+ routerMap.Pairs["bodlano"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.bodlano", args); err != nil {
+ return err
+ }
+ router.AddRoute("PUT", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // mujhe_felo (মুছে ফেলো - DELETE - remove)
+ routerMap.Pairs["mujhe_felo"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.mujhe_felo", args); err != nil {
+ return err
+ }
+ router.AddRoute("DELETE", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // songshodhon (সংশোধন - PATCH - modify)
+ routerMap.Pairs["songshodhon"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.songshodhon", args); err != nil {
+ return err
+ }
+ router.AddRoute("PATCH", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // matha (মাথা - HEAD)
+ routerMap.Pairs["matha"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.matha", args); err != nil {
+ return err
+ }
+ router.AddRoute("HEAD", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // nirdharon (নির্ধারণ - OPTIONS)
+ routerMap.Pairs["nirdharon"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if err := requireRoute("router.nirdharon", args); err != nil {
+ return err
+ }
+ router.AddRoute("OPTIONS", args[0].(*object.String).Value, args[1])
+ return routerMap
+ },
+ }
+
+ // majhe (মাঝে - middleware intercept - agorao = next)
+ routerMap.Pairs["majhe"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 1 {
+ return newError("router.majhe() takes exactly 1 argument (handler), got %d", len(args))
+ }
+ if args[0].Type() != object.FUNCTION_OBJ && args[0].Type() != object.BUILTIN_OBJ {
+ return newError("argument to router.majhe() must be FUNCTION, got %s", args[0].Type())
+ }
+ router.AddMiddleware(args[0])
+ return routerMap
+ },
+ }
+
+ // bebohar (ব্যবহার - mount sub-router) — FIXED: looks up actual sub-router
+ routerMap.Pairs["bebohar"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("router.bebohar() takes 2 arguments (mountPath, subRouter), got %d", len(args))
+ }
+ if args[0].Type() != object.STRING_OBJ {
+ return newError("first argument to router.bebohar() must be STRING (path), got %s", args[0].Type())
+ }
+ if args[1].Type() != object.MAP_OBJ {
+ return newError("second argument to router.bebohar() must be ROUTER, got %s", args[1].Type())
+ }
+ mountPath := args[0].(*object.String).Value
+ subRouterMap := args[1].(*object.Map)
+
+ ridObj, ok := subRouterMap.Pairs["__router_id__"]
+ if !ok {
+ return newError("invalid router object passed to bebohar()")
+ }
+ rid, ok := ridObj.(*object.String)
+ if !ok {
+ return newError("invalid router ID in bebohar()")
+ }
+ subRouter, found := getRouter(rid.Value)
+ if !found {
+ return newError("sub-router not found in registry")
+ }
+ router.MountSubRouter(mountPath, subRouter)
+ return routerMap
+ },
+ }
+
+ return routerMap
+ },
+ }
+}
+
+// requireRoute validates the 2-argument (path STRING, handler FUNCTION) signature
+// used by all HTTP method registrations.
+func requireRoute(name string, args []object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("wrong number of arguments to %s(). got=%d, want=2", name, len(args))
+ }
+ if args[0].Type() != object.STRING_OBJ {
+ return newError("first argument to %s() must be STRING (path), got %s", name, args[0].Type())
+ }
+ if args[1].Type() != object.FUNCTION_OBJ && args[1].Type() != object.BUILTIN_OBJ {
+ return newError("second argument to %s() must be FUNCTION (handler), got %s", name, args[1].Type())
+ }
+ return nil
+}
diff --git a/src/evaluator/builtins/builtins_http_utils.go b/src/evaluator/builtins/builtins_http_utils.go
new file mode 100644
index 0000000..dc3343c
--- /dev/null
+++ b/src/evaluator/builtins/builtins_http_utils.go
@@ -0,0 +1,244 @@
+package builtins
+
+import (
+ "BanglaCode/src/object"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+)
+
+func init() {
+ // cors_chharpao (ছাড়পাও - allow cross-origin requests)
+ // cors_chharpao(app)
+ // cors_chharpao(app, {"origin": "https://example.com", "methods": "GET,POST", "headers": "...", "maxAge": "86400"})
+ Builtins["cors_chharpao"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) < 1 || len(args) > 2 {
+ return newError("wrong number of arguments. got=%d, want=1-2 (app, [options])", len(args))
+ }
+ router, err := extractRouter("cors_chharpao", args[0])
+ if err != nil {
+ return err
+ }
+
+ opts := CORSOptions{
+ Origin: "*",
+ Methods: "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS",
+ Headers: "Content-Type,Authorization,X-Requested-With",
+ MaxAge: "86400",
+ }
+ if len(args) == 2 && args[1].Type() == object.MAP_OBJ {
+ m := args[1].(*object.Map)
+ if v, ok := m.Pairs["origin"].(*object.String); ok {
+ opts.Origin = v.Value
+ }
+ if v, ok := m.Pairs["methods"].(*object.String); ok {
+ opts.Methods = v.Value
+ }
+ if v, ok := m.Pairs["headers"].(*object.String); ok {
+ opts.Headers = v.Value
+ }
+ if v, ok := m.Pairs["maxAge"].(*object.String); ok {
+ opts.MaxAge = v.Value
+ }
+ }
+
+ router.mu.Lock()
+ router.corsEnabled = true
+ router.corsOptions = opts
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // file_dao (ফাইল দাও - serve static files from directory)
+ // file_dao(app, "/public", "./static_dir")
+ Builtins["file_dao"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 3 {
+ return newError("wrong number of arguments. got=%d, want=3 (app, urlPrefix, dirPath)", len(args))
+ }
+ router, err := extractRouter("file_dao", args[0])
+ if err != nil {
+ return err
+ }
+ if args[1].Type() != object.STRING_OBJ {
+ return newError("second argument to `file_dao` must be STRING (url prefix), got %s", args[1].Type())
+ }
+ if args[2].Type() != object.STRING_OBJ {
+ return newError("third argument to `file_dao` must be STRING (directory path), got %s", args[2].Type())
+ }
+
+ urlPrefix := args[1].(*object.String).Value
+ dirPath := args[2].(*object.String).Value
+
+ if !strings.HasPrefix(urlPrefix, "/") {
+ urlPrefix = "/" + urlPrefix
+ }
+ fileHandler := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dirPath)))
+
+ router.mu.Lock()
+ router.fileRoutes = append(router.fileRoutes, FileRoute{prefix: urlPrefix, handler: fileHandler})
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+
+ // ghurao (ঘোরাও - redirect to another URL)
+ // ghurao(res, "/login") → 302 Found
+ // ghurao(res, "/permanent", 301) → 301 Moved Permanently
+ Builtins["ghurao"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) < 2 || len(args) > 3 {
+ return newError("wrong number of arguments. got=%d, want=2-3 (res, url, [status])", len(args))
+ }
+ if args[0].Type() != object.MAP_OBJ {
+ return newError("first argument to `ghurao` must be response MAP, got %s", args[0].Type())
+ }
+ if args[1].Type() != object.STRING_OBJ {
+ return newError("second argument to `ghurao` must be STRING (url), got %s", args[1].Type())
+ }
+ resMap := args[0].(*object.Map)
+ redirectURL := args[1].(*object.String).Value
+ status := 302
+ if len(args) == 3 {
+ if args[2].Type() != object.NUMBER_OBJ {
+ return newError("third argument to `ghurao` must be NUMBER (status), got %s", args[2].Type())
+ }
+ status = int(args[2].(*object.Number).Value)
+ }
+ resMap.Pairs["status"] = &object.Number{Value: float64(status)}
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ h.Pairs["Location"] = &object.String{Value: redirectURL}
+ }
+ return resMap
+ },
+ }
+
+ // kuki_rakho (কুকি রাখো - set a cookie on the response)
+ // kuki_rakho(res, "name", "value")
+ // kuki_rakho(res, "name", "value", {"httpOnly": sotti, "secure": sotti, "maxAge": 3600, "path": "/", "sameSite": "Lax"})
+ Builtins["kuki_rakho"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) < 3 || len(args) > 4 {
+ return newError("wrong number of arguments. got=%d, want=3-4 (res, name, value, [opts])", len(args))
+ }
+ if args[0].Type() != object.MAP_OBJ {
+ return newError("first argument to `kuki_rakho` must be response MAP, got %s", args[0].Type())
+ }
+ if args[1].Type() != object.STRING_OBJ {
+ return newError("second argument to `kuki_rakho` must be STRING (name), got %s", args[1].Type())
+ }
+ if args[2].Type() != object.STRING_OBJ {
+ return newError("third argument to `kuki_rakho` must be STRING (value), got %s", args[2].Type())
+ }
+
+ resMap := args[0].(*object.Map)
+ name := args[1].(*object.String).Value
+ value := args[2].(*object.String).Value
+
+ cookieStr := fmt.Sprintf("%s=%s", name, value)
+
+ if len(args) == 4 && args[3].Type() == object.MAP_OBJ {
+ opts := args[3].(*object.Map)
+ if v, ok := opts.Pairs["path"].(*object.String); ok {
+ cookieStr += "; Path=" + v.Value
+ } else {
+ cookieStr += "; Path=/"
+ }
+ if v, ok := opts.Pairs["maxAge"].(*object.Number); ok {
+ cookieStr += fmt.Sprintf("; Max-Age=%d", int(v.Value))
+ }
+ if v, ok := opts.Pairs["sameSite"].(*object.String); ok {
+ cookieStr += "; SameSite=" + v.Value
+ }
+ if v, ok := opts.Pairs["httpOnly"].(*object.Boolean); ok && v.Value {
+ cookieStr += "; HttpOnly"
+ }
+ if v, ok := opts.Pairs["secure"].(*object.Boolean); ok && v.Value {
+ cookieStr += "; Secure"
+ }
+ } else {
+ cookieStr += "; Path=/"
+ }
+
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ // Append to existing Set-Cookie or set new one
+ existing, hasExisting := h.Pairs["Set-Cookie"].(*object.String)
+ if hasExisting && existing.Value != "" {
+ h.Pairs["Set-Cookie"] = &object.String{Value: existing.Value + "\r\nSet-Cookie: " + cookieStr}
+ } else {
+ h.Pairs["Set-Cookie"] = &object.String{Value: cookieStr}
+ }
+ }
+ return resMap
+ },
+ }
+
+ // html_uttor (HTML উত্তর - serve an HTML file as response)
+ // html_uttor(res, "./views/index.html")
+ Builtins["html_uttor"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 2 {
+ return newError("wrong number of arguments. got=%d, want=2 (res, filepath)", len(args))
+ }
+ if args[0].Type() != object.MAP_OBJ {
+ return newError("first argument to `html_uttor` must be response MAP, got %s", args[0].Type())
+ }
+ if args[1].Type() != object.STRING_OBJ {
+ return newError("second argument to `html_uttor` must be STRING (filepath), got %s", args[1].Type())
+ }
+ resMap := args[0].(*object.Map)
+ filepath := args[1].(*object.String).Value
+ content, err := os.ReadFile(filepath)
+ if err != nil {
+ return newError("html_uttor: could not read file '%s': %s", filepath, err.Error())
+ }
+ resMap.Pairs["body"] = &object.String{Value: string(content)}
+ if h, ok := resMap.Pairs["headers"].(*object.Map); ok {
+ h.Pairs["Content-Type"] = &object.String{Value: "text/html; charset=utf-8"}
+ }
+ return resMap
+ },
+ }
+
+ // log_chalu (লগ চালু - enable request logging on the router)
+ // log_chalu(app)
+ Builtins["log_chalu"] = &object.Builtin{
+ Fn: func(args ...object.Object) object.Object {
+ if len(args) != 1 {
+ return newError("wrong number of arguments. got=%d, want=1 (app)", len(args))
+ }
+ router, err := extractRouter("log_chalu", args[0])
+ if err != nil {
+ return err
+ }
+ router.mu.Lock()
+ router.logEnabled = true
+ router.mu.Unlock()
+ return args[0]
+ },
+ }
+}
+
+// extractRouter retrieves the *Router from a BanglaCode router map.
+func extractRouter(fn string, arg object.Object) (*Router, object.Object) {
+ if arg.Type() != object.MAP_OBJ {
+ return nil, newError("first argument to `%s` must be ROUTER (from router_banao()), got %s", fn, arg.Type())
+ }
+ routerMap := arg.(*object.Map)
+ ridObj, ok := routerMap.Pairs["__router_id__"]
+ if !ok {
+ return nil, newError("first argument to `%s` is not a valid router", fn)
+ }
+ rid, ok := ridObj.(*object.String)
+ if !ok {
+ return nil, newError("invalid router ID in `%s`", fn)
+ }
+ router, found := getRouter(rid.Value)
+ if !found {
+ return nil, newError("`%s`: router not found — was it created with router_banao()?", fn)
+ }
+ return router, nil
+}
diff --git a/src/repl/repl.go b/src/repl/repl.go
index 4f3dfc5..6c40b75 100644
--- a/src/repl/repl.go
+++ b/src/repl/repl.go
@@ -12,7 +12,7 @@ import (
"strings"
)
-const Version = "9.2.1"
+const Version = "9.3.0"
const PROMPT = "\033[1;33m>> \033[0m"
diff --git a/test/http_router_test.go b/test/http_router_test.go
index 413098b..60af274 100644
--- a/test/http_router_test.go
+++ b/test/http_router_test.go
@@ -269,3 +269,323 @@ router.mujhe_felo("/delete", kaj(req, res) { uttor(res, "deleted"); });
testEval(input)
}
}
+
+// ─── New feature tests ────────────────────────────────────────────────────────
+
+// TestRouterPathParams verifies single-segment path parameter registration.
+func TestRouterPathParams(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+app.ana("/users/:id", kaj(req, res) {
+ dhoro id = req["params"]["id"];
+ json_uttor(res, {"id": id});
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("path param route registration failed: %v", result.Inspect())
+ }
+ if result.Type() != object.MAP_OBJ {
+ t.Fatalf("expected MAP (router), got %s", result.Type())
+ }
+}
+
+// TestRouterPathParamsMultiple verifies multi-segment path parameters.
+func TestRouterPathParamsMultiple(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+app.ana("/posts/:pid/comments/:cid", kaj(req, res) {
+ dhoro pid = req["params"]["pid"];
+ dhoro cid = req["params"]["cid"];
+ json_uttor(res, {"pid": pid, "cid": cid});
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("multi-param route failed: %v", result.Inspect())
+ }
+}
+
+// TestQueryParamsParsedAsMap verifies that routes can access req["query"]["key"].
+func TestQueryParamsParsedAsMap(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+app.ana("/search", kaj(req, res) {
+ dhoro term = req["query"]["q"];
+ dhoro page = req["query"]["page"];
+ json_uttor(res, {"term": term, "page": page});
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("query param route failed: %v", result.Inspect())
+ }
+}
+
+// TestRouterMiddleware verifies app.majhe() accepts a 3-arg handler.
+func TestRouterMiddleware(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+app.majhe(kaj(req, res, agorao) {
+ dekho("middleware hit");
+ agorao();
+});
+app.ana("/", kaj(req, res) {
+ uttor(res, "OK");
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("middleware registration failed: %v", result.Inspect())
+ }
+}
+
+// TestRouterMiddlewareChaining verifies multiple middleware layers.
+func TestRouterMiddlewareChaining(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+app.majhe(kaj(req, res, agorao) { agorao(); });
+app.majhe(kaj(req, res, agorao) { agorao(); });
+app.majhe(kaj(req, res, agorao) { agorao(); });
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("chained middleware failed: %v", result.Inspect())
+ }
+}
+
+// TestSubRouterMountingFixed verifies bebohar() correctly mounts a sub-router.
+func TestSubRouterMountingFixed(t *testing.T) {
+ input := `
+dhoro api = router_banao();
+api.ana("/users", kaj(req, res) { json_uttor(res, {"users": []}); });
+api.pathano("/users", kaj(req, res) { json_uttor(res, {"created": sotti}, 201); });
+
+dhoro app = router_banao();
+app.bebohar("/api", api);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("bebohar sub-router mounting failed: %v", result.Inspect())
+ }
+}
+
+// TestCORSHelper verifies cors_chharpao() accepts default and custom options.
+func TestCORSHelper(t *testing.T) {
+ cases := []string{
+ `dhoro app = router_banao(); cors_chharpao(app); app`,
+ `dhoro app = router_banao(); cors_chharpao(app, {"origin": "https://example.com"}); app`,
+ `dhoro app = router_banao(); cors_chharpao(app, {"origin": "*", "methods": "GET,POST"}); app`,
+ }
+ for _, input := range cases {
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("cors_chharpao failed: %v", result.Inspect())
+ }
+ }
+}
+
+// TestStaticFilesHelper verifies file_dao() registers without error.
+func TestStaticFilesHelper(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+file_dao(app, "/public", ".");
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("file_dao failed: %v", result.Inspect())
+ }
+}
+
+// TestGhurao verifies redirect sets 302 status.
+func TestGhurao(t *testing.T) {
+ input := `
+dhoro res = {"status": 200, "body": "", "headers": {}};
+ghurao(res, "/login");
+res["status"]
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("ghurao failed: %v", result.Inspect())
+ }
+ if result.Type() != object.NUMBER_OBJ {
+ t.Fatalf("expected NUMBER status, got %s", result.Type())
+ }
+ if result.(*object.Number).Value != 302 {
+ t.Fatalf("expected status 302, got %v", result.(*object.Number).Value)
+ }
+}
+
+// TestGhuraoCustomStatus verifies redirect with explicit 301.
+func TestGhuraoCustomStatus(t *testing.T) {
+ input := `
+dhoro res = {"status": 200, "body": "", "headers": {}};
+ghurao(res, "/new-page", 301);
+res["status"]
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("ghurao 301 failed: %v", result.Inspect())
+ }
+ if result.(*object.Number).Value != 301 {
+ t.Fatalf("expected status 301, got %v", result.(*object.Number).Value)
+ }
+}
+
+// TestKukiRakho verifies kuki_rakho() sets a Set-Cookie header.
+func TestKukiRakho(t *testing.T) {
+ input := `
+dhoro res = {"status": 200, "body": "", "headers": {}};
+kuki_rakho(res, "token", "abc123");
+res["headers"]["Set-Cookie"]
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("kuki_rakho failed: %v", result.Inspect())
+ }
+ if result.Type() != object.STRING_OBJ {
+ t.Fatalf("expected STRING Set-Cookie, got %s", result.Type())
+ }
+ if result.(*object.String).Value == "" {
+ t.Fatal("Set-Cookie header should not be empty")
+ }
+}
+
+// TestKukiRakhoWithOptions verifies kuki_rakho() with all cookie attributes.
+func TestKukiRakhoWithOptions(t *testing.T) {
+ input := `
+dhoro res = {"status": 200, "body": "", "headers": {}};
+kuki_rakho(res, "session", "xyz789", {"httpOnly": sotti, "maxAge": 3600, "path": "/app", "sameSite": "Lax"});
+res["headers"]["Set-Cookie"]
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("kuki_rakho with options failed: %v", result.Inspect())
+ }
+ if result.Type() != object.STRING_OBJ {
+ t.Fatalf("expected STRING Set-Cookie, got %s", result.Type())
+ }
+}
+
+// TestGotiShima verifies goti_shima() configures the rate limiter without error.
+func TestGotiShima(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+goti_shima(app, 100, 60);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("goti_shima failed: %v", result.Inspect())
+ }
+}
+
+// TestSankochonChalu verifies sankochon_chalu() enables gzip without error.
+func TestSankochonChalu(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+sankochon_chalu(app);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("sankochon_chalu failed: %v", result.Inspect())
+ }
+}
+
+// TestSomoyShima verifies somoy_shima() sets the timeout without error.
+func TestSomoyShima(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+somoy_shima(app, 30);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("somoy_shima failed: %v", result.Inspect())
+ }
+}
+
+// TestAkaarShima verifies akaar_shima() sets the body limit without error.
+func TestAkaarShima(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+akaar_shima(app, 1048576);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("akaar_shima failed: %v", result.Inspect())
+ }
+}
+
+// TestBhulSambhalo verifies bhul_sambhalo() registers an error handler without error.
+func TestBhulSambhalo(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+bhul_sambhalo(app, kaj(err, req, res) {
+ json_uttor(res, {"error": "something went wrong"}, 500);
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("bhul_sambhalo failed: %v", result.Inspect())
+ }
+}
+
+// TestLogChalu verifies log_chalu() enables logging without error.
+func TestLogChalu(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+log_chalu(app);
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("log_chalu failed: %v", result.Inspect())
+ }
+}
+
+// TestFullProductionStack verifies all middleware helpers chain without error.
+func TestFullProductionStack(t *testing.T) {
+ input := `
+dhoro app = router_banao();
+cors_chharpao(app);
+log_chalu(app);
+sankochon_chalu(app);
+somoy_shima(app, 30);
+akaar_shima(app, 1048576);
+goti_shima(app, 100, 60);
+app.majhe(kaj(req, res, agorao) { agorao(); });
+file_dao(app, "/public", ".");
+app.ana("/users/:id", kaj(req, res) {
+ json_uttor(res, {"id": req["params"]["id"]});
+});
+app.pathano("/users", kaj(req, res) {
+ dhoro body = req["json"];
+ json_uttor(res, {"created": sotti}, 201);
+});
+app.ana("/search", kaj(req, res) {
+ json_uttor(res, {"q": req["query"]["q"]});
+});
+bhul_sambhalo(app, kaj(err, req, res) {
+ json_uttor(res, {"error": "internal"}, 500);
+});
+app
+`
+ result := testEval(input)
+ if isError(result) {
+ t.Fatalf("full production stack failed: %v", result.Inspect())
+ }
+ if result.Type() != object.MAP_OBJ {
+ t.Fatalf("expected MAP (router), got %s", result.Type())
+ }
+}