Skip to main content

ldp_protocol/
client.rs

1//! LDP HTTP client.
2//!
3//! Sends and receives LDP protocol messages over HTTP.
4//! Each message is an `LdpEnvelope` serialized as JSON.
5
6use crate::types::identity::LdpIdentityCard;
7use crate::types::messages::LdpEnvelope;
8use reqwest::Client;
9use serde_json::Value;
10use tracing::{debug, instrument};
11
12/// HTTP client for LDP protocol communication.
13#[derive(Debug, Clone)]
14pub struct LdpClient {
15    http: Client,
16}
17
18impl LdpClient {
19    /// Create a new LDP client.
20    pub fn new() -> Self {
21        Self {
22            http: Client::new(),
23        }
24    }
25
26    /// Create with a custom HTTP client.
27    pub fn with_http_client(http: Client) -> Self {
28        Self { http }
29    }
30
31    /// Send an LDP message and receive a response.
32    ///
33    /// Messages are posted to `{url}/ldp/messages` as JSON.
34    #[instrument(skip(self, message), fields(url = %url, msg_type = ?std::mem::discriminant(&message.body)))]
35    pub async fn send_message(
36        &self,
37        url: &str,
38        message: &LdpEnvelope,
39    ) -> Result<LdpEnvelope, String> {
40        let endpoint = format!("{}/ldp/messages", url.trim_end_matches('/'));
41
42        debug!(endpoint = %endpoint, "Sending LDP message");
43
44        let response = self
45            .http
46            .post(&endpoint)
47            .json(message)
48            .send()
49            .await
50            .map_err(|e| format!("LDP HTTP request failed: {e}"))?;
51
52        if !response.status().is_success() {
53            let status = response.status();
54            let body = response
55                .text()
56                .await
57                .unwrap_or_else(|_| "unable to read body".into());
58            return Err(format!("LDP request failed ({}): {}", status, body));
59        }
60
61        let envelope: LdpEnvelope = response
62            .json()
63            .await
64            .map_err(|e| format!("Failed to parse LDP response: {e}"))?;
65
66        Ok(envelope)
67    }
68
69    /// Fetch an LDP identity card from a remote delegate.
70    ///
71    /// Identity cards are served at `{url}/ldp/identity`.
72    #[instrument(skip(self), fields(url = %url))]
73    pub async fn fetch_identity_card(&self, url: &str) -> Result<LdpIdentityCard, String> {
74        let endpoint = format!("{}/ldp/identity", url.trim_end_matches('/'));
75
76        debug!(endpoint = %endpoint, "Fetching LDP identity card");
77
78        let response = self
79            .http
80            .get(&endpoint)
81            .send()
82            .await
83            .map_err(|e| format!("Failed to fetch identity card: {e}"))?;
84
85        if !response.status().is_success() {
86            let status = response.status();
87            return Err(format!("Identity card fetch failed ({})", status));
88        }
89
90        let card: LdpIdentityCard = response
91            .json()
92            .await
93            .map_err(|e| format!("Failed to parse identity card: {e}"))?;
94
95        Ok(card)
96    }
97
98    /// Fetch identity via .well-known convention.
99    ///
100    /// Tries `{url}/.well-known/ldp-identity` first, falls back to `{url}/ldp/identity`.
101    pub async fn fetch_identity_wellknown(&self, url: &str) -> Result<LdpIdentityCard, String> {
102        let wellknown = format!("{}/.well-known/ldp-identity", url.trim_end_matches('/'));
103
104        match self.http.get(&wellknown).send().await {
105            Ok(resp) if resp.status().is_success() => resp
106                .json()
107                .await
108                .map_err(|e| format!("Failed to parse identity: {e}")),
109            _ => self.fetch_identity_card(url).await,
110        }
111    }
112
113    /// Fetch raw capabilities from a remote delegate.
114    ///
115    /// Capabilities are served at `{url}/ldp/capabilities`.
116    #[instrument(skip(self), fields(url = %url))]
117    pub async fn fetch_capabilities(&self, url: &str) -> Result<Value, String> {
118        let endpoint = format!("{}/ldp/capabilities", url.trim_end_matches('/'));
119
120        let response = self
121            .http
122            .get(&endpoint)
123            .send()
124            .await
125            .map_err(|e| format!("Failed to fetch capabilities: {e}"))?;
126
127        if !response.status().is_success() {
128            let status = response.status();
129            return Err(format!("Capabilities fetch failed ({})", status));
130        }
131
132        response
133            .json()
134            .await
135            .map_err(|e| format!("Failed to parse capabilities: {e}"))
136    }
137}
138
139impl Default for LdpClient {
140    fn default() -> Self {
141        Self::new()
142    }
143}