use crate::common::helpers::{Context, PushError, ValidateWithContext, validate_required_string};
use crate::v3_1::spec::Spec;
use crate::validation::Options;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Server {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<BTreeMap<String, ServerVariable>>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ServerVariable {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "enum")]
enum_values: Option<Vec<String>>,
default: String,
description: Option<String>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Server {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.url, ctx, format!("{path}.url"));
let mut visited = HashSet::<String>::new();
if let Some(variables) = &self.variables {
for (name, variable) in variables {
variable.validate_with_context(ctx, format!("{path}.variables[{name}]"));
visited.insert(name.clone());
}
};
let re = Regex::new(r"\{([a-zA-Z0-9.\-_]+)}").unwrap();
for (_, [name]) in re.captures_iter(&self.url).map(|c| c.extract()) {
if !visited.remove(name) {
ctx.error(
path.clone(),
format_args!(".url: `{name}` is not defined in `variables`"),
);
}
}
if !ctx.is_option(Options::IgnoreUnusedServerVariables) {
for name in visited {
ctx.error(
path.clone(),
format_args!(".variables[{name}]: unused in `url`"),
);
}
}
}
}
impl ValidateWithContext<Spec> for ServerVariable {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.default, ctx, format!("{path}.default"));
if let Some(enum_values) = &self.enum_values
&& !enum_values.contains(&self.default)
{
ctx.error(
path,
format!(
".default: `{}` must be in enum values: {:?}",
self.default, enum_values,
),
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::Options;
use enumset::EnumSet;
#[test]
fn test_server_variable_deserialize() {
assert_eq!(
serde_json::from_value::<ServerVariable>(serde_json::json!({
"enum": [
"8443",
"443"
],
"default": "8443",
"description": "the port to serve HTTP traffic on"
}))
.unwrap(),
ServerVariable {
enum_values: Some(vec![String::from("8443"), String::from("443")]),
default: String::from("8443"),
description: Some(String::from("the port to serve HTTP traffic on")),
..Default::default()
},
"deserialize",
);
}
#[test]
fn test_server_variable_serialize() {
assert_eq!(
serde_json::to_value(ServerVariable {
enum_values: Some(vec![String::from("8443"), String::from("443")]),
default: String::from("8443"),
description: Some(String::from("the port to serve HTTP traffic on")),
..Default::default()
})
.unwrap(),
serde_json::json!({
"enum": [
"8443",
"443"
],
"default": "8443",
"description": "the port to serve HTTP traffic on"
}),
"serialize",
);
}
#[test]
fn test_server_variable_validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
ServerVariable {
enum_values: Some(vec![String::from("8443"), String::from("443")]),
default: String::from("8443"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("serverVariable"));
assert_eq!(ctx.errors.len(), 0, "no errors: {:?}", ctx.errors);
ServerVariable {
enum_values: Some(vec![String::from("443")]),
default: String::from("8443"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("serverVariable"));
assert_eq!(ctx.errors.len(), 1, "one error: {:?}", ctx.errors);
assert_eq!(
ctx.errors[0],
"serverVariable.default: `8443` must be in enum values: [\"443\"]",
);
}
#[test]
fn test_server_serialize() {
assert_eq!(
serde_json::to_value(Server {
url: String::from("https://development.gigantic-server.com/v1"),
..Default::default()
})
.unwrap(),
serde_json::json!({
"url": "https://development.gigantic-server.com/v1",
}),
"serialize with url only",
);
assert_eq!(
serde_json::to_value(Server {
url: String::from("https://development.gigantic-server.com/v1"),
description: Some(String::from("Development server")),
..Default::default()
})
.unwrap(),
serde_json::json!({
"url": "https://development.gigantic-server.com/v1",
"description": "Development server",
}),
"serialize with url and description",
);
assert_eq!(
serde_json::to_value(Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
description: Some(String::from("Development server")),
variables: Some({
let mut vars = BTreeMap::<String, ServerVariable>::new();
vars.insert(
String::from("username"),
ServerVariable {
default: String::from("demo"),
description: Some(String::from(
"this value is assigned by the service provider, in this example `gigantic-server.com`"
)),
..Default::default()
},
);
vars.insert(
String::from("port"),
ServerVariable {
enum_values: Some(vec![String::from("8443"), String::from("443")]),
default: String::from("8443"),
description: Some(String::from("the port to serve HTTP traffic on")),
..Default::default()
},
);
vars.insert(
String::from("basePath"),
ServerVariable {
default: String::from("v2"),
description: Some(String::from(
"open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`"
)),
..Default::default()
},
);
vars
}),
..Default::default()
})
.unwrap(),
serde_json::json!({
"url": "https://{username}.gigantic-server.com:{port}/{basePath}",
"description": "Development server",
"variables": {
"username": {
"default": "demo",
"description": "this value is assigned by the service provider, in this example `gigantic-server.com`"
},
"port": {
"enum": [
"8443",
"443"
],
"default": "8443",
"description": "the port to serve HTTP traffic on"
},
"basePath": {
"default": "v2",
"description": "open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`"
}
}
}),
"serialize with url, description and variables",
);
}
#[test]
fn test_server_deserialize() {
assert_eq!(
serde_json::from_value::<Server>(serde_json::json!({
"url": "https://development.gigantic-server.com/v1",
}))
.unwrap(),
Server {
url: String::from("https://development.gigantic-server.com/v1"),
..Default::default()
},
"deserialize with url only",
);
assert_eq!(
serde_json::from_value::<Server>(serde_json::json!({
"url": "https://development.gigantic-server.com/v1",
"description": "Development server",
}))
.unwrap(),
Server {
url: String::from("https://development.gigantic-server.com/v1"),
description: Some(String::from("Development server")),
..Default::default()
},
"deserialize with url and description",
);
assert_eq!(
serde_json::from_value::<Server>(serde_json::json!({
"url": "https://{username}.gigantic-server.com:{port}/{basePath}",
"description": "Development server",
"variables": {
"username": {
"default": "demo",
"description": "this value is assigned by the service provider, in this example `gigantic-server.com`"
},
"port": {
"enum": [
"8443",
"443"
],
"default": "8443",
"description": "the port to serve HTTP traffic on"
},
"basePath": {
"default": "v2",
"description": "open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`"
}
}
})).unwrap(),
Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
description: Some(String::from("Development server")),
variables: Some({
let mut vars = BTreeMap::<String, ServerVariable>::new();
vars.insert(
String::from("username"),
ServerVariable {
default: String::from("demo"),
description: Some(String::from(
"this value is assigned by the service provider, in this example `gigantic-server.com`"
)),
..Default::default()
},
);
vars.insert(
String::from("port"),
ServerVariable {
enum_values: Some(vec![String::from("8443"), String::from("443")]),
default: String::from("8443"),
description: Some(String::from("the port to serve HTTP traffic on")),
..Default::default()
},
);
vars.insert(
String::from("basePath"),
ServerVariable {
default: String::from("v2"),
description: Some(String::from(
"open meaning there is the opportunity to use special base paths as assigned by the provider, default is `v2`"
)),
..Default::default()
},
);
vars
}),
..Default::default()
},
"deserialize with url, description and variables",
);
}
#[test]
fn test_server_validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
Server {
url: String::from("https://development.gigantic-server.com/v1"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("server"));
assert_eq!(ctx.errors.len(), 0, "no errors: {:?}", ctx.errors);
Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("server"));
assert_eq!(ctx.errors.len(), 3, "3 errors: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
variables: Some({
let mut vars = BTreeMap::<String, ServerVariable>::new();
vars.insert(
String::from("username"),
ServerVariable {
default: String::from("demo"),
..Default::default()
},
);
vars.insert(
String::from("port"),
ServerVariable {
default: String::from("8443"),
..Default::default()
},
);
vars.insert(
String::from("basePath"),
ServerVariable {
default: String::from("v2"),
..Default::default()
},
);
vars
}),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("server"));
assert_eq!(
ctx.errors.len(),
0,
"all variables are defined: {:?}",
ctx.errors
);
Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
variables: Some({
let mut vars = BTreeMap::<String, ServerVariable>::new();
vars.insert(
String::from("username"),
ServerVariable {
default: String::from("demo"),
..Default::default()
},
);
vars.insert(
String::from("port"),
ServerVariable {
default: String::from("8443"),
..Default::default()
},
);
vars.insert(
String::from("basePath"),
ServerVariable {
default: String::from("v2"),
..Default::default()
},
);
vars.insert(
String::from("foo"),
ServerVariable {
default: String::from("bar"),
..Default::default()
},
);
vars
}),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("server"));
assert_eq!(ctx.errors.len(), 1, "with used variable: {:?}", ctx.errors);
ctx = Context::new(&spec, EnumSet::only(Options::IgnoreUnusedServerVariables));
Server {
url: String::from("https://{username}.gigantic-server.com:{port}/{basePath}"),
variables: Some({
let mut vars = BTreeMap::<String, ServerVariable>::new();
vars.insert(
String::from("username"),
ServerVariable {
default: String::from("demo"),
..Default::default()
},
);
vars.insert(
String::from("port"),
ServerVariable {
default: String::from("8443"),
..Default::default()
},
);
vars.insert(
String::from("basePath"),
ServerVariable {
default: String::from("v2"),
..Default::default()
},
);
vars.insert(
String::from("foo"),
ServerVariable {
default: String::from("bar"),
..Default::default()
},
);
vars
}),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("server"));
assert_eq!(
ctx.errors.len(),
0,
"ignore used variable: {:?}",
ctx.errors
);
}
}