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(
74        &self,
75        url: &str,
76    ) -> Result<LdpIdentityCard, String> {
77        let endpoint = format!("{}/ldp/identity", url.trim_end_matches('/'));
78
79        debug!(endpoint = %endpoint, "Fetching LDP identity card");
80
81        let response = self
82            .http
83            .get(&endpoint)
84            .send()
85            .await
86            .map_err(|e| format!("Failed to fetch identity card: {e}"))?;
87
88        if !response.status().is_success() {
89            let status = response.status();
90            return Err(format!("Identity card fetch failed ({})", status));
91        }
92
93        let card: LdpIdentityCard = response
94            .json()
95            .await
96            .map_err(|e| format!("Failed to parse identity card: {e}"))?;
97
98        Ok(card)
99    }
100
101    /// Fetch identity via .well-known convention.
102    ///
103    /// Tries `{url}/.well-known/ldp-identity` first, falls back to `{url}/ldp/identity`.
104    pub async fn fetch_identity_wellknown(&self, url: &str) -> Result<LdpIdentityCard, String> {
105        let wellknown = format!("{}/.well-known/ldp-identity", url.trim_end_matches('/'));
106
107        match self.http.get(&wellknown).send().await {
108            Ok(resp) if resp.status().is_success() => {
109                resp.json().await.map_err(|e| format!("Failed to parse identity: {e}"))
110            }
111            _ => self.fetch_identity_card(url).await,
112        }
113    }
114
115    /// Fetch raw capabilities from a remote delegate.
116    ///
117    /// Capabilities are served at `{url}/ldp/capabilities`.
118    #[instrument(skip(self), fields(url = %url))]
119    pub async fn fetch_capabilities(&self, url: &str) -> Result<Value, String> {
120        let endpoint = format!("{}/ldp/capabilities", url.trim_end_matches('/'));
121
122        let response = self
123            .http
124            .get(&endpoint)
125            .send()
126            .await
127            .map_err(|e| format!("Failed to fetch capabilities: {e}"))?;
128
129        if !response.status().is_success() {
130            let status = response.status();
131            return Err(format!("Capabilities fetch failed ({})", status));
132        }
133
134        response
135            .json()
136            .await
137            .map_err(|e| format!("Failed to parse capabilities: {e}"))
138    }
139}
140
141impl Default for LdpClient {
142    fn default() -> Self {
143        Self::new()
144    }
145}