use crate::{algorithms::Properties, traits::as_string_or_list};
use std::string::ToString;
pub mod action;
pub mod extension;
pub mod paging;
pub mod query;
pub mod known_properties;
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Parameters {
#[serde(
default,
alias = "mp-post-status",
alias = "post-status",
skip_serializing_if = "Option::is_none"
)]
pub status: Option<extension::PostStatus>,
#[serde(
default,
alias = "mp-category",
with = "as_string_or_list",
skip_serializing_if = "Vec::is_empty"
)]
pub category: Vec<String>,
#[serde(alias = "mp-slug", default, skip_serializing_if = "Option::is_none")]
pub slug: Option<String>,
#[serde(
default,
alias = "mp-channel",
alias = "channel",
with = "as_string_or_list",
skip_serializing_if = "Vec::is_empty"
)]
pub channels: Vec<String>,
#[serde(
default,
alias = "mp-syndicate-to",
with = "as_string_or_list",
skip_serializing_if = "Vec::is_empty"
)]
pub syndicate_to: Vec<String>,
#[serde(
default,
alias = "mp-destination",
skip_serializing_if = "Option::is_none"
)]
pub destination: Option<url::Url>,
#[serde(
default,
alias = "audience",
alias = "mp-audience",
with = "as_string_or_list",
skip_serializing_if = "Vec::is_empty"
)]
pub audience: Vec<String>,
#[serde(
alias = "visibility",
alias = "mp-visibility",
default,
skip_serializing_if = "Option::is_none"
)]
pub visibility: Option<extension::Visibility>,
}
impl TryFrom<Properties> for Parameters {
type Error = serde_json::Error;
fn try_from(props: Properties) -> Result<Self, Self::Error> {
let obj = props
.0
.into_iter()
.map(|(k, v)| {
(
k,
if let serde_json::Value::Array(ref va) = v {
if va.len() == 1 {
va.first().unwrap().clone()
} else {
v
}
} else {
v
},
)
})
.collect::<_>();
serde_json::from_value(serde_json::Value::Object(obj))
}
}
#[test]
fn deser_to_params() {
assert_eq!(
serde_json::from_value(serde_json::json!({
"category": ["tag", "tag2"]
}))
.ok(),
Some(Parameters {
category: vec!["tag".into(), "tag2".into()],
..Default::default()
}),
"deserializes from direct JSON"
);
}
#[derive(thiserror::Error, Debug, serde::Deserialize, PartialEq, Eq, Clone, serde::Serialize)]
#[error("Failed to process a request from the Micropub server: {error:?}")]
pub struct Error {
pub error: ErrorKind,
pub error_description: String,
#[serde(flatten, default)]
pub fields: serde_json::Map<String, serde_json::Value>,
}
impl Error {
pub fn missing_header(header_name: impl std::fmt::Display) -> Self {
Self {
error: ErrorKind::InvalidRequest,
error_description: format!("The header {} was missing in the response.", header_name),
fields: Default::default(),
}
}
pub fn bad_request(message: impl std::fmt::Display) -> Self {
Self {
error: ErrorKind::InvalidRequest,
error_description: format!("{}", message),
fields: Default::default(),
}
}
pub fn unexpected_status_code(
status: http::StatusCode,
accepted: &[http::StatusCode],
) -> Error {
Self {
error: ErrorKind::InvalidRequest,
error_description: format!(
"The response's status code was {} and not one of the expected {}",
status.as_u16(),
accepted
.iter()
.map(|s| s.as_u16().to_string())
.collect::<Vec<_>>()
.join(", ")
),
fields: Default::default(),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
Forbidden,
InsufficentScope,
Unauthorized,
InvalidRequest,
}
#[test]
fn error_from_json() {
assert_eq!(
Some(Error {
error: ErrorKind::Forbidden,
error_description: "Welp".to_string(),
fields: Default::default()
}),
serde_json::from_value(serde_json::json!({
"error": "forbidden",
"error_description": "Welp"
}))
.ok(),
"converts safely"
);
}
fn convert_error(e: crate::Error) -> crate::Error {
e
}
#[test]
fn deser_params() {
let params_json = serde_json::json!({
"slug": ["wow"]
});
let props: Properties = serde_json::from_value(params_json).expect("failed to deser json");
let params: Parameters = props.try_into().expect("failed to convert");
assert_eq!(params.slug, Some("wow".to_string()));
}