use std::cmp::Ordering;
use reqwest::Url;
use serde::de::{DeserializeOwned, Error as DeserError};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use crate::ApiVersion;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct Link {
pub href: Url,
pub rel: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ref {
pub id: String,
pub links: Vec<Link>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct IdAndName {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq, Hash)]
pub enum IdOrName {
#[serde(rename = "id")]
Id(String),
#[serde(rename = "name")]
Name(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VersionStatus {
Current,
Supported,
Deprecated,
Unknown,
}
impl Default for VersionStatus {
fn default() -> VersionStatus {
VersionStatus::Unknown
}
}
impl VersionStatus {
#[inline]
pub fn is_stable(&self) -> bool {
!matches!(self, VersionStatus::Deprecated)
}
}
impl<T> From<T> for VersionStatus
where
T: Into<String>,
{
fn from(value: T) -> VersionStatus {
match value.into().to_uppercase().as_ref() {
"CURRENT" => VersionStatus::Current,
"SUPPORTED" | "STABLE" => VersionStatus::Supported,
"DEPRECATED" => VersionStatus::Deprecated,
_ => VersionStatus::Unknown,
}
}
}
impl<'de> Deserialize<'de> for VersionStatus {
fn deserialize<D>(deserializer: D) -> Result<VersionStatus, D::Error>
where
D: Deserializer<'de>,
{
let value: String = Deserialize::deserialize(deserializer)?;
Ok(value.into())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Version {
pub id: ApiVersion,
#[serde(default)]
pub links: Vec<Link>,
#[serde(deserialize_with = "empty_as_default", default)]
pub status: VersionStatus,
#[serde(deserialize_with = "empty_as_default", default)]
pub version: Option<ApiVersion>,
#[serde(deserialize_with = "empty_as_default", default)]
pub min_version: Option<ApiVersion>,
}
impl Version {
#[inline]
pub fn is_stable(&self) -> bool {
self.status.is_stable()
}
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Version {}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
pub fn empty_as_default<'de, D, T>(des: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned + Default,
{
let value = Value::deserialize(des)?;
match value {
Value::String(ref s) if s.is_empty() => Ok(T::default()),
_ => serde_json::from_value(value).map_err(D::Error::custom),
}
}
pub fn empty_map_as_default<'de, D, T>(des: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned + Default,
{
let value = Value::deserialize(des)?;
match value {
Value::Object(ref s) if s.is_empty() => Ok(T::default()),
_ => serde_json::from_value(value).map_err(D::Error::custom),
}
}
#[cfg(test)]
pub(crate) mod test {
use serde::{Deserialize, Serialize};
use serde_json;
use super::*;
use crate::ApiVersion;
pub fn compare<T: Serialize>(sample: &str, value: T) {
let converted: Value = serde_json::from_str(sample).unwrap();
let result = serde_json::to_value(value).unwrap();
assert_eq!(result, converted);
}
#[derive(Debug, Deserialize)]
struct Custom(bool);
#[derive(Debug, Deserialize)]
struct EmptyAsDefault {
#[serde(deserialize_with = "empty_as_default")]
number: u8,
#[serde(deserialize_with = "empty_as_default")]
vec: Vec<String>,
#[serde(deserialize_with = "empty_as_default")]
opt: Option<Custom>,
#[serde(deserialize_with = "empty_as_default")]
string: Option<String>,
}
#[test]
fn test_empty_as_default_with_values() {
let s = "{\"number\": 42, \"vec\": [\"value\"], \"opt\": true, \"string\": \"value\"}";
let r: EmptyAsDefault = serde_json::from_str(s).unwrap();
assert_eq!(r.number, 42);
assert_eq!(r.vec, vec!["value".to_string()]);
assert!(r.opt.unwrap().0);
assert_eq!(r.string.unwrap(), "value");
}
#[test]
fn test_empty_as_default_with_empty_string() {
let s = "{\"number\": \"\", \"vec\": \"\", \"opt\": \"\", \"string\": \"\"}";
let r: EmptyAsDefault = serde_json::from_str(s).unwrap();
assert_eq!(r.number, 0);
assert!(r.vec.is_empty());
assert!(r.opt.is_none());
assert!(r.string.is_none());
}
#[test]
fn test_version_current_is_stable() {
let stable = Version {
id: ApiVersion(2, 0),
links: Vec::new(),
status: VersionStatus::Current,
version: None,
min_version: None,
};
assert!(stable.is_stable());
}
#[test]
fn test_version_supported_is_stable() {
let stable = Version {
id: ApiVersion(2, 0),
links: Vec::new(),
status: VersionStatus::Supported,
version: None,
min_version: None,
};
assert!(stable.is_stable());
}
#[test]
fn test_version_no_status_is_stable() {
let stable = Version {
id: ApiVersion(2, 0),
links: Vec::new(),
status: VersionStatus::Unknown,
version: None,
min_version: None,
};
assert!(stable.is_stable());
}
#[test]
fn test_version_deprecated_is_not_stable() {
let unstable = Version {
id: ApiVersion(2, 0),
links: Vec::new(),
status: VersionStatus::Deprecated,
version: None,
min_version: None,
};
assert!(!unstable.is_stable());
}
const COMPUTE_ONE: &str = r#"{
"status": "CURRENT",
"updated": "2013-07-23T11:33:21Z",
"links": [
{
"href": "https://example.org:13774/v2.1/",
"rel": "self"
},
{
"href": "http://docs.openstack.org/",
"type": "text/html",
"rel": "describedby"
}
],
"min_version": "2.1",
"version": "2.42",
"media-types": [
{
"base": "application/json",
"type": "application/vnd.openstack.compute+json;version=2.1"
}
],
"id": "v2.1"
}"#;
#[test]
fn test_version_parse() {
let version: Version = serde_json::from_str(COMPUTE_ONE).unwrap();
assert_eq!(version.id, ApiVersion(2, 1));
}
#[test]
fn test_version_status_from_string() {
assert_eq!(VersionStatus::from("SUPPORTED"), VersionStatus::Supported);
assert_eq!(VersionStatus::from("Stable"), VersionStatus::Supported);
assert_eq!(VersionStatus::from("CURRENT"), VersionStatus::Current);
assert_eq!(VersionStatus::from("deprecated"), VersionStatus::Deprecated);
assert_eq!(VersionStatus::from("banana!"), VersionStatus::Unknown);
}
#[test]
fn test_version_status_parse() {
assert_eq!(
serde_json::from_str::<VersionStatus>("\"SUPPORTED\"").unwrap(),
VersionStatus::Supported
);
}
#[derive(Debug, Deserialize)]
struct TestStruct {
pub answer: u8,
}
#[derive(Debug, Deserialize)]
struct TestDeserialize {
#[serde(deserialize_with = "empty_map_as_default")]
non_empty_map: Option<TestStruct>,
#[serde(deserialize_with = "empty_map_as_default")]
empty_map_as_none: Option<TestStruct>,
#[serde(deserialize_with = "empty_map_as_default")]
empty_map_as_none_is_null: Option<TestStruct>,
#[serde(default, deserialize_with = "empty_map_as_default")]
empty_map_as_none_with_default: Option<TestStruct>,
#[serde(deserialize_with = "empty_as_default")]
empty_string_as_none: Option<TestStruct>,
}
#[test]
fn test_deserialize() {
let json = r#"{
"empty_map_as_none": {},
"non_empty_map": {"answer": 42},
"empty_map_as_none_is_null": null,
"empty_string_as_none": ""
}"#;
let result: TestDeserialize = serde_json::from_str(json).unwrap();
assert_eq!(result.non_empty_map.unwrap().answer, 42);
assert!(result.empty_map_as_none.is_none());
assert!(result.empty_map_as_none_is_null.is_none());
assert!(result.empty_map_as_none_with_default.is_none());
assert!(result.empty_string_as_none.is_none());
}
}