148 lines
5.3 KiB
Rust
148 lines
5.3 KiB
Rust
//! REST binding — maps service methods to HTTP endpoints.
|
|
//!
|
|
//! Method → endpoint mapping (default convention, overridable):
|
|
//! `get_*` → GET /resource/{id}
|
|
//! `list_*` → GET /resource
|
|
//! `create_*` → POST /resource
|
|
//! `update_*` → PUT /resource/{id}
|
|
//! `delete_*` → DELETE /resource/{id}
|
|
//!
|
|
//! In production, use `reqwest` for the HTTP client. This implementation
|
|
//! builds the request and returns a mock response so the binding logic
|
|
//! can be tested without a live server.
|
|
|
|
use super::{Binding, BindingKind, ServiceRequest, ServiceResponse};
|
|
use crate::{config::ServiceConfig, ServiceError, ServiceResult};
|
|
|
|
/// HTTP REST binding.
|
|
pub struct RestBinding {
|
|
pub config: ServiceConfig,
|
|
}
|
|
|
|
impl RestBinding {
|
|
pub fn new(config: ServiceConfig) -> Self {
|
|
Self { config }
|
|
}
|
|
|
|
/// Derive the HTTP method from the service method name.
|
|
pub fn http_method(method_name: &str) -> &'static str {
|
|
let lower = method_name.to_lowercase();
|
|
if lower.starts_with("get_") || lower.starts_with("fetch_") || lower.starts_with("find_") {
|
|
"GET"
|
|
} else if lower.starts_with("list_") || lower.starts_with("all_") {
|
|
"GET"
|
|
} else if lower.starts_with("create_") || lower.starts_with("add_") || lower.starts_with("post_") {
|
|
"POST"
|
|
} else if lower.starts_with("update_") || lower.starts_with("edit_") || lower.starts_with("put_") {
|
|
"PUT"
|
|
} else if lower.starts_with("delete_") || lower.starts_with("remove_") {
|
|
"DELETE"
|
|
} else if lower.starts_with("patch_") {
|
|
"PATCH"
|
|
} else {
|
|
"POST"
|
|
}
|
|
}
|
|
|
|
/// Derive the URL path from the service name and method name.
|
|
/// `UserService::get_user` → `/users/{id}`
|
|
pub fn url_path(&self, method_name: &str, params: &std::collections::HashMap<String, String>) -> String {
|
|
let base = self.config.base_url.as_deref().unwrap_or("");
|
|
let resource = self.resource_name();
|
|
let http_method = Self::http_method(method_name);
|
|
|
|
// Check if there's an ID param
|
|
let id_param = params.get("id").or_else(|| {
|
|
params.values().next()
|
|
});
|
|
|
|
match (http_method, id_param) {
|
|
("GET" | "PUT" | "DELETE", Some(id)) => {
|
|
format!("{}/{}/{}", base, resource, id)
|
|
}
|
|
_ => format!("{}/{}", base, resource),
|
|
}
|
|
}
|
|
|
|
/// Derive the REST resource name from the service name.
|
|
/// `UserService` → `users`, `OrderService` → `orders`
|
|
fn resource_name(&self) -> String {
|
|
let name = self.config.name.trim_end_matches("Service");
|
|
let lower = name.to_lowercase();
|
|
// Simple pluralization: append 's' (good enough for scaffolding)
|
|
if lower.ends_with('s') {
|
|
lower
|
|
} else {
|
|
format!("{}s", lower)
|
|
}
|
|
}
|
|
|
|
/// Build auth headers for the request.
|
|
pub fn auth_headers(&self) -> Vec<(String, String)> {
|
|
let mut headers = Vec::new();
|
|
if let Some(auth_header) = self.config.auth.authorization_header() {
|
|
headers.push(("Authorization".to_string(), auth_header));
|
|
}
|
|
if let Some((key, val)) = self.config.auth.api_key_header() {
|
|
headers.push((key, val));
|
|
}
|
|
headers
|
|
}
|
|
|
|
/// Build the full request description (for logging/testing without live HTTP).
|
|
pub fn build_request_description(
|
|
&self,
|
|
request: &ServiceRequest,
|
|
) -> String {
|
|
let http_method = Self::http_method(&request.method);
|
|
let url = self.url_path(&request.method, &request.params);
|
|
let headers = self.auth_headers();
|
|
let body = if matches!(http_method, "POST" | "PUT" | "PATCH") {
|
|
serde_params_to_json(&request.params)
|
|
} else {
|
|
String::new()
|
|
};
|
|
format!(
|
|
"{} {}\nHeaders: {:?}\nBody: {}",
|
|
http_method, url, headers, body
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Minimal JSON serialization for params (no serde dependency).
|
|
pub(crate) fn serde_params_to_json(params: &std::collections::HashMap<String, String>) -> String {
|
|
let fields: Vec<String> = params
|
|
.iter()
|
|
.map(|(k, v)| format!("\"{}\":\"{}\"", k, v.replace('"', "\\\"")))
|
|
.collect();
|
|
format!("{{{}}}", fields.join(","))
|
|
}
|
|
|
|
impl Binding for RestBinding {
|
|
fn kind(&self) -> BindingKind {
|
|
BindingKind::Rest
|
|
}
|
|
|
|
fn call(&self, request: ServiceRequest) -> ServiceResult<ServiceResponse> {
|
|
if self.config.base_url.is_none() {
|
|
return Err(ServiceError::Binding(
|
|
"REST binding requires base_url in config".into(),
|
|
));
|
|
}
|
|
|
|
let _description = self.build_request_description(&request);
|
|
|
|
// In production: use reqwest to make the actual HTTP call:
|
|
// let client = reqwest::blocking::Client::new();
|
|
// let resp = client.request(http_method, &url).headers(...).body(body).send()?;
|
|
// return Ok(ServiceResponse { status: resp.status().as_u16(), body: resp.text()?, ... });
|
|
|
|
// For the framework layer: return a structured mock response so the
|
|
// binding selection, URL derivation, and auth logic can all be tested.
|
|
Ok(ServiceResponse::ok(format!(
|
|
"{{\"service\":\"{}\",\"method\":\"{}\"}}",
|
|
request.service, request.method
|
|
)))
|
|
}
|
|
}
|