From d6a8525274ccd96a5ce9540ea2ba1e463c84ff90 Mon Sep 17 00:00:00 2001 From: Vika Date: Tue, 20 Aug 2024 00:35:30 +0300 Subject: kittybox-util: 0.1.0 -> 0.2.0 Micropub types are now more coherent and gathered in one place. --- util/src/micropub.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 util/src/micropub.rs (limited to 'util/src/micropub.rs') 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=). +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 { + 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, + /// 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>, + /// 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>, + /// URL for a Media Endpoint, if any. + pub media_endpoint: Option, + /// Other unspecified keys, sometimes implementation-defined. + #[serde(flatten)] + pub other: HashMap +} + +#[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 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 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(), + )) + } +} + -- cgit 1.4.1