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(Serialize, Deserialize, Debug)] #[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(), )) } }