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(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<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(),
))
}
}