about summary refs log tree commit diff
path: root/util/src/micropub.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-08-20 00:35:30 +0300
committerVika <vika@fireburn.ru>2024-08-20 01:46:13 +0300
commitd6a8525274ccd96a5ce9540ea2ba1e463c84ff90 (patch)
tree9daa6fceda3f019629f99acb1b150882a5e151d4 /util/src/micropub.rs
parenta46e16258f83cc5939c568bba3a62ccc28114365 (diff)
kittybox-util: 0.1.0 -> 0.2.0
Micropub types are now more coherent and gathered in one place.
Diffstat (limited to 'util/src/micropub.rs')
-rw-r--r--util/src/micropub.rs173
1 files changed, 173 insertions, 0 deletions
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(),
+        ))
+    }
+}
+