//! 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 { 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 { let fields: Vec = 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 { 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 ))) } }