GURT Client Library
The GURT client library (for Rust) provides a high-level, HTTP-like interface for making requests to GURT servers. It handles TLS encryption, protocol handshakes, and connection management automatically.
Bindings
- Godot by Gurted - 🔗 link
- No bidings for other languages are currently available.
Installation
Install via Cargo:
cargo add gurtlib
Quick Start
use gurtlib::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
let client = GurtClient::new();
// Make a GET request
let response = client.get("gurt://example.com/").await?;
println!("Status: {}", response.status_code);
println!("Body: {}", response.text()?);
Ok(())
}
Creating a Client
Default Client
let client = GurtClient::new();
Custom Configuration
use tokio::time::Duration;
let config = GurtClientConfig {
connect_timeout: Duration::from_secs(10),
request_timeout: Duration::from_secs(30),
handshake_timeout: Duration::from_secs(5),
user_agent: "MyApp/1.0.0".to_string(),
max_redirects: 5,
};
let client = GurtClient::with_config(config);
Making Requests
GET Requests
let response = client.get("gurt://api.example.com/users").await?;
if response.is_success() {
println!("Success: {}", response.text()?);
} else {
println!("Error: {} {}", response.status_code, response.status_message);
}
POST Requests
Text Data
let response = client.post("gurt://api.example.com/submit", "Hello, GURT!").await?;
JSON Data
use serde_json::json;
let data = json!({
"name": "John Doe",
"email": "[email protected]"
});
let response = client.post_json("gurt://api.example.com/users", &data).await?;
PUT Requests
// Text data
let response = client.put("gurt://api.example.com/resource/123", "Updated content").await?;
// JSON data
let update_data = json!({"status": "completed"});
let response = client.put_json("gurt://api.example.com/tasks/456", &update_data).await?;
DELETE Requests
let response = client.delete("gurt://api.example.com/users/123").await?;
HEAD Requests
let response = client.head("gurt://api.example.com/large-file").await?;
// Check headers without downloading body
let content_length = response.headers.get("content-length");
OPTIONS Requests
let response = client.options("gurt://api.example.com/endpoint").await?;
// Check allowed methods
let allowed_methods = response.headers.get("allow");
PATCH Requests
let patch_data = json!({"name": "Updated Name"});
let response = client.patch_json("gurt://api.example.com/users/123", &patch_data).await?;
Response Handling
Response Structure
pub struct GurtResponse {
pub version: String,
pub status_code: u16,
pub status_message: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
Accessing Response Data
let response = client.get("gurt://api.example.com/data").await?;
// Status information
println!("Status Code: {}", response.status_code);
println!("Status Message: {}", response.status_message);
// Headers
for (name, value) in &response.headers {
println!("{}: {}", name, value);
}
// Body as string
let text = response.text()?;
// Body as bytes
let bytes = &response.body;
// Parse JSON response
let json_data: serde_json::Value = serde_json::from_slice(&response.body)?;
Status Code Checking
if response.is_success() {
// 2xx status codes
println!("Request successful");
} else if response.is_client_error() {
// 4xx status codes
println!("Client error: {}", response.status_message);
} else if response.is_server_error() {
// 5xx status codes
println!("Server error: {}", response.status_message);
}
Protocol Implementation
The GURT client automatically handles the complete GURT protocol:
- TCP Connection: Establishes initial connection to the server
- Handshake: Sends
HANDSHAKE
request and waits for101 Switching Protocols
- TLS Upgrade: Upgrades the connection to TLS 1.3 with GURT ALPN
- Request/Response: Sends the actual HTTP-style request over encrypted connection
All of this happens transparently when you call methods like get()
, post()
, etc.
URL Parsing
The client automatically parses gurt://
URLs:
// These are all valid GURT URLs:
client.get("gurt://example.com/").await?; // Port 4878 (default)
client.get("gurt://example.com:8080/api").await?; // Custom port
client.get("gurt://192.168.1.100/test").await?; // IP address
client.get("gurt://localhost:4878/dev").await?; // Localhost
URL Components
The client extracts:
- Host: Domain name or IP address
- Port: Specified port or default (4878)
- Path: Request path (defaults to
/
)
Error Handling
Error Types
use gurtlib::GurtError;
match client.get("gurt://invalid-url").await {
Ok(response) => {
// Handle successful response
}
Err(GurtError::InvalidMessage(msg)) => {
println!("Invalid request: {}", msg);
}
Err(GurtError::Connection(msg)) => {
println!("Connection error: {}", msg);
}
Err(GurtError::Timeout(msg)) => {
println!("Request timeout: {}", msg);
}
Err(GurtError::Io(err)) => {
println!("IO error: {}", err);
}
Err(err) => {
println!("Other error: {}", err);
}
}
Timeout Configuration
let config = GurtClientConfig {
connect_timeout: Duration::from_secs(5), // Connection timeout
request_timeout: Duration::from_secs(30), // Overall request timeout
handshake_timeout: Duration::from_secs(3), // GURT handshake timeout
..Default::default()
};
Why Rust-first?
Rust was chosen for the official GURT protocol implementation due to its embedded nature.
To keep the core organized & not write identical code in GDScript, we used a GDExtension. A GDExtension can be created with a multitude of languages, but Rust was the one that provided the best performance, size, and programming ergonomics.
We expect the community to implement bindings for other languages, such as Python and JavaScript, to make GURT accessible for everybody!
Example: Building a GURT API Client
use gurtlib::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
struct ApiClient {
client: GurtClient,
base_url: String,
}
impl ApiClient {
fn new(base_url: String) -> Self {
Self {
client: GurtClient::new(),
base_url,
}
}
async fn create_user(&self, user: CreateUser) -> Result<User> {
let url = format!("{}/users", self.base_url);
let response = self.client.post_json(&url, &user).await?;
if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}
let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}
async fn get_user(&self, id: u64) -> Result<User> {
let url = format!("{}/users/{}", self.base_url, id);
let response = self.client.get(&url).await?;
if response.status_code == 404 {
return Err(GurtError::invalid_message("User not found".to_string()));
}
if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}
let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let api = ApiClient::new("gurt://api.example.com".to_string());
// Create a user
let new_user = CreateUser {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
};
let user = api.create_user(new_user).await?;
println!("Created user: {} (ID: {})", user.name, user.id);
// Retrieve the user
let retrieved_user = api.get_user(user.id).await?;
println!("Retrieved user: {}", retrieved_user.name);
Ok(())
}