diff --git a/acrate_database/Cargo.toml b/acrate_database/Cargo.toml index 89bab9d..2fbdcd1 100644 --- a/acrate_database/Cargo.toml +++ b/acrate_database/Cargo.toml @@ -15,6 +15,7 @@ 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" diff --git a/acrate_database/src/lib.rs b/acrate_database/src/lib.rs index 17af59e..fe4a7c2 100644 --- a/acrate_database/src/lib.rs +++ b/acrate_database/src/lib.rs @@ -5,5 +5,7 @@ /// 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; diff --git a/acrate_database/src/meta.rs b/acrate_database/src/meta.rs index 95d479b..ee23e19 100644 --- a/acrate_database/src/meta.rs +++ b/acrate_database/src/meta.rs @@ -1,88 +1,410 @@ -use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable, pg::Pg}; +//! 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_async::AsyncPgConnection; use uuid::Uuid; + use super::schema; -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +/// 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)] #[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, + + /// 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, } -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +/// 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, + + /// 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, +} + +/// A matchable record denoting an alias belonging to a subject. +/// +/// # See also +/// +/// - [`MetaAliasInsert`] +/// +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable)] #[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, } -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +/// 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)] #[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, - pub type_: Option, + + /// The media type of the value of the link. + /// + /// Can be [`None`] if it shouldn't be specified. + pub type_: Option, + + /// 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, + + /// 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, } -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)] +/// 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, + + /// 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, + + /// 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, +} + +/// A property that a [`MetaLink`] has. +/// +/// # See also +/// +/// - [`MetaLinkPropertyInsert`] +/// +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, 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, } -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)] +/// 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, +} + +/// A title that a [`MetaLink`] has in a certain language. +#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, 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, } -#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)] +/// 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)] #[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, } +/// 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, +} + +/// Allow [`diesel::sql_types::Text`] values to be parsed as [`Mime`]. +impl FromSql for Mime + where + DB: diesel::backend::Backend, + String: FromSql, +{ + fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + + let mime = mime::Mime::from_str(&s)?; + + Ok(Self(mime)) + } +} + +/// Allow [`diesel::sql_types::Text`] values to be written to with [`Mime`]. +impl ToSql for Mime + where + DB: diesel::backend::Backend, + str: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + let mime = self.0.essence_str(); + + >::to_sql(mime, out) + } +} + impl MetaSubject { - pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject_: &str) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records matching the given document and resource. + pub fn query_matching(conn: &mut PgConnection, doc: &str, resource: &str) -> QueryResult> { + 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::().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> { + use diesel::QueryDsl; use diesel_async::RunQueryDsl; use schema::meta_subjects::dsl::*; let document_is_equal = document.eq(doc); - let subject_matches_pattern = subject_.into_sql::().ilike(pattern); + let resource_matches_pattern = resource.into_sql::().ilike(pattern); meta_subjects .filter(document_is_equal) - .filter(subject_matches_pattern) + .filter(resource_matches_pattern) .select(Self::as_select()) .load(conn) .await @@ -90,8 +412,25 @@ impl MetaSubject { } impl MetaAlias { - pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records matching the given document and resource. + pub fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult> { + 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::().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> { + use diesel::QueryDsl; use diesel_async::RunQueryDsl; use schema::meta_aliases::dsl::*; @@ -108,8 +447,25 @@ impl MetaAlias { } impl MetaLink { - pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records matching the given document and resource. + pub async fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult> { + 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::().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> { + use diesel::QueryDsl; use diesel_async::RunQueryDsl; use schema::meta_links::dsl::*; @@ -126,8 +482,16 @@ impl MetaLink { } impl MetaLinkProperty { - pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records belonging to the given [`MetaLink`]s. + pub fn query_by_link(conn: &mut PgConnection, links: &[MetaLink]) -> QueryResult> { + 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> { use diesel_async::RunQueryDsl; Self::belonging_to(links) @@ -137,8 +501,16 @@ impl MetaLinkProperty { } impl MetaLinkTitle { - pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records belonging to the given [`MetaLink`]s. + pub fn query_by_link(conn: &mut PgConnection, links: &[MetaLink]) -> QueryResult> { + 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> { use diesel_async::RunQueryDsl; Self::belonging_to(links) @@ -148,8 +520,25 @@ impl MetaLinkTitle { } impl MetaProperty { - pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> { - use diesel::prelude::*; + /// Synchronously query the records matching the given document and resource. + pub fn query_matching(conn: &mut PgConnection, doc: &str, subject: &str) -> QueryResult> { + 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::().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> { + use diesel::QueryDsl; use diesel_async::RunQueryDsl; use schema::meta_properties::dsl::*;