about summary refs log blame commit diff
path: root/util/src/micropub.rs
blob: ce9e2d70986db5ddd488d3050e4665e977b2e69f (plain) (tree)














































                                                                                           
                                        


























































































































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