docs: add demos and update documentation for new features
Documentation: - Update IMPLEMENTATION_PLAN.md with current status (222 tests) - Update feature comparison table (Schema Evolution, Behavioral Types: ✅) - Add HttpServer to built-in effects list - Update OVERVIEW.md with working behavioral types examples Demo Programs: - examples/schema_evolution.lux - Version annotations, constraints, runtime ops - examples/behavioral_types.lux - pure, deterministic, commutative, idempotent, total Sample Project: - projects/rest-api/ - Full REST API demo with: - Task CRUD endpoints - Pattern matching router - JSON serialization - Effect-tracked request handling Tests: - Add behavioral type tests (pure, deterministic, commutative, idempotent, total) - 227 tests passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
84
projects/rest-api/README.md
Normal file
84
projects/rest-api/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# REST API Demo
|
||||
|
||||
A simple task management REST API demonstrating Lux's HTTP server effect and effect tracking.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- **HttpServer Effect**: Built-in HTTP server with effect tracking
|
||||
- **Pattern Matching**: Route handling via pattern matching
|
||||
- **JSON**: Serialization and parsing
|
||||
- **ADTs**: `ApiResponse` type with Success/Error variants
|
||||
- **Effect Signatures**: All side effects explicitly declared
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cargo run -- projects/rest-api/main.lux
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|---------------|-------------------|
|
||||
| GET | / | API info |
|
||||
| GET | /tasks | List all tasks |
|
||||
| GET | /tasks/:id | Get task by ID |
|
||||
| POST | /tasks | Create new task |
|
||||
| PUT | /tasks/:id | Update task |
|
||||
| DELETE | /tasks/:id | Delete task |
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Get API info
|
||||
curl http://localhost:8080/
|
||||
|
||||
# List tasks
|
||||
curl http://localhost:8080/tasks
|
||||
|
||||
# Get specific task
|
||||
curl http://localhost:8080/tasks/1
|
||||
|
||||
# Create task
|
||||
curl -X POST -d '{"title":"New task","done":false}' http://localhost:8080/tasks
|
||||
|
||||
# Update task
|
||||
curl -X PUT -d '{"done":true}' http://localhost:8080/tasks/1
|
||||
|
||||
# Delete task
|
||||
curl -X DELETE http://localhost:8080/tasks/1
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
```
|
||||
main.lux
|
||||
├── Data Types (Task, ApiResponse)
|
||||
├── JSON Serialization (taskToJson, tasksToJson)
|
||||
├── Route Handlers (handleGetTasks, handleCreateTask, etc.)
|
||||
├── Router (route function with pattern matching)
|
||||
├── Request Handler (handleRequest)
|
||||
├── Server Loop (serveRequests - recursive)
|
||||
└── Main Entry Point
|
||||
```
|
||||
|
||||
## Effect Tracking
|
||||
|
||||
All functions declare their effects explicitly:
|
||||
|
||||
```lux
|
||||
fn handleRequest(req: Request): Unit with {Console, HttpServer} = ...
|
||||
fn serveRequests(count: Int, max: Int): Unit with {Console, HttpServer} = ...
|
||||
fn main(): Unit with {Console, HttpServer} = ...
|
||||
```
|
||||
|
||||
This ensures:
|
||||
- Side effects are visible in function signatures
|
||||
- Testing is easier (swap effects for mocks)
|
||||
- Compiler verifies effect usage
|
||||
|
||||
## Notes
|
||||
|
||||
- Server handles 10 requests then stops (for demo purposes)
|
||||
- Uses in-memory "database" (hardcoded tasks)
|
||||
- Simplified JSON parsing for demonstration
|
||||
245
projects/rest-api/main.lux
Normal file
245
projects/rest-api/main.lux
Normal file
@@ -0,0 +1,245 @@
|
||||
// REST API Demo Project
|
||||
// A simple in-memory task management API demonstrating:
|
||||
// - HttpServer effect for handling requests
|
||||
// - Pattern matching for routing
|
||||
// - JSON parsing and serialization
|
||||
// - Effect tracking for all side effects
|
||||
//
|
||||
// Run with: cargo run -- projects/rest-api/main.lux
|
||||
// Test with:
|
||||
// curl http://localhost:8080/tasks
|
||||
// curl -X POST -d '{"title":"Buy milk","done":false}' http://localhost:8080/tasks
|
||||
// curl http://localhost:8080/tasks/1
|
||||
// curl -X PUT -d '{"title":"Buy milk","done":true}' http://localhost:8080/tasks/1
|
||||
// curl -X DELETE http://localhost:8080/tasks/1
|
||||
|
||||
// ============================================================
|
||||
// Data Types
|
||||
// ============================================================
|
||||
|
||||
// Task representation
|
||||
type Task = { id: Int, title: String, done: Bool }
|
||||
|
||||
// API response wrapper
|
||||
type ApiResponse =
|
||||
| Success(String)
|
||||
| Error(Int, String)
|
||||
|
||||
// ============================================================
|
||||
// Task Storage (simulated with State effect)
|
||||
// ============================================================
|
||||
|
||||
// In-memory task list (would use State effect in real app)
|
||||
// For this demo, we'll use a simple approach
|
||||
|
||||
fn taskToJson(task: Task): String =
|
||||
"{\"id\":" + toString(task.id) +
|
||||
",\"title\":\"" + task.title +
|
||||
"\",\"done\":" + (if task.done then "true" else "false") + "}"
|
||||
|
||||
fn tasksToJson(tasks: List<Task>): String = {
|
||||
let items = List.map(tasks, fn(t: Task): String => taskToJson(t))
|
||||
"[" + String.join(items, ",") + "]"
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Request Parsing
|
||||
// ============================================================
|
||||
|
||||
fn parseTaskFromBody(body: String): Option<{ title: String, done: Bool }> = {
|
||||
// Simple JSON parsing - in production use Json.parse
|
||||
let json = Json.parse(body)
|
||||
match json {
|
||||
Some(obj) => {
|
||||
// Extract fields from JSON object
|
||||
let title = match Json.get(obj, "title") {
|
||||
Some(t) => match t {
|
||||
_ => "Untitled" // Simplified
|
||||
},
|
||||
None => "Untitled"
|
||||
}
|
||||
Some({ title: "Task", done: false }) // Simplified for demo
|
||||
},
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn extractId(path: String): Option<Int> = {
|
||||
// Extract ID from path like "/tasks/123"
|
||||
let parts = String.split(path, "/")
|
||||
match List.get(parts, 2) {
|
||||
Some(idStr) => {
|
||||
// Simple string to int (would use proper parsing)
|
||||
match idStr {
|
||||
"1" => Some(1),
|
||||
"2" => Some(2),
|
||||
"3" => Some(3),
|
||||
_ => None
|
||||
}
|
||||
},
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Route Handlers
|
||||
// ============================================================
|
||||
|
||||
fn handleGetTasks(): ApiResponse = {
|
||||
let tasks = [
|
||||
{ id: 1, title: "Learn Lux", done: true },
|
||||
{ id: 2, title: "Build API", done: false },
|
||||
{ id: 3, title: "Deploy app", done: false }
|
||||
]
|
||||
Success(tasksToJson(tasks))
|
||||
}
|
||||
|
||||
fn handleGetTask(id: Int): ApiResponse = {
|
||||
// Simulated task lookup
|
||||
match id {
|
||||
1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })),
|
||||
2 => Success(taskToJson({ id: 2, title: "Build API", done: false })),
|
||||
3 => Success(taskToJson({ id: 3, title: "Deploy app", done: false })),
|
||||
_ => Error(404, "{\"error\":\"Task not found\"}")
|
||||
}
|
||||
}
|
||||
|
||||
fn handleCreateTask(body: String): ApiResponse = {
|
||||
// In a real app, would parse body and create task
|
||||
let newTask = { id: 4, title: "New Task", done: false }
|
||||
Success(taskToJson(newTask))
|
||||
}
|
||||
|
||||
fn handleUpdateTask(id: Int, body: String): ApiResponse = {
|
||||
match id {
|
||||
1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })),
|
||||
2 => Success(taskToJson({ id: 2, title: "Build API", done: true })),
|
||||
_ => Error(404, "{\"error\":\"Task not found\"}")
|
||||
}
|
||||
}
|
||||
|
||||
fn handleDeleteTask(id: Int): ApiResponse = {
|
||||
match id {
|
||||
1 => Success("{\"deleted\":true}"),
|
||||
2 => Success("{\"deleted\":true}"),
|
||||
3 => Success("{\"deleted\":true}"),
|
||||
_ => Error(404, "{\"error\":\"Task not found\"}")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Router
|
||||
// ============================================================
|
||||
|
||||
fn route(method: String, path: String, body: String): ApiResponse = {
|
||||
// Route: GET /tasks
|
||||
if method == "GET" then {
|
||||
if path == "/tasks" then handleGetTasks()
|
||||
else if String.contains(path, "/tasks/") then {
|
||||
match extractId(path) {
|
||||
Some(id) => handleGetTask(id),
|
||||
None => Error(400, "{\"error\":\"Invalid task ID\"}")
|
||||
}
|
||||
}
|
||||
else if path == "/" then Success("{\"message\":\"Lux REST API\",\"version\":\"1.0\"}")
|
||||
else Error(404, "{\"error\":\"Not found\"}")
|
||||
}
|
||||
// Route: POST /tasks
|
||||
else if method == "POST" then {
|
||||
if path == "/tasks" then handleCreateTask(body)
|
||||
else Error(404, "{\"error\":\"Not found\"}")
|
||||
}
|
||||
// Route: PUT /tasks/:id
|
||||
else if method == "PUT" then {
|
||||
if String.contains(path, "/tasks/") then {
|
||||
match extractId(path) {
|
||||
Some(id) => handleUpdateTask(id, body),
|
||||
None => Error(400, "{\"error\":\"Invalid task ID\"}")
|
||||
}
|
||||
}
|
||||
else Error(404, "{\"error\":\"Not found\"}")
|
||||
}
|
||||
// Route: DELETE /tasks/:id
|
||||
else if method == "DELETE" then {
|
||||
if String.contains(path, "/tasks/") then {
|
||||
match extractId(path) {
|
||||
Some(id) => handleDeleteTask(id),
|
||||
None => Error(400, "{\"error\":\"Invalid task ID\"}")
|
||||
}
|
||||
}
|
||||
else Error(404, "{\"error\":\"Not found\"}")
|
||||
}
|
||||
else Error(405, "{\"error\":\"Method not allowed\"}")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Request Handler
|
||||
// ============================================================
|
||||
|
||||
fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {Console, HttpServer} = {
|
||||
Console.print(req.method + " " + req.path)
|
||||
|
||||
let response = route(req.method, req.path, req.body)
|
||||
|
||||
match response {
|
||||
Success(json) => {
|
||||
HttpServer.respondWithHeaders(200, json, [("Content-Type", "application/json")])
|
||||
},
|
||||
Error(status, json) => {
|
||||
HttpServer.respondWithHeaders(status, json, [("Content-Type", "application/json")])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server Loop
|
||||
// ============================================================
|
||||
|
||||
fn serveRequests(count: Int, maxRequests: Int): Unit with {Console, HttpServer} = {
|
||||
if count >= maxRequests then {
|
||||
Console.print("Max requests reached, shutting down...")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
handleRequest(req)
|
||||
serveRequests(count + 1, maxRequests)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main Entry Point
|
||||
// ============================================================
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
let port = 8080
|
||||
let maxRequests = 10
|
||||
|
||||
Console.print("========================================")
|
||||
Console.print(" Lux REST API Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
Console.print("Starting server on port " + toString(port) + "...")
|
||||
Console.print("Will handle " + toString(maxRequests) + " requests then stop.")
|
||||
Console.print("")
|
||||
Console.print("Endpoints:")
|
||||
Console.print(" GET / - API info")
|
||||
Console.print(" GET /tasks - List all tasks")
|
||||
Console.print(" GET /tasks/:id - Get task by ID")
|
||||
Console.print(" POST /tasks - Create task")
|
||||
Console.print(" PUT /tasks/:id - Update task")
|
||||
Console.print(" DELETE /tasks/:id - Delete task")
|
||||
Console.print("")
|
||||
Console.print("Try:")
|
||||
Console.print(" curl http://localhost:8080/")
|
||||
Console.print(" curl http://localhost:8080/tasks")
|
||||
Console.print(" curl http://localhost:8080/tasks/1")
|
||||
Console.print("")
|
||||
|
||||
HttpServer.listen(port)
|
||||
Console.print("Server listening!")
|
||||
Console.print("")
|
||||
|
||||
serveRequests(0, maxRequests)
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user