Update.
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user