Compare commits
No commits in common. "aec5a01f2c681998097067ebf71f7d1536177736" and "17a0dfd21df593d1ae6a4c4edbd61223612243b5" have entirely different histories.
aec5a01f2c
...
17a0dfd21d
22 changed files with 124 additions and 629 deletions
|
@ -3,13 +3,13 @@
|
|||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-hostmeta/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-hostmeta/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-inbox/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate-webfinger/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate_database/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate_rd/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/acrate_rd/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["acrate_database", "acrate_rd", "acrate-nodeinfo", "acrate-webfinger"]
|
||||
members = ["acrate_database", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo", "acrate-webfinger"]
|
||||
|
|
|
@ -16,8 +16,9 @@ Federation database
|
|||
|
||||
### Libraries
|
||||
|
||||
- `acrate_database`: Database schema, migrations, and high level database-reliant structures for the [`acrate`] project
|
||||
- `acrate_rd`: Rust typing and utilities for the JSON and XML resource descriptior formats
|
||||
- `acrate-core`: Database stuff
|
||||
- `acrate-hostmeta`: RFC 6415 serde
|
||||
- `acrate-nodeinfo`: NodeInfo serde
|
||||
|
||||
### Binaries
|
||||
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
[package]
|
||||
name = "acrate_rd"
|
||||
version = "0.3.0"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
name = "acrate-hostmeta"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "Rust typing and utilities for the JSON and XML resource descriptior formats"
|
||||
repository = "https://forge.steffo.eu/unimore/tirocinio-canali-steffo-acrate"
|
||||
license = "EUPL-1.2"
|
||||
keywords = ["jrd", "xrd", "hostmeta", "webfinger", "resource-descriptor"]
|
||||
categories = ["web-programming"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
mime = "0.3.17"
|
||||
quick-xml = { version = "0.37.0", features = ["overlapped-lists", "serialize"] }
|
||||
reqwest = { version = "0.12.9", features = ["json", "stream"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
|
@ -1,18 +1,14 @@
|
|||
//! Definition and implementation of [`ResourceDescriptorJRD`].
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use thiserror::Error;
|
||||
use crate::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD, ResourceDescriptorXRD};
|
||||
|
||||
/// A resource descriptor in JRD format.
|
||||
/// A resource descriptor object in JRD format.
|
||||
///
|
||||
/// # Specification
|
||||
///
|
||||
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3>
|
||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
|
||||
///
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceDescriptorJRD {
|
||||
/// The resource this document refers to.
|
||||
|
@ -76,8 +72,7 @@ pub struct ResourceDescriptorLinkJRD {
|
|||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
|
||||
///
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "crate::utils::serde_mime_opt")]
|
||||
pub r#type: Option<mime::Mime>,
|
||||
pub r#type: Option<String>,
|
||||
|
||||
/// URI to the resource put in relation.
|
||||
///
|
||||
|
@ -128,7 +123,7 @@ impl ResourceDescriptorJRD {
|
|||
///
|
||||
/// ```
|
||||
/// # tokio_test::block_on(async {
|
||||
/// use acrate_rd::jrd::ResourceDescriptorJRD;
|
||||
/// use acrate_hostmeta::jrd::ResourceDescriptorJRD;
|
||||
///
|
||||
/// let client = reqwest::Client::new();
|
||||
/// let url: reqwest::Url = "https://junimo.party/.well-known/nodeinfo".parse()
|
||||
|
@ -173,25 +168,14 @@ impl ResourceDescriptorJRD {
|
|||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.ok_or(ContentTypeMissing)?;
|
||||
|
||||
log::trace!("Extracting media type from the `Content-Type` header...");
|
||||
let mime_type = content_type.to_str()
|
||||
.map_err(ContentTypeUnprintable)?;
|
||||
log::trace!("Extracting MIME type from the `Content-Type` header...");
|
||||
let mime_type = crate::utils::extract_mime_from_content_type(content_type)
|
||||
.ok_or(ContentTypeInvalid)?;
|
||||
|
||||
log::trace!("Parsing media type: {mime_type:?}");
|
||||
let mime_type = mime::Mime::from_str(mime_type)
|
||||
.map_err(ContentTypeInvalid)?;
|
||||
|
||||
log::trace!("Checking if media type is supported...");
|
||||
let mime_is_json = mime_type == mime::APPLICATION_JSON;
|
||||
log::trace!("Is media type application/json? {mime_is_json:?}");
|
||||
let mime_is_jrd =
|
||||
mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype() == "jrd"
|
||||
&& mime_type.suffix() == Some(mime::JSON);
|
||||
log::trace!("Is media type application/jrd+json? {mime_is_jrd:?}");
|
||||
if !(mime_is_json || mime_is_jrd) {
|
||||
log::trace!("Ensuring MIME type is acceptable for JRD parsing...");
|
||||
if !(mime_type == "application/json" || mime_type == "application/jrd+json") {
|
||||
log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing.");
|
||||
return Err(ContentTypeUnsupported);
|
||||
return Err(ContentTypeInvalid);
|
||||
}
|
||||
|
||||
log::trace!("Attempting to parse response as JSON...");
|
||||
|
@ -261,17 +245,9 @@ pub enum GetJRDError {
|
|||
#[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(mime::FromStrError),
|
||||
|
||||
/// 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 `Content-Type` header of the response is invalid.
|
||||
#[error("the Content-Type header of the response is invalid")]
|
||||
ContentTypeInvalid,
|
||||
|
||||
/// The document failed to be parsed as JSON by [`reqwest`].
|
||||
#[error("the document failed to be parsed as JSON")]
|
|
@ -1,4 +1,4 @@
|
|||
//! Rust typing and utilities for the JSON and XML resource descriptior formats.
|
||||
//! Resource descriptior handler.
|
||||
//!
|
||||
//! # Specification
|
||||
//!
|
||||
|
@ -8,4 +8,5 @@
|
|||
pub mod jrd;
|
||||
pub mod xrd;
|
||||
pub mod any;
|
||||
|
||||
mod utils;
|
8
acrate-hostmeta/src/utils.rs
Normal file
8
acrate-hostmeta/src/utils.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
/// Extract the MIME type from the value of the `Content-Type` header.
|
||||
pub fn extract_mime_from_content_type(value: &reqwest::header::HeaderValue) -> Option<String> {
|
||||
let value = value.to_str().ok()?;
|
||||
match value.split_once("; ") {
|
||||
None => Some(value.to_string()),
|
||||
Some((mime, _)) => Some(mime.to_string()),
|
||||
}
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
//! Definition and implementation of [`ResourceDescriptorXRD`].
|
||||
|
||||
use std::str::FromStr;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use thiserror::Error;
|
||||
use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD};
|
||||
|
@ -82,8 +79,7 @@ pub struct ResourceDescriptorLinkXRD {
|
|||
///
|
||||
#[serde(rename = "@type")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "crate::utils::serde_mime_opt")]
|
||||
pub r#type: Option<mime::Mime>,
|
||||
pub r#type: Option<String>,
|
||||
|
||||
/// URI to the resource put in relation.
|
||||
///
|
||||
|
@ -167,7 +163,7 @@ impl ResourceDescriptorXRD {
|
|||
///
|
||||
/// ```
|
||||
/// # tokio_test::block_on(async {
|
||||
/// use acrate_rd::xrd::ResourceDescriptorXRD;
|
||||
/// use acrate_hostmeta::xrd::ResourceDescriptorXRD;
|
||||
///
|
||||
/// let client = reqwest::Client::new();
|
||||
/// let url: reqwest::Url = "https://junimo.party/.well-known/host-meta".parse()
|
||||
|
@ -209,23 +205,14 @@ impl ResourceDescriptorXRD {
|
|||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.ok_or(ContentTypeMissing)?;
|
||||
|
||||
log::trace!("Extracting media type from the `Content-Type` header...");
|
||||
let mime_type = content_type.to_str()
|
||||
.map_err(ContentTypeUnprintable)?;
|
||||
log::trace!("Extracting MIME type from the `Content-Type` header...");
|
||||
let mime_type = crate::utils::extract_mime_from_content_type(content_type)
|
||||
.ok_or(ContentTypeInvalid)?;
|
||||
|
||||
log::trace!("Parsing media type: {mime_type:?}");
|
||||
let mime_type = mime::Mime::from_str(mime_type)
|
||||
.map_err(ContentTypeInvalid)?;
|
||||
|
||||
log::trace!("Checking if media type is supported...");
|
||||
let mime_is_xrd =
|
||||
mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype() == "xrd"
|
||||
&& mime_type.suffix() == Some(mime::XML);
|
||||
log::trace!("Is media type application/xrd+xml? {mime_is_xrd:?}");
|
||||
if !mime_is_xrd {
|
||||
log::trace!("Ensuring MIME type is acceptable for XRD parsing...");
|
||||
if mime_type != "application/xrd+xml" {
|
||||
log::error!("MIME type `{mime_type}` is not acceptable for XRD parsing.");
|
||||
return Err(ContentTypeUnsupported);
|
||||
return Err(ContentTypeInvalid)
|
||||
}
|
||||
|
||||
log::trace!("Attempting to parse response as text...");
|
||||
|
@ -303,17 +290,9 @@ pub enum GetXRDError {
|
|||
#[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(mime::FromStrError),
|
||||
|
||||
/// 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 `Content-Type` header of the response is invalid.
|
||||
#[error("the Content-Type header of the response is invalid")]
|
||||
ContentTypeInvalid,
|
||||
|
||||
/// The document failed to be decoded as text.
|
||||
#[error("the document failed to be decoded as text")]
|
|
@ -38,7 +38,7 @@ macro_rules! test_discover_hostmeta {
|
|||
let base: reqwest::Url = $url.parse()
|
||||
.expect("a valid URL");
|
||||
|
||||
let doc = acrate_rd::any::ResourceDescriptor::discover_hostmeta(&client, base)
|
||||
let doc = acrate_hostmeta::any::ResourceDescriptor::discover_hostmeta(&client, base)
|
||||
.await
|
||||
.expect("host-meta discovery to succeed");
|
||||
|
||||
|
@ -61,7 +61,7 @@ macro_rules! test_de_ser_jrd {
|
|||
|
||||
log::info!("Starting document: {:#?}", JRD_DOCUMENT);
|
||||
|
||||
let de: acrate_rd::jrd::ResourceDescriptorJRD = serde_json::from_str(JRD_DOCUMENT)
|
||||
let de: acrate_hostmeta::jrd::ResourceDescriptorJRD = serde_json::from_str(JRD_DOCUMENT)
|
||||
.expect("document to be deserialized successfully");
|
||||
|
||||
log::info!("Serialized document: {de:#?}");
|
||||
|
@ -85,10 +85,11 @@ macro_rules! test_de_ser_xrd {
|
|||
$(#[$tag])*
|
||||
fn $id() {
|
||||
init_log();
|
||||
let client = make_client();
|
||||
|
||||
log::info!("Starting document: {:#?}", XRD_DOCUMENT);
|
||||
|
||||
let de: acrate_rd::xrd::ResourceDescriptorXRD = quick_xml::de::from_str(XRD_DOCUMENT)
|
||||
let de: acrate_hostmeta::xrd::ResourceDescriptorXRD = quick_xml::de::from_str(XRD_DOCUMENT)
|
||||
.expect("document to be deserialized successfully");
|
||||
|
||||
log::info!("Serialized document: {de:#?}");
|
||||
|
@ -112,5 +113,4 @@ test_discover_hostmeta!(test_discover_hostmeta_threads_net, "https://threads.net
|
|||
test_discover_hostmeta!(test_discover_hostmeta_ngoa_giao_loan, "https://ngoa.giao.loan", ignore = "does not support host-meta");
|
||||
test_discover_hostmeta!(test_discover_hostmeta_hollo_social, "https://hollo.social", ignore = "does not support host-meta");
|
||||
|
||||
test_de_ser_jrd!(test_de_ser_jrd_sample_junimo_party, "samples/junimo_party.nodeinfo.jrd.json");
|
||||
test_de_ser_xrd!(test_de_ser_xrd_sample_junimo_party, "samples/junimo_party.host-meta.xrd.xml");
|
||||
test_de_ser_jrd!(test_de_ser_sample_junimo_party, "samples/junimo_party.nodeinfo.jrd.json");
|
12
acrate-inbox/Cargo.toml
Normal file
12
acrate-inbox/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "acrate-inbox"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.93"
|
||||
axum = "0.7.7"
|
||||
log = "0.4.22"
|
||||
micronfig = "0.3.0"
|
||||
pretty_env_logger = "0.5.0"
|
||||
tokio = { version = "1.41.1", features = ["macros", "net", "rt-multi-thread"] }
|
3
acrate-inbox/src/config.rs
Normal file
3
acrate-inbox/src/config.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
micronfig::config!(
|
||||
ACRATE_INBOX_BIND_ADDRESS: String,
|
||||
);
|
31
acrate-inbox/src/main.rs
Normal file
31
acrate-inbox/src/main.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use anyhow::Context;
|
||||
|
||||
mod config;
|
||||
mod route;
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<std::convert::Infallible> {
|
||||
pretty_env_logger::init();
|
||||
log::debug!("Logging initialized!");
|
||||
|
||||
log::trace!("Creating Axum router...");
|
||||
let app = axum::Router::new()
|
||||
.route("/inbox", axum::routing::post(route::inbox_handler));
|
||||
log::trace!("Axum router created successfully!");
|
||||
|
||||
log::trace!("Creating Tokio listener...");
|
||||
let bind_address = config::ACRATE_INBOX_BIND_ADDRESS();
|
||||
let listener = tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.context("failed to bind listener to address")?;
|
||||
log::trace!("Tokio listener bound to: {bind_address}");
|
||||
|
||||
log::debug!("Starting server...");
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.context("server exited with error")?;
|
||||
|
||||
log::error!("Server exited with no error, panicking.");
|
||||
panic!("server exited with no error");
|
||||
}
|
8
acrate-inbox/src/route.rs
Normal file
8
acrate-inbox/src/route.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
#[allow(unreachable_code)]
|
||||
pub async fn inbox_handler() {
|
||||
todo!("pre-validation hook");
|
||||
todo!("validate signature");
|
||||
todo!("post-validation hook");
|
||||
todo!("database storage");
|
||||
todo!("post-storage hook");
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "acrate_database"
|
||||
version = "0.3.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
edition = "2021"
|
||||
description = "Database schema and migrations for the acrate project"
|
||||
|
@ -15,7 +15,6 @@ diesel-async = { version = "0.5.1", features = ["postgres"] }
|
|||
diesel_migrations = { version = "2.2.0", optional = true }
|
||||
log = "0.4.22"
|
||||
micronfig = { version = "0.3.0", optional = true }
|
||||
mime = "0.3.17"
|
||||
pretty_env_logger = { version = "0.5.0", optional = true }
|
||||
uuid = "1.11.0"
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
UPDATE meta_subjects SET subject = 'invalid:subject.null' WHERE subject IS NULL;
|
||||
|
||||
ALTER TABLE meta_subjects ALTER subject SET NOT NULL;
|
||||
|
||||
ALTER TABLE meta_subjects ADD CONSTRAINT either_subject_or_redirect_not_null
|
||||
CHECK (
|
||||
(subject IS NOT NULL AND redirect IS NULL)
|
||||
OR
|
||||
(subject IS NULL AND redirect IS NOT NULL)
|
||||
);
|
|
@ -1,5 +0,0 @@
|
|||
ALTER TABLE meta_subjects DROP CONSTRAINT either_subject_or_redirect_not_null;
|
||||
|
||||
ALTER TABLE meta_subjects ALTER subject DROP NOT NULL;
|
||||
|
||||
UPDATE meta_subjects SET subject = NULL WHERE subject = 'invalid:subject.null';
|
|
@ -5,7 +5,5 @@
|
|||
/// Configured by `diesel.toml`.
|
||||
mod schema;
|
||||
|
||||
/// Tables related to web page metadata, XRD and JRD, `host-meta`, `WebFinger`, and so on.
|
||||
pub mod meta;
|
||||
|
||||
pub use diesel;
|
||||
pub use diesel_async;
|
||||
|
|
|
@ -1,415 +1,88 @@
|
|||
//! Tables related to web page metadata, XRD and JRD, `host-meta`, `WebFinger`, and so on.
|
||||
//!
|
||||
//! # Intended usage
|
||||
//!
|
||||
//! 1. An user agent requests an application for the descriptor for a given resource;
|
||||
//! 2. **The application queries the database using these structures to:**
|
||||
//! 1. [Determine if a descriptor should be returned, or if it should be handled by another application](MetaSubject);
|
||||
//! 2. [Determine which aliases the resource has](MetaAlias);
|
||||
//! 3. [Determine which properties the resource has](MetaProperty);
|
||||
//! 4. [Determine which links the resource has](MetaLink);
|
||||
//! 5. [Determine the titles of each link](MetaLinkTitle);
|
||||
//! 6. [Determine the properties of each link](MetaLinkProperty);
|
||||
//! 3. The application compiles the data in a single structure that the user agent supports
|
||||
//! 4. The application returns the data to the user agent
|
||||
//!
|
||||
//! # Matching
|
||||
//!
|
||||
//! Matching is performed for each single record separately, allowing for example a system administrator to define many separate subjects while globally defining a property for all the resources served.
|
||||
//!
|
||||
//! # Reference
|
||||
//!
|
||||
//! See [`acrate_rd`] for more information on the specification these structures are based on.
|
||||
//!
|
||||
//! # Used by
|
||||
//!
|
||||
//! - [`acrate_rdserver`]
|
||||
//!
|
||||
|
||||
use std::str::FromStr;
|
||||
use diesel::deserialize::FromSql;
|
||||
use diesel::{AsExpression, Associations, FromSqlRow, Identifiable, Insertable, IntoSql, PgTextExpressionMethods, QueryResult, Queryable, QueryableByName, Selectable, SelectableHelper, ExpressionMethods, BelongingToDsl};
|
||||
use diesel::pg::{Pg, PgConnection};
|
||||
use diesel::serialize::{Output, ToSql};
|
||||
use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable, pg::Pg};
|
||||
use diesel_async::AsyncPgConnection;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::schema;
|
||||
|
||||
|
||||
/// Wrapper to use [`mime::Mime`] with [`diesel`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression)]
|
||||
#[diesel(sql_type = diesel::sql_types::Text)]
|
||||
pub struct Mime(pub mime::Mime);
|
||||
|
||||
|
||||
/// A matchable record denoting the existence of a resource descriptor.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`MetaSubjectInsert`]
|
||||
///
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
|
||||
#[diesel(table_name = schema::meta_subjects)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaSubject {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The value the `subject` property should take when this record is matched.
|
||||
///
|
||||
/// If [`None`], its value is up for specification by the querying application.
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Where the querying application should redirect the user agent to when this record is matched.
|
||||
///
|
||||
/// If [`Some`], should always override everything else.
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaSubject`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = schema::meta_subjects)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaSubjectInsert {
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The value the `subject` property should take when this record is matched.
|
||||
///
|
||||
/// If [`None`], its value is up for specification by the querying application.
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Where the querying application should redirect the user agent to when this record is matched.
|
||||
///
|
||||
/// If [`Some`], should always override everything else.
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
/// A matchable record denoting an alias belonging to a subject.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`MetaAliasInsert`]
|
||||
///
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
|
||||
#[diesel(table_name = schema::meta_aliases)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaAlias {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The alias to **add** to the list of aliases of the resource.
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaAlias`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = schema::meta_aliases)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaAliasInsert {
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The alias to **add** to the list of aliases of the resource.
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
/// A matchable record denoting a link towards another resource someway related with the subject.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`MetaLinkInsert`]
|
||||
///
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
|
||||
#[diesel(table_name = schema::meta_links)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLink {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The relationship the link establishes between itself and the value of the link.
|
||||
pub rel: String,
|
||||
|
||||
/// The media type of the value of the link.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified.
|
||||
pub type_: Option<Mime>,
|
||||
|
||||
/// The URI to the document this property is linking the subject to.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified, for example if [`Self::template`] is [`Some`].
|
||||
pub type_: Option<String>,
|
||||
pub href: Option<String>,
|
||||
|
||||
/// The template to the document this property is linking the subject to.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified, for example if [`Self::href`] is [`Some`].
|
||||
pub template: Option<String>,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaLink`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = schema::meta_links)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLinkInsert {
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The relationship the link establishes between itself and the value of the link.
|
||||
pub rel: String,
|
||||
|
||||
/// The media type of the value of the link.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified.
|
||||
pub type_: Option<Mime>,
|
||||
|
||||
/// The URI to the document this property is linking the subject to.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified, for example if [`Self::template`] is [`Some`].
|
||||
pub href: Option<String>,
|
||||
|
||||
/// The template to the document this property is linking the subject to.
|
||||
///
|
||||
/// Can be [`None`] if it shouldn't be specified, for example if [`Self::href`] is [`Some`].
|
||||
pub template: Option<String>,
|
||||
}
|
||||
|
||||
/// A property that a [`MetaLink`] has.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`MetaLinkPropertyInsert`]
|
||||
///
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Associations)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)]
|
||||
#[diesel(belongs_to(MetaLink))]
|
||||
#[diesel(table_name = schema::meta_link_properties)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLinkProperty {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The [`MetaLink::id`] this record refers to.
|
||||
pub meta_link_id: Uuid,
|
||||
|
||||
/// The relationship the property establishes between the [`MetaLink`] and the [`MetaLinkProperty::value`].
|
||||
pub rel: String,
|
||||
|
||||
/// The value that the property has.
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaLinkProperty`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(belongs_to(MetaLink))]
|
||||
#[diesel(table_name = schema::meta_link_properties)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLinkPropertyInsert {
|
||||
|
||||
/// The [`MetaLink::id`] this record refers to.
|
||||
pub meta_link_id: Uuid,
|
||||
|
||||
/// The relationship the property establishes between the [`MetaLink`] and the [`MetaLinkProperty::value`].
|
||||
pub rel: String,
|
||||
|
||||
/// The value that the property has.
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// A title that a [`MetaLink`] has in a certain language.
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Associations)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)]
|
||||
#[diesel(belongs_to(MetaLink))]
|
||||
#[diesel(table_name = schema::meta_link_titles)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLinkTitle {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The [`MetaLink::id`] this record refers to.
|
||||
pub meta_link_id: Uuid,
|
||||
|
||||
/// The language of the title.
|
||||
pub language: String,
|
||||
|
||||
/// The actual contents of the title.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaLinkTitle`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(belongs_to(MetaLink))]
|
||||
#[diesel(table_name = schema::meta_link_titles)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaLinkTitleInsert {
|
||||
|
||||
/// The [`MetaLink::id`] this record refers to.
|
||||
pub meta_link_id: Uuid,
|
||||
|
||||
/// The language of the title.
|
||||
pub language: String,
|
||||
|
||||
/// The actual contents of the title.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// A matchable record denoting a property that a subject has.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`MetaPropertyInsert`]
|
||||
///
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)]
|
||||
#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
|
||||
#[diesel(table_name = schema::meta_properties)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaProperty {
|
||||
|
||||
/// The identity column of the record.
|
||||
pub id: Uuid,
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The relationship the property establishes between the [`MetaSubject`] and the [`MetaProperty::value`].
|
||||
pub rel: String,
|
||||
|
||||
/// The value that the property has.
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// An [`Insertable`] version of [`MetaLinkTitle`].
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = schema::meta_properties)]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct MetaPropertyInsert {
|
||||
|
||||
/// The document the record is valid for.
|
||||
pub document: String,
|
||||
|
||||
/// The [PostgreSQL ILIKE pattern] to match the search term against for the record to be valid.
|
||||
///
|
||||
/// [PostgreSQL ILIKE pattern]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE
|
||||
pub pattern: String,
|
||||
|
||||
/// The relationship the property establishes between the [`MetaSubject`] and the [`MetaProperty::value`].
|
||||
pub rel: String,
|
||||
|
||||
/// The value that the property has.
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// Allow [`diesel::sql_types::Text`] values to be parsed as [`Mime`].
|
||||
impl<DB> FromSql<diesel::sql_types::Text, DB> for Mime
|
||||
where
|
||||
DB: diesel::backend::Backend,
|
||||
String: FromSql<diesel::sql_types::Text, DB>,
|
||||
{
|
||||
fn from_sql(bytes: <DB as diesel::backend::Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||
log::trace!("Reading TEXT from the database...");
|
||||
let s = <String as FromSql<diesel::sql_types::Text, DB>>::from_sql(bytes)?;
|
||||
|
||||
log::trace!("Attempting to parse as a media type: {s:?}");
|
||||
let mime = mime::Mime::from_str(&s)?;
|
||||
|
||||
log::trace!("Successfully parsed media type: {mime:?}");
|
||||
Ok(Self(mime))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow [`diesel::sql_types::Text`] values to be written to with [`Mime`].
|
||||
impl<DB> ToSql<diesel::sql_types::Text, DB> for Mime
|
||||
where
|
||||
DB: diesel::backend::Backend,
|
||||
str: ToSql<diesel::sql_types::Text, DB>,
|
||||
{
|
||||
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
|
||||
log::trace!("Getting the essence of a media type to prepare for serialization...");
|
||||
let mime = self.0.essence_str();
|
||||
|
||||
log::trace!("Serializing media type as TEXT: {mime:?}");
|
||||
<str as ToSql<diesel::sql_types::Text, DB>>::to_sql(mime, out)
|
||||
}
|
||||
}
|
||||
|
||||
impl MetaSubject {
|
||||
/// Synchronously query the records matching the given document and resource.
|
||||
pub fn query_matching(conn: &mut PgConnection, doc: &str, resource: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
use diesel::RunQueryDsl;
|
||||
use schema::meta_subjects::dsl::*;
|
||||
|
||||
let document_is_equal = document.eq(doc);
|
||||
let resource_matches_pattern = resource.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
|
||||
meta_subjects
|
||||
.filter(document_is_equal)
|
||||
.filter(resource_matches_pattern)
|
||||
.select(Self::as_select())
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records matching the given document and resource.
|
||||
pub async fn aquery_matching(conn: &mut AsyncPgConnection, doc: &str, resource: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject_: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use schema::meta_subjects::dsl::*;
|
||||
|
||||
let document_is_equal = document.eq(doc);
|
||||
let resource_matches_pattern = resource.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
let subject_matches_pattern = subject_.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
|
||||
meta_subjects
|
||||
.filter(document_is_equal)
|
||||
.filter(resource_matches_pattern)
|
||||
.filter(subject_matches_pattern)
|
||||
.select(Self::as_select())
|
||||
.load(conn)
|
||||
.await
|
||||
|
@ -417,25 +90,8 @@ impl MetaSubject {
|
|||
}
|
||||
|
||||
impl MetaAlias {
|
||||
/// Synchronously query the records matching the given document and resource.
|
||||
pub fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
use diesel::RunQueryDsl;
|
||||
use schema::meta_aliases::dsl::*;
|
||||
|
||||
let document_is_equal = document.eq(doc);
|
||||
let subject_matches_pattern = subject.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
|
||||
meta_aliases
|
||||
.filter(document_is_equal)
|
||||
.filter(subject_matches_pattern)
|
||||
.select(Self::as_select())
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records matching the given document and resource.
|
||||
pub async fn aquery_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use schema::meta_aliases::dsl::*;
|
||||
|
||||
|
@ -452,25 +108,8 @@ impl MetaAlias {
|
|||
}
|
||||
|
||||
impl MetaLink {
|
||||
/// Synchronously query the records matching the given document and resource.
|
||||
pub async fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
use diesel::RunQueryDsl;
|
||||
use schema::meta_links::dsl::*;
|
||||
|
||||
let document_is_equal = document.eq(doc);
|
||||
let subject_matches_pattern = subject.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
|
||||
meta_links
|
||||
.filter(document_is_equal)
|
||||
.filter(subject_matches_pattern)
|
||||
.select(Self::as_select())
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records matching the given document and resource.
|
||||
pub async fn aquery_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use schema::meta_links::dsl::*;
|
||||
|
||||
|
@ -487,16 +126,8 @@ impl MetaLink {
|
|||
}
|
||||
|
||||
impl MetaLinkProperty {
|
||||
/// Synchronously query the records belonging to the given [`MetaLink`]s.
|
||||
pub fn query_by_link(conn: &mut PgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
use diesel::RunQueryDsl;
|
||||
|
||||
Self::belonging_to(links)
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records belonging to the given [`MetaLink`]s.
|
||||
pub async fn aquery_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
Self::belonging_to(links)
|
||||
|
@ -506,16 +137,8 @@ impl MetaLinkProperty {
|
|||
}
|
||||
|
||||
impl MetaLinkTitle {
|
||||
/// Synchronously query the records belonging to the given [`MetaLink`]s.
|
||||
pub fn query_by_link(conn: &mut PgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
use diesel::RunQueryDsl;
|
||||
|
||||
Self::belonging_to(links)
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records belonging to the given [`MetaLink`]s.
|
||||
pub async fn aquery_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
Self::belonging_to(links)
|
||||
|
@ -525,25 +148,8 @@ impl MetaLinkTitle {
|
|||
}
|
||||
|
||||
impl MetaProperty {
|
||||
/// Synchronously query the records matching the given document and resource.
|
||||
pub fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
use diesel::RunQueryDsl;
|
||||
use schema::meta_properties::dsl::*;
|
||||
|
||||
let document_is_equal = document.eq(doc);
|
||||
let subject_matches_pattern = subject.into_sql::<diesel::sql_types::Text>().ilike(pattern);
|
||||
|
||||
meta_properties
|
||||
.filter(document_is_equal)
|
||||
.filter(subject_matches_pattern)
|
||||
.select(Self::as_select())
|
||||
.load(conn)
|
||||
}
|
||||
|
||||
/// Asynchronously query the records matching the given document and resource.
|
||||
pub async fn aquery_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::QueryDsl;
|
||||
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use schema::meta_properties::dsl::*;
|
||||
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
//! Various utilities reused in the whole crate.
|
||||
|
||||
/// Module to use in `serde(with = ...)` to [`serde`] a [`mime::Mime`].
|
||||
#[allow(dead_code)]
|
||||
pub mod serde_mime {
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
use serde::de::{Error, Visitor};
|
||||
use serde::{Deserializer, Serializer};
|
||||
|
||||
pub struct MimeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MimeVisitor {
|
||||
type Value = mime::Mime;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a media type (MIME type)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
mime::Mime::from_str(v)
|
||||
.map_err(|_| E::custom("failed to parse media type"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, De>(deserializer: De) -> Result<<MimeVisitor as Visitor<'de>>::Value, De::Error>
|
||||
where
|
||||
De: Deserializer<'de>
|
||||
{
|
||||
let s = deserializer.deserialize_str(MimeVisitor)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn serialize<Ser>(data: mime::Mime, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
|
||||
where
|
||||
Ser: Serializer
|
||||
{
|
||||
let s = data.essence_str();
|
||||
serializer.serialize_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Module to use in `serde(with = ...)` to [`serde`] an [`Option`] of [`mime::Mime`].
|
||||
#[allow(dead_code)]
|
||||
pub mod serde_mime_opt {
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
use serde::de::{Error, Visitor};
|
||||
use serde::{Deserializer, Serializer};
|
||||
|
||||
pub struct MimeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MimeVisitor {
|
||||
type Value = Option<mime::Mime>;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("optionally, a media type (MIME type)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(
|
||||
Some(
|
||||
mime::Mime::from_str(v)
|
||||
.map_err(|_| E::custom("failed to parse media type"))?
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, De>(deserializer: De) -> Result<<MimeVisitor as Visitor<'de>>::Value, De::Error>
|
||||
where
|
||||
De: Deserializer<'de>
|
||||
{
|
||||
let s = deserializer.deserialize_str(MimeVisitor)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn serialize<Ser>(data: &Option<mime::Mime>, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
|
||||
where
|
||||
Ser: Serializer
|
||||
{
|
||||
match data {
|
||||
None => {
|
||||
serializer.serialize_none()
|
||||
}
|
||||
Some(data) => {
|
||||
let s = data.essence_str();
|
||||
serializer.serialize_str(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue