feat: add example scripts for argparse, bytes, dataset, html, markdown, uuid, wdantic, and web chat modules

Add comprehensive demo scripts showcasing the usage of multiple Wren modules including argument parsing, byte manipulation, in-memory dataset operations, HTML encoding/slugification, markdown-to-HTML conversion, UUID generation/validation, schema validation with wdantic, and a full web chat application with REST endpoints.
This commit is contained in:
2026-01-24 22:40:22 +00:00
parent 0a65272f80
commit 5a4054ec66
105 changed files with 23275 additions and 94 deletions
+739
View File
@@ -0,0 +1,739 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Building a CLI Tool - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html">Tutorial List</a></li>
<li><a href="http-client.html">HTTP Client</a></li>
<li><a href="websocket-chat.html">WebSocket Chat</a></li>
<li><a href="database-app.html">Database App</a></li>
<li><a href="template-rendering.html">Template Rendering</a></li>
<li><a href="cli-tool.html" class="active">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">Tutorials</a>
<span class="separator">/</span>
<span>CLI Tool</span>
</nav>
<article>
<h1>Building a CLI Tool</h1>
<p>In this tutorial, you will build a complete command-line application. You will learn to parse arguments, handle user input, run subprocesses, and create a professional CLI experience.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Parsing command-line arguments</li>
<li>Handling user input interactively</li>
<li>Running external commands</li>
<li>Working with environment variables</li>
<li>Signal handling for graceful shutdown</li>
<li>Creating subcommands</li>
</ul>
<h2>Step 1: Accessing Command-Line Arguments</h2>
<p>Create a file called <code>cli_tool.wren</code>:</p>
<pre><code>import "os" for Process
var args = Process.arguments
System.print("Script: %(Process.arguments[0])")
System.print("Arguments: %(args.count - 1)")
for (i in 1...args.count) {
System.print(" arg[%(i)]: %(args[i])")
}</code></pre>
<p>Run it:</p>
<pre><code>$ wren_cli cli_tool.wren hello world --verbose
Script: cli_tool.wren
Arguments: 3
arg[1]: hello
arg[2]: world
arg[3]: --verbose</code></pre>
<h2>Step 2: Building an Argument Parser</h2>
<pre><code>import "os" for Process
class ArgParser {
construct new() {
_commands = {}
_options = {}
_flags = {}
_positional = []
}
parse(args) {
var i = 1
while (i < args.count) {
var arg = args[i]
if (arg.startsWith("--")) {
var key = arg[2..-1]
if (key.contains("=")) {
var parts = key.split("=")
_options[parts[0]] = parts[1]
} else if (i + 1 < args.count && !args[i + 1].startsWith("-")) {
_options[key] = args[i + 1]
i = i + 1
} else {
_flags[key] = true
}
} else if (arg.startsWith("-")) {
for (char in arg[1..-1]) {
_flags[char] = true
}
} else {
_positional.add(arg)
}
i = i + 1
}
}
option(name) { _options[name] }
flag(name) { _flags.containsKey(name) && _flags[name] }
positional { _positional }
hasOption(name) { _options.containsKey(name) }
}
var parser = ArgParser.new()
parser.parse(Process.arguments)
System.print("Positional: %(parser.positional)")
System.print("Verbose: %(parser.flag("verbose") || parser.flag("v"))")
System.print("Output: %(parser.option("output") || "stdout")")</code></pre>
<p>Run it:</p>
<pre><code>$ wren_cli cli_tool.wren file1.txt file2.txt -v --output=result.txt
Positional: [file1.txt, file2.txt]
Verbose: true
Output: result.txt</code></pre>
<h2>Step 3: Creating Subcommands</h2>
<pre><code>import "os" for Process
import "io" for File, Directory, Stdin
class CLI {
construct new(name, version) {
_name = name
_version = version
_commands = {}
}
command(name, description, handler) {
_commands[name] = {"description": description, "handler": handler}
}
run(args) {
if (args.count < 2) {
showHelp()
return
}
var cmd = args[1]
if (cmd == "--help" || cmd == "-h") {
showHelp()
} else if (cmd == "--version" || cmd == "-V") {
System.print("%(_name) %(_version)")
} else if (_commands.containsKey(cmd)) {
_commands[cmd]["handler"].call(args[2..-1])
} else {
System.print("Unknown command: %(cmd)")
System.print("Run '%(_name) --help' for usage.")
}
}
showHelp() {
System.print("%(_name) %(_version)")
System.print("")
System.print("Usage: %(_name) <command> [options]")
System.print("")
System.print("Commands:")
for (name in _commands.keys) {
var desc = _commands[name]["description"]
System.print(" %(name.padRight(15)) %(desc)")
}
System.print("")
System.print("Options:")
System.print(" --help, -h Show this help")
System.print(" --version, -V Show version")
}
}
var cli = CLI.new("mytool", "1.0.0")
cli.command("list", "List files in directory", Fn.new { |args|
var path = args.count > 0 ? args[0] : "."
var files = Directory.list(path)
for (file in files) {
System.print(file)
}
})
cli.command("read", "Read and display a file", Fn.new { |args|
if (args.count == 0) {
System.print("Usage: mytool read <file>")
return
}
System.print(File.read(args[0]))
})
cli.command("info", "Show file information", Fn.new { |args|
if (args.count == 0) {
System.print("Usage: mytool info <file>")
return
}
var path = args[0]
if (File.exists(path)) {
System.print("File: %(path)")
System.print("Size: %(File.size(path)) bytes")
} else {
System.print("File not found: %(path)")
}
})
cli.run(Process.arguments)</code></pre>
<h2>Step 4: Interactive Input</h2>
<pre><code>import "io" for Stdin
class Prompt {
static ask(question) {
System.write("%(question) ")
return Stdin.readLine()
}
static confirm(question) {
System.write("%(question) (y/n) ")
var answer = Stdin.readLine().lower
return answer == "y" || answer == "yes"
}
static choose(question, options) {
System.print(question)
var i = 1
for (opt in options) {
System.print(" %(i). %(opt)")
i = i + 1
}
System.write("Choice: ")
var choice = Num.fromString(Stdin.readLine())
if (choice && choice >= 1 && choice <= options.count) {
return options[choice - 1]
}
return null
}
static password(question) {
System.write("%(question) ")
return Stdin.readLine()
}
}
var name = Prompt.ask("What is your name?")
System.print("Hello, %(name)!")
if (Prompt.confirm("Do you want to continue?")) {
var color = Prompt.choose("Pick a color:", ["Red", "Green", "Blue"])
System.print("You chose: %(color)")
}</code></pre>
<h2>Step 5: Running External Commands</h2>
<pre><code>import "subprocess" for Subprocess
import "os" for Process
class Shell {
static run(command) {
var result = Subprocess.exec(command)
return {
"output": result.stdout,
"error": result.stderr,
"code": result.exitCode
}
}
static runOrFail(command) {
var result = run(command)
if (result["code"] != 0) {
Fiber.abort("Command failed: %(command)\n%(result["error"])")
}
return result["output"]
}
static which(program) {
var result = run("which %(program)")
return result["code"] == 0 ? result["output"].trim() : null
}
}
System.print("Git status:")
var result = Shell.run("git status --porcelain")
if (result["code"] == 0) {
if (result["output"].count == 0) {
System.print(" Working directory clean")
} else {
System.print(result["output"])
}
} else {
System.print(" Not a git repository")
}
var gitPath = Shell.which("git")
if (gitPath) {
System.print("Git found at: %(gitPath)")
}</code></pre>
<h2>Step 6: Environment Variables</h2>
<pre><code>import "env" for Env
import "os" for Process
class Config {
static get(key, defaultValue) {
return Env.get(key) || defaultValue
}
static require(key) {
var value = Env.get(key)
if (!value) {
Fiber.abort("Required environment variable not set: %(key)")
}
return value
}
}
var home = Config.get("HOME", "/tmp")
var editor = Config.get("EDITOR", "vim")
var debug = Config.get("DEBUG", "false") == "true"
System.print("Home: %(home)")
System.print("Editor: %(editor)")
System.print("Debug mode: %(debug)")
System.print("\nAll environment variables:")
for (key in Env.keys()) {
System.print(" %(key)=%(Env.get(key))")
}</code></pre>
<h2>Step 7: Signal Handling</h2>
<pre><code>import "signal" for Signal
import "timer" for Timer
var running = true
Signal.handle("SIGINT", Fn.new {
System.print("\nReceived SIGINT, shutting down...")
running = false
})
Signal.handle("SIGTERM", Fn.new {
System.print("\nReceived SIGTERM, shutting down...")
running = false
})
System.print("Running... Press Ctrl+C to stop")
var counter = 0
while (running) {
counter = counter + 1
System.write("\rProcessed %(counter) items...")
Timer.sleep(100)
}
System.print("\nGraceful shutdown complete. Processed %(counter) items.")</code></pre>
<h2>Step 8: Complete CLI Application</h2>
<p>Let's build a complete file utility tool:</p>
<pre><code>import "os" for Process
import "io" for File, Directory, Stdin
import "json" for Json
import "crypto" for Crypto
import "subprocess" for Subprocess
import "signal" for Signal
import "env" for Env
class FileUtil {
construct new() {
_verbose = false
}
verbose=(value) { _verbose = value }
log(message) {
if (_verbose) System.print("[INFO] %(message)")
}
list(path, recursive) {
var files = Directory.list(path)
var results = []
for (file in files) {
var fullPath = "%(path)/%(file)"
results.add(fullPath)
if (recursive && Directory.exists(fullPath)) {
var subFiles = list(fullPath, true)
for (sub in subFiles) {
results.add(sub)
}
}
}
return results
}
copy(source, dest) {
log("Copying %(source) to %(dest)")
File.copy(source, dest)
}
hash(path, algorithm) {
var content = File.read(path)
if (algorithm == "md5") {
return Crypto.md5(content)
} else if (algorithm == "sha256") {
return Crypto.sha256(content)
}
return null
}
search(path, pattern) {
var files = list(path, true)
var matches = []
for (file in files) {
if (file.contains(pattern)) {
matches.add(file)
}
}
return matches
}
stats(path) {
var files = list(path, true)
var totalSize = 0
var fileCount = 0
var dirCount = 0
for (file in files) {
if (Directory.exists(file)) {
dirCount = dirCount + 1
} else if (File.exists(file)) {
fileCount = fileCount + 1
totalSize = totalSize + File.size(file)
}
}
return {
"files": fileCount,
"directories": dirCount,
"totalSize": totalSize
}
}
}
class CLI {
construct new() {
_name = "fileutil"
_version = "1.0.0"
_util = FileUtil.new()
}
run(args) {
if (args.count < 2) {
showHelp()
return
}
var verbose = args.contains("-v") || args.contains("--verbose")
_util.verbose = verbose
var cmd = args[1]
if (cmd == "list" || cmd == "ls") {
cmdList(args)
} else if (cmd == "copy" || cmd == "cp") {
cmdCopy(args)
} else if (cmd == "hash") {
cmdHash(args)
} else if (cmd == "search" || cmd == "find") {
cmdSearch(args)
} else if (cmd == "stats") {
cmdStats(args)
} else if (cmd == "--help" || cmd == "-h") {
showHelp()
} else if (cmd == "--version" || cmd == "-V") {
System.print("%(_name) %(_version)")
} else {
System.print("Unknown command: %(cmd)")
System.print("Run '%(_name) --help' for usage.")
}
}
cmdList(args) {
var path = "."
var recursive = args.contains("-r") || args.contains("--recursive")
for (arg in args[2..-1]) {
if (!arg.startsWith("-")) {
path = arg
break
}
}
var files = _util.list(path, recursive)
for (file in files) {
if (File.exists(file)) {
var size = File.size(file)
System.print("%(formatSize(size).padLeft(10)) %(file)")
} else {
System.print(" DIR %(file)/")
}
}
System.print("\n%(files.count) items")
}
cmdCopy(args) {
if (args.count < 4) {
System.print("Usage: %(_name) copy <source> <dest>")
return
}
var source = args[2]
var dest = args[3]
if (!File.exists(source)) {
System.print("Source file not found: %(source)")
return
}
_util.copy(source, dest)
System.print("Copied %(source) to %(dest)")
}
cmdHash(args) {
if (args.count < 3) {
System.print("Usage: %(_name) hash <file> [--algorithm=sha256|md5]")
return
}
var file = args[2]
var algorithm = "sha256"
for (arg in args) {
if (arg.startsWith("--algorithm=")) {
algorithm = arg[12..-1]
}
}
if (!File.exists(file)) {
System.print("File not found: %(file)")
return
}
var hash = _util.hash(file, algorithm)
System.print("%(algorithm.upper): %(hash)")
}
cmdSearch(args) {
if (args.count < 3) {
System.print("Usage: %(_name) search <pattern> [path]")
return
}
var pattern = args[2]
var path = args.count > 3 ? args[3] : "."
var matches = _util.search(path, pattern)
if (matches.count == 0) {
System.print("No matches found for '%(pattern)'")
} else {
System.print("Found %(matches.count) matches:")
for (match in matches) {
System.print(" %(match)")
}
}
}
cmdStats(args) {
var path = args.count > 2 ? args[2] : "."
var stats = _util.stats(path)
System.print("Statistics for %(path):")
System.print(" Files: %(stats["files"])")
System.print(" Directories: %(stats["directories"])")
System.print(" Total size: %(formatSize(stats["totalSize"]))")
}
formatSize(bytes) {
if (bytes < 1024) return "%(bytes) B"
if (bytes < 1024 * 1024) return "%(((bytes / 1024) * 10).round / 10) KB"
if (bytes < 1024 * 1024 * 1024) return "%(((bytes / 1024 / 1024) * 10).round / 10) MB"
return "%(((bytes / 1024 / 1024 / 1024) * 10).round / 10) GB"
}
showHelp() {
System.print("%(_name) %(_version) - File utility tool")
System.print("")
System.print("Usage: %(_name) <command> [options] [arguments]")
System.print("")
System.print("Commands:")
System.print(" list, ls List files in directory")
System.print(" -r, --recursive Include subdirectories")
System.print("")
System.print(" copy, cp Copy a file")
System.print(" <source> <dest>")
System.print("")
System.print(" hash Calculate file hash")
System.print(" --algorithm=sha256|md5")
System.print("")
System.print(" search Search for files by name")
System.print(" <pattern> [path]")
System.print("")
System.print(" stats Show directory statistics")
System.print(" [path]")
System.print("")
System.print("Global Options:")
System.print(" -v, --verbose Verbose output")
System.print(" -h, --help Show this help")
System.print(" -V, --version Show version")
}
}
var cli = CLI.new()
cli.run(Process.arguments)</code></pre>
<h2>Running the Tool</h2>
<pre><code>$ wren_cli fileutil.wren --help
fileutil 1.0.0 - File utility tool
Usage: fileutil &lt;command&gt; [options] [arguments]
Commands:
list, ls List files in directory
...
$ wren_cli fileutil.wren list -r src/
1.2 KB src/main.wren
3.4 KB src/utils.wren
DIR src/lib/
2.1 KB src/lib/helper.wren
4 items
$ wren_cli fileutil.wren hash README.md
SHA256: a3f2e8b9c4d5...</code></pre>
<div class="admonition tip">
<div class="admonition-title">Tip</div>
<p>Create a shell alias or wrapper script to run your Wren CLI tools more conveniently: <code>alias fileutil='wren_cli /path/to/fileutil.wren'</code></p>
</div>
<h2>Next Steps</h2>
<ul>
<li>Add configuration file support with <a href="../api/json.html">JSON</a></li>
<li>Implement colored output for better UX</li>
<li>Add tab completion hints</li>
<li>See the <a href="../api/os.html">OS</a> and <a href="../api/subprocess.html">Subprocess</a> API references</li>
</ul>
</article>
<footer class="page-footer">
<a href="template-rendering.html" class="prev">Template Rendering</a>
<a href="../howto/index.html" class="next">How-To Guides</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>
+722
View File
@@ -0,0 +1,722 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Application - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html">Tutorial List</a></li>
<li><a href="http-client.html">HTTP Client</a></li>
<li><a href="websocket-chat.html">WebSocket Chat</a></li>
<li><a href="database-app.html" class="active">Database App</a></li>
<li><a href="template-rendering.html">Template Rendering</a></li>
<li><a href="cli-tool.html">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">Tutorials</a>
<span class="separator">/</span>
<span>Database App</span>
</nav>
<article>
<h1>Database Application</h1>
<p>In this tutorial, you will build a complete task management application using SQLite for persistent storage. You will learn to create tables, perform CRUD operations, and build a command-line interface.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Creating and managing SQLite databases</li>
<li>Designing database schemas</li>
<li>Performing CRUD operations (Create, Read, Update, Delete)</li>
<li>Using parameterized queries to prevent SQL injection</li>
<li>Building a command-line interface</li>
</ul>
<h2>Step 1: Setting Up the Database</h2>
<p>Create a file called <code>task_app.wren</code>:</p>
<pre><code>import "sqlite" for Sqlite
var db = Sqlite.open("tasks.db")
db.execute("
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
due_date DATE
)
")
System.print("Database initialized!")
db.close()</code></pre>
<h2>Step 2: Creating a Task Model</h2>
<p>Let's create a class to manage task operations:</p>
<pre><code>import "sqlite" for Sqlite
import "datetime" for DateTime
class TaskManager {
construct new(dbPath) {
_db = Sqlite.open(dbPath)
initDatabase()
}
initDatabase() {
_db.execute("
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
due_date DATE
)
")
}
create(title, description, priority, dueDate) {
_db.execute(
"INSERT INTO tasks (title, description, priority, due_date) VALUES (?, ?, ?, ?)",
[title, description, priority, dueDate]
)
return _db.lastInsertId
}
findAll() {
return _db.execute("SELECT * FROM tasks ORDER BY priority DESC, due_date ASC")
}
findById(id) {
var results = _db.execute("SELECT * FROM tasks WHERE id = ?", [id])
return results.count > 0 ? results[0] : null
}
findPending() {
return _db.execute("SELECT * FROM tasks WHERE completed = 0 ORDER BY priority DESC")
}
findCompleted() {
return _db.execute("SELECT * FROM tasks WHERE completed = 1 ORDER BY created_at DESC")
}
update(id, title, description, priority, dueDate) {
_db.execute(
"UPDATE tasks SET title = ?, description = ?, priority = ?, due_date = ? WHERE id = ?",
[title, description, priority, dueDate, id]
)
}
complete(id) {
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
}
uncomplete(id) {
_db.execute("UPDATE tasks SET completed = 0 WHERE id = ?", [id])
}
delete(id) {
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
}
search(query) {
return _db.execute(
"SELECT * FROM tasks WHERE title LIKE ? OR description LIKE ?",
["\%%(query)\%", "\%%(query)\%"]
)
}
close() {
_db.close()
}
}
var tasks = TaskManager.new("tasks.db")
var id = tasks.create("Learn Wren-CLI", "Complete the database tutorial", 3, "2024-12-31")
System.print("Created task with ID: %(id)")
tasks.close()</code></pre>
<h2>Step 3: Building the CLI Interface</h2>
<p>Now let's create an interactive command-line interface:</p>
<pre><code>import "sqlite" for Sqlite
import "io" for Stdin
class TaskManager {
construct new(dbPath) {
_db = Sqlite.open(dbPath)
initDatabase()
}
initDatabase() {
_db.execute("
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
")
}
create(title, description, priority) {
_db.execute(
"INSERT INTO tasks (title, description, priority) VALUES (?, ?, ?)",
[title, description, priority]
)
return _db.lastInsertId
}
findAll() {
return _db.execute("SELECT * FROM tasks ORDER BY completed ASC, priority DESC")
}
complete(id) {
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
}
delete(id) {
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
}
close() {
_db.close()
}
}
class TaskApp {
construct new() {
_tasks = TaskManager.new("tasks.db")
_running = true
}
run() {
System.print("=== Task Manager ===\n")
showHelp()
while (_running) {
System.write("\n> ")
var input = Stdin.readLine()
if (input == null) break
var parts = input.split(" ")
var command = parts.count > 0 ? parts[0] : ""
if (command == "list") {
listTasks()
} else if (command == "add") {
addTask()
} else if (command == "done") {
if (parts.count > 1) {
completeTask(Num.fromString(parts[1]))
} else {
System.print("Usage: done <id>")
}
} else if (command == "delete") {
if (parts.count > 1) {
deleteTask(Num.fromString(parts[1]))
} else {
System.print("Usage: delete <id>")
}
} else if (command == "help") {
showHelp()
} else if (command == "quit" || command == "exit") {
_running = false
} else if (command.count > 0) {
System.print("Unknown command: %(command)")
}
}
_tasks.close()
System.print("Goodbye!")
}
showHelp() {
System.print("Commands:")
System.print(" list - Show all tasks")
System.print(" add - Add a new task")
System.print(" done <id> - Mark task as complete")
System.print(" delete <id> - Delete a task")
System.print(" help - Show this help")
System.print(" quit - Exit the application")
}
listTasks() {
var tasks = _tasks.findAll()
if (tasks.count == 0) {
System.print("No tasks found.")
return
}
System.print("\nID | Pri | Status | Title")
System.print("----|-----|--------|------")
for (task in tasks) {
var status = task["completed"] == 1 ? "[X]" : "[ ]"
var priority = ["Low", "Med", "High"][task["priority"] - 1]
System.print("%(task["id"]) | %(priority) | %(status) | %(task["title"])")
}
}
addTask() {
System.write("Title: ")
var title = Stdin.readLine()
System.write("Description (optional): ")
var description = Stdin.readLine()
System.write("Priority (1=Low, 2=Medium, 3=High): ")
var priorityStr = Stdin.readLine()
var priority = Num.fromString(priorityStr) || 1
if (priority < 1) priority = 1
if (priority > 3) priority = 3
var id = _tasks.create(title, description, priority)
System.print("Task created with ID: %(id)")
}
completeTask(id) {
_tasks.complete(id)
System.print("Task %(id) marked as complete.")
}
deleteTask(id) {
_tasks.delete(id)
System.print("Task %(id) deleted.")
}
}
var app = TaskApp.new()
app.run()</code></pre>
<h2>Step 4: Adding Advanced Features</h2>
<p>Let's enhance our application with categories and due dates:</p>
<pre><code>import "sqlite" for Sqlite
import "io" for Stdin
import "datetime" for DateTime
class TaskManager {
construct new(dbPath) {
_db = Sqlite.open(dbPath)
initDatabase()
}
initDatabase() {
_db.execute("
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#808080'
)
")
_db.execute("
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
category_id INTEGER,
priority INTEGER DEFAULT 1,
completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
due_date DATE,
FOREIGN KEY (category_id) REFERENCES categories(id)
)
")
var categories = _db.execute("SELECT COUNT(*) as count FROM categories")
if (categories[0]["count"] == 0) {
_db.execute("INSERT INTO categories (name, color) VALUES ('Work', '#FF6B6B')")
_db.execute("INSERT INTO categories (name, color) VALUES ('Personal', '#4ECDC4')")
_db.execute("INSERT INTO categories (name, color) VALUES ('Shopping', '#45B7D1')")
}
}
createTask(title, description, categoryId, priority, dueDate) {
_db.execute(
"INSERT INTO tasks (title, description, category_id, priority, due_date) VALUES (?, ?, ?, ?, ?)",
[title, description, categoryId, priority, dueDate]
)
return _db.lastInsertId
}
findAllTasks() {
return _db.execute("
SELECT t.*, c.name as category_name
FROM tasks t
LEFT JOIN categories c ON t.category_id = c.id
ORDER BY t.completed ASC, t.priority DESC, t.due_date ASC
")
}
findTasksByCategory(categoryId) {
return _db.execute("
SELECT t.*, c.name as category_name
FROM tasks t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.category_id = ?
ORDER BY t.completed ASC, t.priority DESC
", [categoryId])
}
findOverdueTasks() {
return _db.execute("
SELECT t.*, c.name as category_name
FROM tasks t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.completed = 0 AND t.due_date < DATE('now')
ORDER BY t.due_date ASC
")
}
findDueToday() {
return _db.execute("
SELECT t.*, c.name as category_name
FROM tasks t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.completed = 0 AND t.due_date = DATE('now')
ORDER BY t.priority DESC
")
}
findCategories() {
return _db.execute("SELECT * FROM categories ORDER BY name")
}
createCategory(name, color) {
_db.execute("INSERT INTO categories (name, color) VALUES (?, ?)", [name, color])
return _db.lastInsertId
}
completeTask(id) {
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
}
deleteTask(id) {
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
}
getStats() {
var total = _db.execute("SELECT COUNT(*) as count FROM tasks")[0]["count"]
var completed = _db.execute("SELECT COUNT(*) as count FROM tasks WHERE completed = 1")[0]["count"]
var pending = total - completed
var overdue = _db.execute("SELECT COUNT(*) as count FROM tasks WHERE completed = 0 AND due_date < DATE('now')")[0]["count"]
return {
"total": total,
"completed": completed,
"pending": pending,
"overdue": overdue
}
}
close() {
_db.close()
}
}
class TaskApp {
construct new() {
_tasks = TaskManager.new("tasks.db")
_running = true
}
run() {
System.print("=== Task Manager v2 ===\n")
while (_running) {
showMenu()
System.write("\nChoice: ")
var choice = Stdin.readLine()
if (choice == "1") {
listTasks()
} else if (choice == "2") {
addTask()
} else if (choice == "3") {
completeTask()
} else if (choice == "4") {
showOverdue()
} else if (choice == "5") {
showStats()
} else if (choice == "6") {
manageCategories()
} else if (choice == "0") {
_running = false
}
}
_tasks.close()
System.print("\nGoodbye!")
}
showMenu() {
System.print("\n--- Main Menu ---")
System.print("1. List all tasks")
System.print("2. Add new task")
System.print("3. Complete task")
System.print("4. Show overdue")
System.print("5. Statistics")
System.print("6. Categories")
System.print("0. Exit")
}
listTasks() {
var tasks = _tasks.findAllTasks()
if (tasks.count == 0) {
System.print("\nNo tasks found.")
return
}
System.print("\n--- All Tasks ---")
for (task in tasks) {
var status = task["completed"] == 1 ? "[X]" : "[ ]"
var category = task["category_name"] || "None"
var due = task["due_date"] || "No due date"
System.print("%(task["id"]). %(status) %(task["title"])")
System.print(" Category: %(category) | Due: %(due)")
}
}
addTask() {
System.print("\n--- Add Task ---")
System.write("Title: ")
var title = Stdin.readLine()
if (title.count == 0) return
System.write("Description: ")
var description = Stdin.readLine()
var categories = _tasks.findCategories()
System.print("\nCategories:")
for (cat in categories) {
System.print(" %(cat["id"]). %(cat["name"])")
}
System.write("Category ID (or 0 for none): ")
var categoryId = Num.fromString(Stdin.readLine())
if (categoryId == 0) categoryId = null
System.write("Priority (1-3): ")
var priority = Num.fromString(Stdin.readLine()) || 1
System.write("Due date (YYYY-MM-DD or empty): ")
var dueDate = Stdin.readLine()
if (dueDate.count == 0) dueDate = null
var id = _tasks.createTask(title, description, categoryId, priority, dueDate)
System.print("\nTask created with ID: %(id)")
}
completeTask() {
System.write("\nTask ID to complete: ")
var id = Num.fromString(Stdin.readLine())
if (id) {
_tasks.completeTask(id)
System.print("Task %(id) marked as complete!")
}
}
showOverdue() {
var tasks = _tasks.findOverdueTasks()
if (tasks.count == 0) {
System.print("\nNo overdue tasks!")
return
}
System.print("\n--- Overdue Tasks ---")
for (task in tasks) {
System.print("%(task["id"]). %(task["title"]) (Due: %(task["due_date"]))")
}
}
showStats() {
var stats = _tasks.getStats()
System.print("\n--- Statistics ---")
System.print("Total tasks: %(stats["total"])")
System.print("Completed: %(stats["completed"])")
System.print("Pending: %(stats["pending"])")
System.print("Overdue: %(stats["overdue"])")
if (stats["total"] > 0) {
var pct = (stats["completed"] / stats["total"] * 100).round
System.print("Completion rate: %(pct)\%")
}
}
manageCategories() {
var categories = _tasks.findCategories()
System.print("\n--- Categories ---")
for (cat in categories) {
System.print("%(cat["id"]). %(cat["name"])")
}
System.write("\nAdd new category? (y/n): ")
if (Stdin.readLine().lower == "y") {
System.write("Category name: ")
var name = Stdin.readLine()
if (name.count > 0) {
_tasks.createCategory(name, "#808080")
System.print("Category created!")
}
}
}
}
var app = TaskApp.new()
app.run()</code></pre>
<h2>Running the Application</h2>
<pre><code>$ wren_cli task_app.wren
=== Task Manager v2 ===
--- Main Menu ---
1. List all tasks
2. Add new task
3. Complete task
4. Show overdue
5. Statistics
6. Categories
0. Exit
Choice: 2
--- Add Task ---
Title: Complete database tutorial
Description: Learn SQLite with Wren-CLI
Categories:
1. Work
2. Personal
3. Shopping
Category ID (or 0 for none): 1
Priority (1-3): 3
Due date (YYYY-MM-DD or empty): 2024-12-31
Task created with ID: 1</code></pre>
<div class="admonition tip">
<div class="admonition-title">Tip</div>
<p>Always use parameterized queries with <code>?</code> placeholders instead of string concatenation. This prevents SQL injection vulnerabilities.</p>
</div>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Use <code>":memory:"</code> as the database path for testing without creating a file on disk.</p>
</div>
<h2>Next Steps</h2>
<ul>
<li>Add data export to JSON or CSV</li>
<li>Implement task reminders with <a href="../api/timer.html">Timer</a></li>
<li>Generate HTML reports with <a href="template-rendering.html">Jinja templates</a></li>
<li>See the <a href="../api/sqlite.html">SQLite API reference</a></li>
</ul>
</article>
<footer class="page-footer">
<a href="websocket-chat.html" class="prev">WebSocket Chat</a>
<a href="template-rendering.html" class="next">Template Rendering</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>
+527
View File
@@ -0,0 +1,527 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Building an HTTP Client - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html">Tutorial List</a></li>
<li><a href="http-client.html" class="active">HTTP Client</a></li>
<li><a href="websocket-chat.html">WebSocket Chat</a></li>
<li><a href="database-app.html">Database App</a></li>
<li><a href="template-rendering.html">Template Rendering</a></li>
<li><a href="cli-tool.html">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">Tutorials</a>
<span class="separator">/</span>
<span>HTTP Client</span>
</nav>
<article>
<h1>Building an HTTP Client</h1>
<p>In this tutorial, you will learn how to build a REST API client using Wren-CLI's <code>http</code> and <code>json</code> modules. By the end, you will have a reusable API client class that can interact with any JSON REST API.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Making GET, POST, PUT, and DELETE requests</li>
<li>Parsing JSON responses</li>
<li>Handling HTTP errors</li>
<li>Working with request headers</li>
<li>Building a reusable API client class</li>
</ul>
<h2>Step 1: Your First HTTP Request</h2>
<p>Let's start with a simple GET request. Create a file called <code>http_client.wren</code>:</p>
<pre><code>import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
System.print("Status: %(response.statusCode)")
System.print("Body: %(response.body)")</code></pre>
<p>Run it:</p>
<pre><code>$ wren_cli http_client.wren
Status: 200
Body: {
"userId": 1,
"id": 1,
"title": "sunt aut facere...",
"body": "quia et suscipit..."
}</code></pre>
<h2>Step 2: Parsing JSON Responses</h2>
<p>Raw JSON strings are not very useful. Let's parse them into Wren objects:</p>
<pre><code>import "http" for Http
import "json" for Json
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
if (response.statusCode == 200) {
var post = Json.parse(response.body)
System.print("Title: %(post["title"])")
System.print("User ID: %(post["userId"])")
} else {
System.print("Error: %(response.statusCode)")
}</code></pre>
<p>The <code>HttpResponse</code> class also provides a convenient <code>json</code> property:</p>
<pre><code>import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts/1")
var post = response.json
System.print("Title: %(post["title"])")</code></pre>
<h2>Step 3: Fetching Lists</h2>
<p>APIs often return arrays of objects. Let's fetch multiple posts:</p>
<pre><code>import "http" for Http
var response = Http.get("https://jsonplaceholder.typicode.com/posts")
var posts = response.json
System.print("Found %(posts.count) posts\n")
for (i in 0...5) {
var post = posts[i]
System.print("%(post["id"]). %(post["title"])")
}</code></pre>
<h2>Step 4: Making POST Requests</h2>
<p>To create resources, use POST with a JSON body:</p>
<pre><code>import "http" for Http
import "json" for Json
var newPost = {
"title": "My New Post",
"body": "This is the content of my post.",
"userId": 1
}
var response = Http.post(
"https://jsonplaceholder.typicode.com/posts",
Json.stringify(newPost),
{"Content-Type": "application/json"}
)
System.print("Status: %(response.statusCode)")
System.print("Created post with ID: %(response.json["id"])")</code></pre>
<h2>Step 5: PUT and DELETE Requests</h2>
<p>Update and delete resources with PUT and DELETE:</p>
<pre><code>import "http" for Http
import "json" for Json
var updatedPost = {
"id": 1,
"title": "Updated Title",
"body": "Updated content.",
"userId": 1
}
var putResponse = Http.put(
"https://jsonplaceholder.typicode.com/posts/1",
Json.stringify(updatedPost),
{"Content-Type": "application/json"}
)
System.print("PUT Status: %(putResponse.statusCode)")
var deleteResponse = Http.delete("https://jsonplaceholder.typicode.com/posts/1")
System.print("DELETE Status: %(deleteResponse.statusCode)")</code></pre>
<h2>Step 6: Building an API Client Class</h2>
<p>Let's create a reusable API client that encapsulates all these patterns:</p>
<pre><code>import "http" for Http
import "json" for Json
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
headers { _headers }
headers=(value) { _headers = value }
setHeader(name, value) {
_headers[name] = value
}
get(path) {
var response = Http.get(_baseUrl + path, _headers)
return handleResponse(response)
}
post(path, data) {
var body = Json.stringify(data)
var response = Http.post(_baseUrl + path, body, _headers)
return handleResponse(response)
}
put(path, data) {
var body = Json.stringify(data)
var response = Http.put(_baseUrl + path, body, _headers)
return handleResponse(response)
}
delete(path) {
var response = Http.delete(_baseUrl + path, _headers)
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.count > 0) {
return response.json
}
return null
}
Fiber.abort("API Error: %(response.statusCode)")
}
}
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
var posts = api.get("/posts")
System.print("Fetched %(posts.count) posts")
var newPost = api.post("/posts", {
"title": "Hello from Wren",
"body": "Created with ApiClient",
"userId": 1
})
System.print("Created post: %(newPost["id"])")</code></pre>
<h2>Step 7: Adding Authentication</h2>
<p>Many APIs require authentication. Add support for API keys and bearer tokens:</p>
<pre><code>import "http" for Http
import "json" for Json
import "base64" for Base64
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
setApiKey(key) {
_headers["X-API-Key"] = key
}
setBearerToken(token) {
_headers["Authorization"] = "Bearer %(token)"
}
setBasicAuth(username, password) {
var credentials = Base64.encode("%(username):%(password)")
_headers["Authorization"] = "Basic %(credentials)"
}
get(path) {
var response = Http.get(_baseUrl + path, _headers)
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode == 401) {
Fiber.abort("Authentication failed")
}
if (response.statusCode == 403) {
Fiber.abort("Access forbidden")
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.json
}
Fiber.abort("API Error: %(response.statusCode)")
}
}
var api = ApiClient.new("https://api.example.com")
api.setBearerToken("your-auth-token")
var data = api.get("/protected/resource")</code></pre>
<h2>Step 8: Error Handling</h2>
<p>Proper error handling makes your client robust:</p>
<pre><code>import "http" for Http
import "json" for Json
class ApiError {
construct new(statusCode, message) {
_statusCode = statusCode
_message = message
}
statusCode { _statusCode }
message { _message }
toString { "ApiError %(statusCode): %(message)" }
}
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
}
get(path) {
var fiber = Fiber.new {
return Http.get(_baseUrl + path, _headers)
}
var response = fiber.try()
if (fiber.error) {
return {"error": ApiError.new(0, fiber.error)}
}
return handleResponse(response)
}
handleResponse(response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return {"data": response.json}
}
var message = "Unknown error"
var fiber = Fiber.new { response.json["message"] }
var apiMessage = fiber.try()
if (!fiber.error && apiMessage) {
message = apiMessage
}
return {"error": ApiError.new(response.statusCode, message)}
}
}
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
var result = api.get("/posts/1")
if (result["error"]) {
System.print("Error: %(result["error"])")
} else {
System.print("Success: %(result["data"]["title"])")
}</code></pre>
<h2>Complete Example</h2>
<p>Here is a complete, production-ready API client:</p>
<pre><code>import "http" for Http
import "json" for Json
import "base64" for Base64
class ApiClient {
construct new(baseUrl) {
_baseUrl = baseUrl
_headers = {"Content-Type": "application/json"}
_timeout = 30000
}
baseUrl { _baseUrl }
headers { _headers }
setHeader(name, value) { _headers[name] = value }
removeHeader(name) { _headers.remove(name) }
setBearerToken(token) {
_headers["Authorization"] = "Bearer %(token)"
}
setBasicAuth(username, password) {
var credentials = Base64.encode("%(username):%(password)")
_headers["Authorization"] = "Basic %(credentials)"
}
get(path) { request("GET", path, null) }
post(path, data) { request("POST", path, data) }
put(path, data) { request("PUT", path, data) }
patch(path, data) { request("PATCH", path, data) }
delete(path) { request("DELETE", path, null) }
request(method, path, data) {
var url = _baseUrl + path
var body = data ? Json.stringify(data) : ""
var response
if (method == "GET") {
response = Http.get(url, _headers)
} else if (method == "POST") {
response = Http.post(url, body, _headers)
} else if (method == "PUT") {
response = Http.put(url, body, _headers)
} else if (method == "PATCH") {
response = Http.patch(url, body, _headers)
} else if (method == "DELETE") {
response = Http.delete(url, _headers)
}
return parseResponse(response)
}
parseResponse(response) {
var result = {
"status": response.statusCode,
"headers": response.headers,
"ok": response.statusCode >= 200 && response.statusCode < 300
}
if (response.body.count > 0) {
var fiber = Fiber.new { Json.parse(response.body) }
var data = fiber.try()
result["data"] = fiber.error ? response.body : data
}
return result
}
}
System.print("=== API Client Demo ===\n")
var api = ApiClient.new("https://jsonplaceholder.typicode.com")
System.print("--- Fetching posts ---")
var result = api.get("/posts")
if (result["ok"]) {
System.print("Found %(result["data"].count) posts")
System.print("First post: %(result["data"][0]["title"])")
}
System.print("\n--- Creating post ---")
result = api.post("/posts", {
"title": "Created with Wren-CLI",
"body": "This is a test post",
"userId": 1
})
if (result["ok"]) {
System.print("Created post ID: %(result["data"]["id"])")
}
System.print("\n--- Updating post ---")
result = api.put("/posts/1", {
"id": 1,
"title": "Updated Title",
"body": "Updated body",
"userId": 1
})
System.print("Update status: %(result["status"])")
System.print("\n--- Deleting post ---")
result = api.delete("/posts/1")
System.print("Delete status: %(result["status"])")</code></pre>
<div class="admonition tip">
<div class="admonition-title">Tip</div>
<p>Save the <code>ApiClient</code> class in a separate file like <code>api_client.wren</code> and import it in your projects for reuse.</p>
</div>
<h2>Next Steps</h2>
<ul>
<li>Learn about <a href="../api/http.html">HTTP module</a> features in detail</li>
<li>Explore <a href="../api/json.html">JSON module</a> for advanced parsing</li>
<li>Build a <a href="websocket-chat.html">WebSocket chat application</a></li>
</ul>
</article>
<footer class="page-footer">
<a href="index.html" class="prev">Tutorials</a>
<a href="websocket-chat.html" class="next">WebSocket Chat</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>
+174
View File
@@ -0,0 +1,174 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tutorials - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html" class="active">Tutorial List</a></li>
<li><a href="http-client.html">HTTP Client</a></li>
<li><a href="websocket-chat.html">WebSocket Chat</a></li>
<li><a href="database-app.html">Database App</a></li>
<li><a href="template-rendering.html">Template Rendering</a></li>
<li><a href="cli-tool.html">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<span>Tutorials</span>
</nav>
<article>
<h1>Tutorials</h1>
<p>Step-by-step tutorials that guide you through building complete applications with Wren-CLI. Each tutorial introduces new concepts and builds upon previous knowledge.</p>
<div class="card-grid">
<div class="card">
<h3><a href="http-client.html">Building an HTTP Client</a></h3>
<p>Learn to make HTTP requests, parse JSON responses, and handle errors while building a REST API client.</p>
<div class="card-meta">
<span class="tag">http</span>
<span class="tag">json</span>
</div>
</div>
<div class="card">
<h3><a href="websocket-chat.html">WebSocket Chat Application</a></h3>
<p>Build a real-time chat application using WebSockets with both client and server components.</p>
<div class="card-meta">
<span class="tag">websocket</span>
<span class="tag">fibers</span>
</div>
</div>
<div class="card">
<h3><a href="database-app.html">Database Application</a></h3>
<p>Create a complete CRUD application using SQLite for persistent data storage.</p>
<div class="card-meta">
<span class="tag">sqlite</span>
<span class="tag">io</span>
</div>
</div>
<div class="card">
<h3><a href="template-rendering.html">Template Rendering</a></h3>
<p>Use Jinja templates to generate HTML pages, reports, and configuration files.</p>
<div class="card-meta">
<span class="tag">jinja</span>
<span class="tag">io</span>
</div>
</div>
<div class="card">
<h3><a href="cli-tool.html">Building a CLI Tool</a></h3>
<p>Create a command-line application with argument parsing, user input, and subprocess management.</p>
<div class="card-meta">
<span class="tag">os</span>
<span class="tag">subprocess</span>
</div>
</div>
</div>
<h2>Learning Path</h2>
<p>If you are new to Wren-CLI, we recommend following the tutorials in this order:</p>
<ol>
<li><strong>HTTP Client</strong> - Introduces async operations and JSON handling</li>
<li><strong>Database Application</strong> - Covers data persistence and file I/O</li>
<li><strong>Template Rendering</strong> - Learn the Jinja template system</li>
<li><strong>WebSocket Chat</strong> - Advanced async patterns with fibers</li>
<li><strong>CLI Tool</strong> - Bringing it all together in a real application</li>
</ol>
<h2>Prerequisites</h2>
<p>Before starting the tutorials, you should:</p>
<ul>
<li>Have Wren-CLI <a href="../getting-started/installation.html">installed</a></li>
<li>Understand the <a href="../language/index.html">basic syntax</a></li>
<li>Be familiar with <a href="../language/classes.html">classes</a> and <a href="../language/methods.html">methods</a></li>
</ul>
</article>
<footer class="page-footer">
<a href="../api/math.html" class="prev">math</a>
<a href="http-client.html" class="next">HTTP Client</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>
+667
View File
@@ -0,0 +1,667 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Rendering - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html">Tutorial List</a></li>
<li><a href="http-client.html">HTTP Client</a></li>
<li><a href="websocket-chat.html">WebSocket Chat</a></li>
<li><a href="database-app.html">Database App</a></li>
<li><a href="template-rendering.html" class="active">Template Rendering</a></li>
<li><a href="cli-tool.html">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">Tutorials</a>
<span class="separator">/</span>
<span>Template Rendering</span>
</nav>
<article>
<h1>Template Rendering</h1>
<p>In this tutorial, you will learn to use Jinja templates to generate HTML pages, reports, and configuration files. Jinja is a powerful templating engine that separates your presentation logic from your data.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Basic template syntax (variables, expressions)</li>
<li>Control structures (if, for)</li>
<li>Template inheritance</li>
<li>Filters and macros</li>
<li>Loading templates from files</li>
</ul>
<h2>Step 1: Basic Templates</h2>
<p>Create a file called <code>template_demo.wren</code>:</p>
<pre><code>import "jinja" for Environment, DictLoader
var env = Environment.new(DictLoader.new({
"greeting": "Hello, {{ name }}!"
}))
var template = env.getTemplate("greeting")
var result = template.render({"name": "World"})
System.print(result) // Hello, World!</code></pre>
<h2>Step 2: Variables and Expressions</h2>
<p>Jinja supports various expressions inside <code>{{ }}</code>:</p>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"expressions": "
Name: {{ user.name }}
Age: {{ user.age }}
Adult: {{ user.age >= 18 }}
Items: {{ items | length }}
First: {{ items[0] }}
Upper: {{ user.name | upper }}
"
}
var env = Environment.new(DictLoader.new(templates))
var template = env.getTemplate("expressions")
var result = template.render({
"user": {"name": "Alice", "age": 25},
"items": ["apple", "banana", "cherry"]
})
System.print(result)</code></pre>
<p>Output:</p>
<pre><code>Name: Alice
Age: 25
Adult: true
Items: 3
First: apple
Upper: ALICE</code></pre>
<h2>Step 3: Control Structures</h2>
<h3>Conditionals</h3>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"status": "
{% if user.active %}
User {{ user.name }} is active.
{% elif user.pending %}
User {{ user.name }} is pending approval.
{% else %}
User {{ user.name }} is inactive.
{% endif %}
"
}
var env = Environment.new(DictLoader.new(templates))
var template = env.getTemplate("status")
System.print(template.render({"user": {"name": "Bob", "active": true}}))
System.print(template.render({"user": {"name": "Carol", "pending": true}}))
System.print(template.render({"user": {"name": "Dave", "active": false}}))</code></pre>
<h3>Loops</h3>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"list": "
Shopping List:
{% for item in items %}
- {{ item.name }}: ${{ item.price }}
{% endfor %}
Total items: {{ items | length }}
"
}
var env = Environment.new(DictLoader.new(templates))
var template = env.getTemplate("list")
var result = template.render({
"items": [
{"name": "Apples", "price": 2.99},
{"name": "Bread", "price": 3.50},
{"name": "Milk", "price": 4.25}
]
})
System.print(result)</code></pre>
<h3>Loop Variables</h3>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"numbered": "
{% for item in items %}
{{ loop.index }}. {{ item }}{% if loop.first %} (first){% endif %}{% if loop.last %} (last){% endif %}
{% endfor %}
"
}
var env = Environment.new(DictLoader.new(templates))
var template = env.getTemplate("numbered")
var result = template.render({
"items": ["Red", "Green", "Blue"]
})
System.print(result)</code></pre>
<p>Output:</p>
<pre><code>1. Red (first)
2. Green
3. Blue (last)</code></pre>
<h2>Step 4: Filters</h2>
<p>Filters transform values using the pipe (<code>|</code>) syntax:</p>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"filters": "
{{ name | upper }}
{{ name | lower }}
{{ name | capitalize }}
{{ name | title }}
{{ price | round(2) }}
{{ items | join(', ') }}
{{ text | truncate(20) }}
{{ html | escape }}
{{ value | default('N/A') }}
"
}
var env = Environment.new(DictLoader.new(templates))
var template = env.getTemplate("filters")
var result = template.render({
"name": "hELLo WoRLD",
"price": 19.99567,
"items": ["a", "b", "c"],
"text": "This is a very long string that should be truncated",
"html": "<script>alert('xss')</script>",
"value": null
})
System.print(result)</code></pre>
<h2>Step 5: Template Inheritance</h2>
<p>Template inheritance allows you to build a base template with common structure:</p>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"base.html": "
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
<header>
<nav>Home | About | Contact</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
Copyright 2024
</footer>
</body>
</html>
",
"home.html": "
{% extends 'base.html' %}
{% block title %}Home - My Site{% endblock %}
{% block content %}
<h1>Welcome!</h1>
<p>This is the home page.</p>
{% endblock %}
",
"about.html": "
{% extends 'base.html' %}
{% block title %}About - My Site{% endblock %}
{% block content %}
<h1>About Us</h1>
<p>{{ description }}</p>
{% endblock %}
"
}
var env = Environment.new(DictLoader.new(templates))
System.print("=== Home Page ===")
System.print(env.getTemplate("home.html").render({}))
System.print("\n=== About Page ===")
System.print(env.getTemplate("about.html").render({
"description": "We are a software company."
}))</code></pre>
<h2>Step 6: Macros</h2>
<p>Macros are reusable template functions:</p>
<pre><code>import "jinja" for Environment, DictLoader
var templates = {
"forms": "
{% macro input(name, type='text', value='', placeholder='') %}
<input type=\"{{ type }}\" name=\"{{ name }}\" value=\"{{ value }}\" placeholder=\"{{ placeholder }}\">
{% endmacro %}
{% macro button(text, type='button', class='btn') %}
<button type=\"{{ type }}\" class=\"{{ class }}\">{{ text }}</button>
{% endmacro %}
<form>
{{ input('username', placeholder='Enter username') }}
{{ input('password', type='password', placeholder='Enter password') }}
{{ button('Login', type='submit', class='btn btn-primary') }}
</form>
"
}
var env = Environment.new(DictLoader.new(templates))
var result = env.getTemplate("forms").render({})
System.print(result)</code></pre>
<h2>Step 7: Loading Templates from Files</h2>
<p>For larger projects, store templates in files:</p>
<pre><code>import "jinja" for Environment, FileSystemLoader
import "io" for File
File.write("templates/base.html", "
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
")
File.write("templates/page.html", "
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
{{ content }}
{% endblock %}
")
var env = Environment.new(FileSystemLoader.new("templates"))
var template = env.getTemplate("page.html")
var html = template.render({
"title": "My Page",
"content": "<p>Hello from a file-based template!</p>"
})
System.print(html)</code></pre>
<h2>Step 8: Building a Report Generator</h2>
<p>Let's build a practical example - generating HTML reports:</p>
<pre><code>import "jinja" for Environment, DictLoader
import "io" for File
import "sqlite" for Sqlite
import "datetime" for DateTime
class ReportGenerator {
construct new() {
_env = Environment.new(DictLoader.new({
"report": "
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100\%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.summary { background: #f9f9f9; padding: 20px; margin: 20px 0; }
.footer { margin-top: 40px; color: #666; font-size: 12px; }
</style>
</head>
<body>
<h1>{{ title }}</h1>
<p>Generated: {{ generated_at }}</p>
<div class=\"summary\">
<h2>Summary</h2>
<p>Total Records: {{ data | length }}</p>
{% if total %}
<p>Total Amount: ${{ total | round(2) }}</p>
{% endif %}
</div>
<h2>Details</h2>
<table>
<thead>
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in data %}
<tr>
{% for header in headers %}
<td>{{ row[header] }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<div class=\"footer\">
Report generated by Wren-CLI Report Generator
</div>
</body>
</html>
"
}))
}
generate(title, headers, data, options) {
var template = _env.getTemplate("report")
var total = null
if (options && options["sumColumn"]) {
total = 0
for (row in data) {
total = total + (row[options["sumColumn"]] || 0)
}
}
return template.render({
"title": title,
"headers": headers,
"data": data,
"total": total,
"generated_at": DateTime.now().toString
})
}
save(filename, html) {
File.write(filename, html)
System.print("Report saved to %(filename)")
}
}
var generator = ReportGenerator.new()
var salesData = [
{"Product": "Widget A", "Quantity": 150, "Price": 29.99, "Total": 4498.50},
{"Product": "Widget B", "Quantity": 75, "Price": 49.99, "Total": 3749.25},
{"Product": "Widget C", "Quantity": 200, "Price": 19.99, "Total": 3998.00},
{"Product": "Widget D", "Quantity": 50, "Price": 99.99, "Total": 4999.50}
]
var html = generator.generate(
"Sales Report Q4 2024",
["Product", "Quantity", "Price", "Total"],
salesData,
{"sumColumn": "Total"}
)
generator.save("sales_report.html", html)
System.print("Report generated successfully!")</code></pre>
<h2>Step 9: Email Templates</h2>
<p>Create personalized emails with templates:</p>
<pre><code>import "jinja" for Environment, DictLoader
class EmailGenerator {
construct new() {
_env = Environment.new(DictLoader.new({
"welcome": "
Subject: Welcome to {{ company }}, {{ user.name }}!
Dear {{ user.name }},
Thank you for joining {{ company }}! We are excited to have you.
Your account details:
- Username: {{ user.username }}
- Email: {{ user.email }}
- Plan: {{ user.plan | default('Free') }}
{% if user.plan == 'Premium' %}
As a Premium member, you have access to:
{% for feature in premium_features %}
- {{ feature }}
{% endfor %}
{% endif %}
If you have any questions, please contact us at {{ support_email }}.
Best regards,
The {{ company }} Team
",
"order_confirmation": "
Subject: Order #{{ order.id }} Confirmed
Dear {{ customer.name }},
Thank you for your order!
Order Details:
{% for item in order.items %}
- {{ item.name }} x {{ item.quantity }} @ ${{ item.price }} = ${{ item.total }}
{% endfor %}
Subtotal: ${{ order.subtotal | round(2) }}
Tax: ${{ order.tax | round(2) }}
Total: ${{ order.total | round(2) }}
Shipping to:
{{ customer.address.street }}
{{ customer.address.city }}, {{ customer.address.state }} {{ customer.address.zip }}
Estimated delivery: {{ delivery_date }}
Thank you for shopping with us!
"
}))
}
welcome(user, company) {
return _env.getTemplate("welcome").render({
"user": user,
"company": company,
"support_email": "support@%(company.lower).com",
"premium_features": [
"Unlimited storage",
"Priority support",
"Advanced analytics",
"Custom integrations"
]
})
}
orderConfirmation(customer, order) {
return _env.getTemplate("order_confirmation").render({
"customer": customer,
"order": order,
"delivery_date": "3-5 business days"
})
}
}
var emails = EmailGenerator.new()
var welcomeEmail = emails.welcome(
{
"name": "Alice Smith",
"username": "alice",
"email": "alice@example.com",
"plan": "Premium"
},
"Acme Corp"
)
System.print(welcomeEmail)
System.print("\n---\n")
var orderEmail = emails.orderConfirmation(
{
"name": "Bob Jones",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
},
{
"id": "ORD-12345",
"items": [
{"name": "Blue Widget", "quantity": 2, "price": 29.99, "total": 59.98},
{"name": "Red Gadget", "quantity": 1, "price": 49.99, "total": 49.99}
],
"subtotal": 109.97,
"tax": 9.90,
"total": 119.87
}
)
System.print(orderEmail)</code></pre>
<div class="admonition tip">
<div class="admonition-title">Tip</div>
<p>Use the <code>escape</code> filter on user-provided content to prevent XSS attacks in HTML output: <code>{{ user_input | escape }}</code></p>
</div>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Jinja whitespace can be controlled with <code>{%-</code> and <code>-%}</code> to strip whitespace before or after tags.</p>
</div>
<h2>Next Steps</h2>
<ul>
<li>Explore all <a href="../api/jinja.html">Jinja filters and features</a></li>
<li>Combine with <a href="database-app.html">database queries</a> for dynamic reports</li>
<li>Build a <a href="cli-tool.html">CLI tool</a> for template processing</li>
</ul>
</article>
<footer class="page-footer">
<a href="database-app.html" class="prev">Database App</a>
<a href="cli-tool.html" class="next">CLI Tool</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>
+620
View File
@@ -0,0 +1,620 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat Application - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="../api/index.html">Overview</a></li>
<li><a href="../api/http.html">http</a></li>
<li><a href="../api/websocket.html">websocket</a></li>
<li><a href="../api/tls.html">tls</a></li>
<li><a href="../api/net.html">net</a></li>
<li><a href="../api/dns.html">dns</a></li>
<li><a href="../api/json.html">json</a></li>
<li><a href="../api/base64.html">base64</a></li>
<li><a href="../api/regex.html">regex</a></li>
<li><a href="../api/jinja.html">jinja</a></li>
<li><a href="../api/crypto.html">crypto</a></li>
<li><a href="../api/os.html">os</a></li>
<li><a href="../api/env.html">env</a></li>
<li><a href="../api/signal.html">signal</a></li>
<li><a href="../api/subprocess.html">subprocess</a></li>
<li><a href="../api/sqlite.html">sqlite</a></li>
<li><a href="../api/datetime.html">datetime</a></li>
<li><a href="../api/timer.html">timer</a></li>
<li><a href="../api/io.html">io</a></li>
<li><a href="../api/scheduler.html">scheduler</a></li>
<li><a href="../api/math.html">math</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="index.html">Tutorial List</a></li>
<li><a href="http-client.html">HTTP Client</a></li>
<li><a href="websocket-chat.html" class="active">WebSocket Chat</a></li>
<li><a href="database-app.html">Database App</a></li>
<li><a href="template-rendering.html">Template Rendering</a></li>
<li><a href="cli-tool.html">CLI Tool</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">Tutorials</a>
<span class="separator">/</span>
<span>WebSocket Chat</span>
</nav>
<article>
<h1>WebSocket Chat Application</h1>
<p>In this tutorial, you will build a real-time chat application using WebSockets. You will create both a server that handles multiple clients and a client that can send and receive messages.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Creating a WebSocket server</li>
<li>Handling multiple client connections</li>
<li>Broadcasting messages to all clients</li>
<li>Building a WebSocket client</li>
<li>Working with fibers for concurrent operations</li>
</ul>
<h2>Part 1: The Chat Server</h2>
<h3>Step 1: Basic Server Setup</h3>
<p>Create a file called <code>chat_server.wren</code>:</p>
<pre><code>import "websocket" for WebSocketServer
import "json" for Json
System.print("Starting chat server on port 8080...")
var server = WebSocketServer.new("0.0.0.0", 8080)
var clients = []
while (true) {
var client = server.accept()
System.print("Client connected!")
clients.add(client)
}</code></pre>
<h3>Step 2: Handling Client Messages</h3>
<p>Now let's process messages from clients using fibers:</p>
<pre><code>import "websocket" for WebSocketServer, WebSocketMessage
import "json" for Json
System.print("Starting chat server on port 8080...")
var server = WebSocketServer.new("0.0.0.0", 8080)
var clients = []
var handleClient = Fn.new { |client|
while (true) {
var message = client.receive()
if (message == null) {
System.print("Client disconnected")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
System.print("Received: %(message.payload)")
}
}
}
while (true) {
var client = server.accept()
System.print("Client connected!")
clients.add(client)
var fiber = Fiber.new { handleClient.call(client) }
fiber.call()
}</code></pre>
<h3>Step 3: Broadcasting Messages</h3>
<p>Let's broadcast messages to all connected clients:</p>
<pre><code>import "websocket" for WebSocketServer, WebSocketMessage
import "json" for Json
System.print("=== Chat Server ===")
System.print("Listening on ws://0.0.0.0:8080")
var server = WebSocketServer.new("0.0.0.0", 8080)
var clients = []
var broadcast = Fn.new { |message, sender|
var data = Json.stringify({
"type": "message",
"from": sender,
"text": message
})
for (client in clients) {
var fiber = Fiber.new { client.send(data) }
fiber.try()
}
}
var removeClient = Fn.new { |client|
var index = clients.indexOf(client)
if (index >= 0) {
clients.removeAt(index)
}
}
var handleClient = Fn.new { |client, clientId|
client.send(Json.stringify({
"type": "welcome",
"message": "Welcome to the chat!",
"clientId": clientId
}))
broadcast.call("%(clientId) joined the chat", "System")
while (true) {
var message = client.receive()
if (message == null) {
System.print("Client %(clientId) disconnected")
removeClient.call(client)
broadcast.call("%(clientId) left the chat", "System")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
var data = Json.parse(message.payload)
System.print("[%(clientId)] %(data["text"])")
broadcast.call(data["text"], clientId)
}
}
}
var clientCounter = 0
while (true) {
var client = server.accept()
clientCounter = clientCounter + 1
var clientId = "User%(clientCounter)"
System.print("%(clientId) connected")
clients.add(client)
Fiber.new { handleClient.call(client, clientId) }.call()
}</code></pre>
<h2>Part 2: The Chat Client</h2>
<h3>Step 4: Basic Client</h3>
<p>Create a file called <code>chat_client.wren</code>:</p>
<pre><code>import "websocket" for WebSocket, WebSocketMessage
import "json" for Json
import "io" for Stdin
System.print("Connecting to chat server...")
var ws = WebSocket.connect("ws://localhost:8080")
System.print("Connected!")
var receiveMessages = Fn.new {
while (true) {
var message = ws.receive()
if (message == null) {
System.print("Disconnected from server")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
var data = Json.parse(message.payload)
if (data["type"] == "welcome") {
System.print("\n%(data["message"])")
System.print("You are: %(data["clientId"])\n")
} else if (data["type"] == "message") {
System.print("[%(data["from"])] %(data["text"])")
}
}
}
}
Fiber.new { receiveMessages.call() }.call()
System.print("Type messages and press Enter to send. Type 'quit' to exit.\n")
while (true) {
System.write("> ")
var input = Stdin.readLine()
if (input == "quit") {
ws.close()
break
}
if (input.count > 0) {
ws.send(Json.stringify({"text": input}))
}
}</code></pre>
<h2>Part 3: Enhanced Features</h2>
<h3>Step 5: Private Messages</h3>
<p>Add support for private messages with the <code>/msg</code> command:</p>
<pre><code>import "websocket" for WebSocketServer, WebSocketMessage
import "json" for Json
import "regex" for Regex
System.print("=== Enhanced Chat Server ===")
System.print("Listening on ws://0.0.0.0:8080")
var server = WebSocketServer.new("0.0.0.0", 8080)
var clients = {}
var broadcast = Fn.new { |type, data|
var message = Json.stringify({"type": type}.merge(data))
for (id in clients.keys) {
var fiber = Fiber.new { clients[id].send(message) }
fiber.try()
}
}
var sendTo = Fn.new { |clientId, type, data|
if (clients.containsKey(clientId)) {
var message = Json.stringify({"type": type}.merge(data))
clients[clientId].send(message)
return true
}
return false
}
var handleCommand = Fn.new { |client, clientId, text|
var msgMatch = Regex.new("^/msg (\\w+) (.+)$").match(text)
if (msgMatch) {
var target = msgMatch.group(1)
var message = msgMatch.group(2)
if (sendTo.call(target, "private", {"from": clientId, "text": message})) {
sendTo.call(clientId, "private", {"from": "You -> %(target)", "text": message})
} else {
sendTo.call(clientId, "error", {"message": "User %(target) not found"})
}
return true
}
if (text == "/users") {
var userList = clients.keys.toList.join(", ")
sendTo.call(clientId, "info", {"message": "Online users: %(userList)"})
return true
}
if (text == "/help") {
sendTo.call(clientId, "info", {
"message": "Commands: /msg <user> <message>, /users, /help"
})
return true
}
return false
}
var handleClient = Fn.new { |client, clientId|
client.send(Json.stringify({
"type": "welcome",
"clientId": clientId
}))
broadcast.call("join", {"user": clientId})
while (true) {
var message = client.receive()
if (message == null) {
clients.remove(clientId)
broadcast.call("leave", {"user": clientId})
System.print("%(clientId) disconnected")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
var data = Json.parse(message.payload)
var text = data["text"]
if (!handleCommand.call(client, clientId, text)) {
broadcast.call("message", {"from": clientId, "text": text})
}
}
}
}
var clientCounter = 0
while (true) {
var client = server.accept()
clientCounter = clientCounter + 1
var clientId = "User%(clientCounter)"
System.print("%(clientId) connected")
clients[clientId] = client
Fiber.new { handleClient.call(client, clientId) }.call()
}</code></pre>
<h3>Step 6: Enhanced Client</h3>
<p>Update the client to handle new message types:</p>
<pre><code>import "websocket" for WebSocket, WebSocketMessage
import "json" for Json
import "io" for Stdin
System.print("Connecting to chat server...")
var ws = WebSocket.connect("ws://localhost:8080")
var myId = ""
var receiveMessages = Fn.new {
while (true) {
var message = ws.receive()
if (message == null) {
System.print("\nDisconnected from server")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
var data = Json.parse(message.payload)
if (data["type"] == "welcome") {
myId = data["clientId"]
System.print("Connected as %(myId)")
System.print("Type /help for commands\n")
} else if (data["type"] == "message") {
System.print("[%(data["from"])] %(data["text"])")
} else if (data["type"] == "private") {
System.print("[PM %(data["from"])] %(data["text"])")
} else if (data["type"] == "join") {
System.print("* %(data["user"]) joined the chat")
} else if (data["type"] == "leave") {
System.print("* %(data["user"]) left the chat")
} else if (data["type"] == "info") {
System.print("INFO: %(data["message"])")
} else if (data["type"] == "error") {
System.print("ERROR: %(data["message"])")
}
}
}
}
Fiber.new { receiveMessages.call() }.call()
while (true) {
var input = Stdin.readLine()
if (input == null || input == "/quit") {
ws.close()
break
}
if (input.count > 0) {
ws.send(Json.stringify({"text": input}))
}
}</code></pre>
<h2>Part 4: Running the Application</h2>
<h3>Starting the Server</h3>
<pre><code>$ wren_cli chat_server.wren
=== Enhanced Chat Server ===
Listening on ws://0.0.0.0:8080</code></pre>
<h3>Connecting Clients</h3>
<p>Open multiple terminals and run:</p>
<pre><code>$ wren_cli chat_client.wren
Connecting to chat server...
Connected as User1
Type /help for commands
> Hello everyone!
[User1] Hello everyone!
* User2 joined the chat
[User2] Hi there!
> /msg User2 This is a private message
[PM You -> User2] This is a private message</code></pre>
<h2>Complete Server Code</h2>
<pre><code>import "websocket" for WebSocketServer, WebSocketMessage
import "json" for Json
import "regex" for Regex
import "datetime" for DateTime
class ChatServer {
construct new(host, port) {
_server = WebSocketServer.new(host, port)
_clients = {}
_messageHistory = []
_maxHistory = 100
}
broadcast(type, data) {
var message = Json.stringify({"type": type, "timestamp": DateTime.now().toString}.merge(data))
for (id in _clients.keys) {
var fiber = Fiber.new { _clients[id].send(message) }
fiber.try()
}
}
sendTo(clientId, type, data) {
if (_clients.containsKey(clientId)) {
var message = Json.stringify({"type": type}.merge(data))
_clients[clientId].send(message)
return true
}
return false
}
handleCommand(client, clientId, text) {
if (text.startsWith("/msg ")) {
var parts = text[5..-1].split(" ")
if (parts.count >= 2) {
var target = parts[0]
var message = parts[1..-1].join(" ")
if (sendTo(target, "private", {"from": clientId, "text": message})) {
sendTo(clientId, "private", {"from": "You -> %(target)", "text": message})
} else {
sendTo(clientId, "error", {"message": "User not found"})
}
}
return true
}
if (text == "/users") {
sendTo(clientId, "info", {"message": "Online: %(_clients.keys.toList.join(", "))"})
return true
}
if (text == "/help") {
sendTo(clientId, "info", {"message": "/msg <user> <text>, /users, /help, /quit"})
return true
}
return false
}
handleClient(client, clientId) {
_clients[clientId] = client
sendTo(clientId, "welcome", {"clientId": clientId, "users": _clients.keys.toList})
broadcast("join", {"user": clientId})
while (true) {
var message = client.receive()
if (message == null) {
_clients.remove(clientId)
broadcast("leave", {"user": clientId})
System.print("[%(DateTime.now())] %(clientId) disconnected")
break
}
if (message.opcode == WebSocketMessage.TEXT) {
var data = Json.parse(message.payload)
var text = data["text"]
if (!handleCommand(client, clientId, text)) {
System.print("[%(DateTime.now())] %(clientId): %(text)")
broadcast("message", {"from": clientId, "text": text})
_messageHistory.add({"from": clientId, "text": text, "time": DateTime.now().toString})
if (_messageHistory.count > _maxHistory) {
_messageHistory.removeAt(0)
}
}
} else if (message.opcode == WebSocketMessage.PING) {
client.pong(message.payload)
}
}
}
run() {
System.print("Chat server running on port 8080")
var counter = 0
while (true) {
var client = _server.accept()
counter = counter + 1
var clientId = "User%(counter)"
System.print("[%(DateTime.now())] %(clientId) connected")
Fiber.new { handleClient(client, clientId) }.call()
}
}
}
var server = ChatServer.new("0.0.0.0", 8080)
server.run()</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>WebSocket connections in Wren-CLI use fibers for concurrent handling. Each client runs in its own fiber, allowing the server to handle multiple connections simultaneously.</p>
</div>
<h2>Next Steps</h2>
<ul>
<li>Add user authentication</li>
<li>Store message history in <a href="database-app.html">SQLite</a></li>
<li>Create chat rooms</li>
<li>See the <a href="../api/websocket.html">WebSocket API reference</a></li>
</ul>
</article>
<footer class="page-footer">
<a href="http-client.html" class="prev">HTTP Client</a>
<a href="database-app.html" class="next">Database App</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>