diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/Cargo.toml | 18 | ||||
-rw-r--r-- | util/src/error.rs | 95 | ||||
-rw-r--r-- | util/src/lib.rs | 16 | ||||
-rw-r--r-- | util/src/micropub.rs | 173 |
4 files changed, 188 insertions, 114 deletions
diff --git a/util/Cargo.toml b/util/Cargo.toml index 3d8327c..9a6558b 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -1,21 +1,23 @@ [package] name = "kittybox-util" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -fs = ["rand", "tokio", "tokio/fs"] +fs = ["dep:rand", "dep:tokio", "tokio/fs"] +sqlx = ["dep:sqlx"] +axum = ["dep:axum-core", "http"] +http = ["dep:http"] [dependencies] serde = { version = "^1.0.170", features = ["derive"] } serde_json = "^1.0.64" -axum-core = "^0.4.3" -http = "^1.0" async-trait = "^0.1.50" futures-util = "^0.3.14" uuid = "^1.3.3" +url = "2.5.2" [dependencies.rand] version = "^0.8.5" optional = true @@ -26,4 +28,10 @@ optional = true [dependencies.sqlx] version = "0.8" features = ["json"] -optional = true \ No newline at end of file +optional = true +[dependencies.axum-core] +version = "^0.4.3" +optional = true +[dependencies.http] +version = "^1.0" +optional = true diff --git a/util/src/error.rs b/util/src/error.rs deleted file mode 100644 index 1c95020..0000000 --- a/util/src/error.rs +++ /dev/null @@ -1,95 +0,0 @@ -use serde::{Deserialize, Serialize}; -use http::StatusCode; -use axum_core::response::{Response, IntoResponse}; - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] -#[serde(rename_all = "snake_case")] -/// Kinds of errors that can happen within a Micropub operation. -pub enum ErrorType { - /// An erroneous attempt to create something that already exists. - AlreadyExists, - /// Current user is expressly forbidden from performing this action. - Forbidden, - /// The Micropub server experienced an internal error. - InternalServerError, - /// The request was invalid or malformed. - InvalidRequest, - /// The provided OAuth2 scopes were insufficient to allow performing this action. - InvalidScope, - /// There was no token or other means of authorization in the request. - NotAuthorized, - /// Whatever was requested was not found. - NotFound, - /// The request payload was of a type unsupported by the Micropub endpoint. - UnsupportedMediaType, -} - -/// Representation of the Micropub API error. -#[derive(Serialize, Deserialize, Debug)] -pub struct MicropubError { - /// General kind of an error that occured. - pub error: ErrorType, - /// A human-readable error description intended for application developers. - // TODO use Cow<'static, str> to save on heap allocations - pub error_description: String, -} - -impl std::error::Error for MicropubError {} - -impl std::fmt::Display for MicropubError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Micropub error: ")?; - f.write_str(&self.error_description) - } -} - -impl From<serde_json::Error> for MicropubError { - fn from(err: serde_json::Error) -> Self { - use ErrorType::*; - Self { - error: InvalidRequest, - error_description: err.to_string(), - } - } -} - -impl MicropubError { - /// Create a new Micropub error. - pub fn new(error: ErrorType, error_description: &str) -> Self { - Self { - error, - error_description: error_description.to_owned(), - } - } -} - -impl From<&MicropubError> for StatusCode { - fn from(err: &MicropubError) -> Self { - use ErrorType::*; - match err.error { - AlreadyExists => StatusCode::CONFLICT, - Forbidden => StatusCode::FORBIDDEN, - InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, - InvalidRequest => StatusCode::BAD_REQUEST, - InvalidScope => StatusCode::UNAUTHORIZED, - NotAuthorized => StatusCode::UNAUTHORIZED, - NotFound => StatusCode::NOT_FOUND, - UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE, - } - } -} -impl From<MicropubError> for StatusCode { - fn from(err: MicropubError) -> Self { - (&err).into() - } -} - -impl IntoResponse for MicropubError { - fn into_response(self) -> Response { - IntoResponse::into_response(( - StatusCode::from(&self), - [("Content-Type", "application/json")], - serde_json::to_string(&self).unwrap(), - )) - } -} diff --git a/util/src/lib.rs b/util/src/lib.rs index c840e59..a919fd8 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -14,16 +14,6 @@ pub struct IndiewebEndpoints { pub microsub: Option<String>, } -/// Data structure representing a Micropub channel in the ?q=channels output. -#[derive(Serialize, Deserialize, PartialEq, Debug)] -#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] -pub struct MicropubChannel { - /// The channel's UID. It is usually also a publically accessible permalink URL. - pub uid: String, - /// The channel's user-friendly name used to recognize it in lists. - pub name: String, -} - #[derive(Debug, Default)] /// Common types of webmentions. pub enum MentionType { @@ -40,10 +30,6 @@ pub enum MentionType { Mention } -/// Common errors from the IndieWeb protocols that can be reused between modules. -pub mod error; -pub use error::{ErrorType, MicropubError}; - /// Common data-types useful in creating smart authentication systems. pub mod auth { #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -57,6 +43,8 @@ pub mod auth { } } +pub mod micropub; + /// A collection of traits for implementing a robust job queue. pub mod queue; diff --git a/util/src/micropub.rs b/util/src/micropub.rs new file mode 100644 index 0000000..1a3bcf3 --- /dev/null +++ b/util/src/micropub.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "kebab-case")] +/// Query types supported by a Micropub server (as in ?q=<type>). +pub enum QueryType { + /// Query source URL or recent posts (subject to extension implementation) + Source, + /// Query config (mandatory) + Config, + /// Query available channels + Channel, + /// Query available syndication destinations + SyndicateTo, + /// Query known categories/tags + Category, + /// Unsupported query type + // TODO: make this take a lifetime parameter for zero-copy deserialization if possible? + Unknown(std::borrow::Cow<'static, str>) +} + +/// Data structure representing a Micropub channel in the ?q=channels output. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct Channel { + /// The channel's UID, opaque to the client. + pub uid: String, + /// A human-friendly name. + pub name: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +/// A destination place to syndicate a post to. +/// +/// Commonly seen as part of [`?q=syndicate-to`][QueryType::SyndicateTo]. +pub struct SyndicationDestination { + /// The syndication destination's UID, opaque to the client. + pub uid: String, + /// A human-friendly name. + pub name: String +} + +fn default_q_list() -> Vec<QueryType> { + vec![QueryType::Config] +} +/// ?q=config output of a Micropub endpoint. +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Config { + /// Query types supported by this server. + #[serde(default = "default_q_list")] + pub q: Vec<QueryType>, + /// List of channels this server provides. + /// If [`None`][Option::None], you may want to consult [`?q=channel`][QueryType::Channel]. + #[serde(skip_serializing_if = "Option::is_none")] + pub channels: Option<Vec<Channel>>, + /// List of available syndication destinations. + /// If [`None`][Option::None], you may want to consult [`?q=channel`][QueryType::Channel]. + #[serde(skip_serializing_if = "Option::is_none")] + pub syndicate_to: Option<Vec<SyndicationDestination>>, + /// URL for a Media Endpoint, if any. + pub media_endpoint: Option<url::Url>, + /// Other unspecified keys, sometimes implementation-defined. + #[serde(flatten)] + pub other: HashMap<String, serde_json::Value> +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(rename_all = "snake_case")] +/// Kinds of errors that can happen within a Micropub operation. +pub enum ErrorKind { + /// An erroneous attempt to create something that already exists. + AlreadyExists, + /// Current user is expressly forbidden from performing this action. + Forbidden, + /// The Micropub server experienced an internal error. + InternalServerError, + /// The request was invalid or malformed. + InvalidRequest, + /// The provided OAuth2 scopes were insufficient to allow performing this action. + InvalidScope, + /// There was no token or other means of authorization in the request. + NotAuthorized, + /// Whatever was requested was not found. + NotFound, + /// The request payload was of a type unsupported by the Micropub endpoint. + UnsupportedMediaType, +} + +/// Representation of the Micropub API error. +#[derive(Serialize, Deserialize, Debug)] +pub struct Error { + /// General kind of an error that occured. + pub error: ErrorKind, + /// A human-readable error description intended for application developers. + // TODO use Cow<'static, str> to save on heap allocations + pub error_description: String, +} + +impl std::error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = if let serde_json::Value::String(s) = serde_json::to_value(&self.error).unwrap() { + s + } else { + unreachable!() + }; + + f.write_str(&s)?; + f.write_str(" (")?; + f.write_str(&self.error_description)?; + f.write_str(")") + } +} + +impl From<serde_json::Error> for Error { + fn from(err: serde_json::Error) -> Self { + use ErrorKind::*; + Self { + error: InvalidRequest, + error_description: err.to_string(), + } + } +} + +impl Error { + /// Create a new Micropub error. + pub fn new(error: ErrorKind, error_description: &str) -> Self { + Self { + error, + error_description: error_description.to_owned(), + } + } +} + +#[cfg(feature = "http")] +impl From<&Error> for http::StatusCode { + fn from(err: &Error) -> Self { + use ErrorKind::*; + match err.error { + AlreadyExists => http::StatusCode::CONFLICT, + Forbidden => http::StatusCode::FORBIDDEN, + InternalServerError => http::StatusCode::INTERNAL_SERVER_ERROR, + InvalidRequest => http::StatusCode::BAD_REQUEST, + InvalidScope => http::StatusCode::UNAUTHORIZED, + NotAuthorized => http::StatusCode::UNAUTHORIZED, + NotFound => http::StatusCode::NOT_FOUND, + UnsupportedMediaType => http::StatusCode::UNSUPPORTED_MEDIA_TYPE, + } + } +} + +#[cfg(feature = "http")] +impl From<Error> for http::StatusCode { + fn from(err: Error) -> Self { + (&err).into() + } +} + +#[cfg(feature = "axum")] +impl axum_core::response::IntoResponse for Error { + fn into_response(self) -> axum_core::response::Response { + axum_core::response::IntoResponse::into_response(( + http::StatusCode::from(&self), + [("Content-Type", "application/json")], + serde_json::to_string(&self).unwrap(), + )) + } +} + |