//! Definition and implementation of [`ResourceDescriptorJRD`]. use std::collections::HashMap; use mediatype::MediaTypeBuf; use serde::{Serialize, Deserialize}; use thiserror::Error; use crate::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD, ResourceDescriptorXRD}; /// A resource descriptor in JRD format. /// /// # Specification /// /// - /// - /// #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceDescriptorJRD { /// The resource this document refers to. /// /// # Specification /// /// - /// #[serde(skip_serializing_if = "Option::is_none")] pub subject: Option, /// Other names the resource described by this document can be referred to. /// /// # Specification /// /// - /// #[serde(default)] pub aliases: Vec, /// Additional information about the resource described by this document. /// /// # Specification /// /// - /// #[serde(default)] pub properties: HashMap>, /// Links established between the [`Self::subject`] and other resources. /// /// # Specification /// /// - /// - /// #[serde(default)] pub links: Vec, } /// A link element, which puts the subject resource in relation with another. /// /// # Specification /// /// - /// #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceDescriptorLinkJRD { /// The kind of relation established by the subject with the attached resource. /// /// # Specification /// /// - /// pub rel: String, /// The media type of the resource put in relation. /// /// # Specification /// /// - /// #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option, /// URI to the resource put in relation. /// /// # Specification /// /// - /// #[serde(skip_serializing_if = "Option::is_none")] pub href: Option, /// Titles of the resource put in relation in various languages. /// /// # Specification /// /// - /// #[serde(default)] pub titles: HashMap, /// Additional information about the resource put in relation. /// /// # Specification /// /// - /// #[serde(default)] pub properties: HashMap>, /// Template to fill to get the URL to resource-specific information. /// /// # Specification /// /// - /// #[serde(skip_serializing_if = "Option::is_none")] pub template: Option, } impl ResourceDescriptorJRD { /// Get a [`ResourceDescriptorJRD`] from an URL. /// /// # Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// /// # Examples /// /// ``` /// # tokio_test::block_on(async { /// use acrate_rd::jrd::ResourceDescriptorJRD; /// /// let client = reqwest::Client::new(); /// let url: reqwest::Url = "https://junimo.party/.well-known/nodeinfo".parse() /// .expect("URL to be valid"); /// /// let rd = ResourceDescriptorJRD::get(&client, url) /// .await /// .expect("JRD to be processed correctly"); /// # }) /// ``` /// pub async fn get(client: &reqwest::Client, url: reqwest::Url) -> Result { use GetJRDError::*; log::debug!("Getting JRD document at: {url}"); log::trace!("Building request..."); let request = { log::trace!("Creating new request..."); let mut request = reqwest::Request::new(reqwest::Method::GET, url); log::trace!("Setting request headers..."); let headers = request.headers_mut(); log::trace!("Setting `Accept: application/jrd+json, application/json`..."); let _ = headers.insert( reqwest::header::ACCEPT, "application/jrd+json, application/json".parse().unwrap() ); request }; log::trace!("Sending request..."); let response = client.execute(request) .await .map_err(Request)?; log::trace!("Checking `Content-Type` of the response..."); let content_type = response .headers() .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; log::trace!("Extracting media type from the `Content-Type` header: {content_type:?}"); let media_type: MediaTypeBuf = content_type .to_str() .map_err(ContentTypeUnprintable)? .parse() .map_err(ContentTypeInvalid)?; log::trace!("Checking if media type is supported: {media_type:?}"); let mime_is_json = media_type.essence().eq(&"application/json".parse::().unwrap()); log::trace!("Is media type `application/json`? {mime_is_json:?}"); let mime_is_jrd = media_type.essence().eq(&"application/jrd+json".parse::().unwrap()); log::trace!("Is media type `application/jrd+json`? {mime_is_jrd:?}"); if !(mime_is_json || mime_is_jrd) { log::error!("Media type `{media_type}` is not acceptable for JRD parsing."); return Err(ContentTypeUnsupported); } log::trace!("Attempting to parse response as JSON..."); let data = response.json::() .await .map_err(Parse)?; Ok(data) } } impl From for ResourceDescriptorJRD { fn from(value: ResourceDescriptorXRD) -> Self { Self { subject: value.subject, aliases: value.aliases, properties: value.properties.into_iter() .map(From::from) .collect(), links: value.links.into_iter() .map(From::from) .collect(), } } } impl From for ResourceDescriptorLinkJRD { fn from(value: ResourceDescriptorLinkXRD) -> Self { Self { rel: value.rel, r#type: value.r#type, href: value.href, titles: HashMap::from_iter( value.titles.into_iter() .map(From::from) ), properties: HashMap::from_iter( value.properties.into_iter() .map(From::from) ), template: value.template, } } } impl From for (String, Option) { fn from(value: ResourceDescriptorPropertyXRD) -> Self { (value.rel, value.value) } } impl From for (String, String) { fn from(value: ResourceDescriptorTitleXRD) -> Self { (value.language, value.value) } } /// Error occurred during [`ResourceDescriptor::get_jrd`]. #[derive(Debug, Error)] pub enum GetJRDError { /// The HTTP request failed. #[error("the HTTP request failed")] Request(reqwest::Error), /// The `Content-Type` header of the response is missing. #[error("the Content-Type header of the response is missing")] ContentTypeMissing, /// The `Content-Type` header of the response can't be converted to a [`str`]. #[error("the Content-Type header of the response cannot be converted to a &str")] ContentTypeUnprintable(reqwest::header::ToStrError), /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. #[error("the Content-Type header of the response is not a valid media type")] ContentTypeInvalid(mediatype::MediaTypeError), /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. #[error("the Content-Type header of the response is not a supported media type")] ContentTypeUnsupported, /// The document failed to be parsed as JSON by [`reqwest`]. #[error("the document failed to be parsed as JSON")] Parse(reqwest::Error), }