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:
@@ -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 <command> [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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user