Skip to content

Commit 799b546

Browse files
committed
feat(validator): Simplify and change output JSON
Simplify/flatten the reply JSON object. With this patch, the validator will output a JSON object with mandatory success field set to true and an empty error list. E.g: { "success": true, "errors": [] } If there is an error, success will be false and errors contain one or more error objects. Each of the error objects contains a type, a message and, optionally a subtype. Type is one of: "Environment" "Deserialization" "Metadata" "Conversion" "Configuration" The optional subtype is informational. Currently it is only set for errors of type Deserialization. E.g: { "success": false, "errors": [ { "type": "Deserialization", "subtype": "Syntax", "message": "trailing comma at line 8 column 3" } ] } Signed-off-by: Fredi Raspall <fredi@githedgehog.com>
1 parent 0d4eb31 commit 799b546

File tree

3 files changed

+114
-57
lines changed

3 files changed

+114
-57
lines changed

Cargo.lock

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

validator/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ path = "src/main.rs"
1212
[dependencies]
1313
config = { workspace = true }
1414
k8s-intf = { workspace = true }
15-
ordermap = { workspace = true, features = ["serde"] }
1615

1716
serde = { workspace = true }
1817
serde_json = { workspace = true }

validator/src/main.rs

Lines changed: 114 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,21 @@
1212

1313
use config::{ExternalConfig, GwConfig};
1414
use k8s_intf::generated::gateway_agent_crd::GatewayAgent;
15-
use ordermap::OrderMap;
1615
use serde::{Deserialize, Serialize};
1716
use std::io::{self, Read};
1817

19-
#[derive(Serialize, Deserialize, Default)]
18+
#[derive(Default)]
2019
struct ConfigErrors {
2120
errors: Vec<String>, // only one error is supported at the moment
2221
}
2322

24-
#[derive(Serialize, Deserialize, Default)]
2523
struct DeserializeError {
2624
hint: String,
27-
line: usize,
28-
column: usize,
29-
category: String,
30-
context: OrderMap<usize, String>,
25+
subtype: String,
3126
}
3227

33-
/// The type representing the outcome of a validation request
34-
#[derive(Serialize, Deserialize, Default)]
35-
enum ValidateReply {
28+
/// The type representing an error when validating a request
29+
enum ValidateError {
3630
/// This type contains errors that may occur when using this tool.
3731
EnvironmentError(String),
3832

@@ -55,108 +49,175 @@ enum ValidateReply {
5549
/// - contains values that are not allowed / supported
5650
///
5751
/// which would prevent the gateway from functioning correctly.
58-
/// Unlike any of the previous, these are errors the user is responsible for.
52+
/// Together with some conversion errors, these are errors the user is responsible for.
5953
Configuration(ConfigErrors),
54+
}
55+
impl ValidateError {
56+
/// Provide a string indicating the type of error
57+
fn get_type(&self) -> &str {
58+
match self {
59+
ValidateError::EnvironmentError(_) => "Environment",
60+
ValidateError::DeserializeError(_) => "Deserialization",
61+
ValidateError::MetadataError(_) => "Metadata",
62+
ValidateError::ConversionError(_) => "Conversion",
63+
ValidateError::Configuration(_) => "Configuration",
64+
}
65+
}
66+
/// Provide a string indicating the subtype of an error
67+
fn get_subtype(&self) -> Option<String> {
68+
match self {
69+
ValidateError::DeserializeError(e) => Some(e.subtype.clone()),
70+
_ => None,
71+
}
72+
}
6073

61-
/// This type signals that the configuration could be processed and that it is VALID.
62-
#[default]
63-
Success,
74+
/// Provide a list of messages depending on the error type
75+
fn get_msg(&self) -> Vec<String> {
76+
match self {
77+
ValidateError::EnvironmentError(v) => vec![v.clone()],
78+
ValidateError::DeserializeError(v) => vec![v.hint.clone()],
79+
ValidateError::MetadataError(v) => vec![v.clone()],
80+
ValidateError::ConversionError(v) => vec![v.clone()],
81+
ValidateError::Configuration(v) => v.errors.to_vec(),
82+
}
83+
}
6484
}
6585

66-
/// Deserialize json string as a `GatewayAgent`
67-
fn deserialize(ga_json: &str) -> Result<GatewayAgent, ValidateReply> {
68-
let crd = serde_json::from_str::<GatewayAgent>(ga_json).map_err(|e| {
69-
let mut deserr = DeserializeError::default();
70-
deserr.hint = e.to_string();
71-
deserr.line = e.line();
72-
deserr.column = e.column();
73-
deserr.category = format!("{:?}", e.classify());
74-
75-
let mut context = OrderMap::new();
76-
let start = e.line() - 5;
77-
let end = e.line() + 5;
78-
for k in start..end {
79-
let line = ga_json.lines().nth(k).unwrap_or("").to_string();
80-
context.insert(k, line);
86+
impl From<&ValidateError> for ValidateReply {
87+
fn from(value: &ValidateError) -> Self {
88+
let r#type = value.get_type();
89+
let subtype = value.get_subtype();
90+
let msg = value.get_msg();
91+
92+
ValidateReply {
93+
success: false,
94+
errors: msg
95+
.iter()
96+
.map(|m| ValidateErrorJson {
97+
r#type: r#type.to_owned(),
98+
subtype: subtype.clone(),
99+
message: m.clone(),
100+
context: None,
101+
})
102+
.collect(),
81103
}
82-
deserr.context = context;
104+
}
105+
}
106+
107+
#[derive(Serialize, Deserialize)]
108+
struct ValidateErrorJson {
109+
r#type: String,
110+
#[serde(skip_serializing_if = "Option::is_none")]
111+
subtype: Option<String>,
112+
message: String,
113+
#[serde(skip_serializing_if = "Option::is_none")]
114+
context: Option<String>,
115+
}
83116

84-
ValidateReply::DeserializeError(deserr)
117+
/// The type representing the outcome of a validation request
118+
#[derive(Serialize, Deserialize)]
119+
struct ValidateReply {
120+
success: bool,
121+
errors: Vec<ValidateErrorJson>,
122+
}
123+
impl ValidateReply {
124+
fn success() -> Self {
125+
Self {
126+
success: true,
127+
errors: vec![],
128+
}
129+
}
130+
}
131+
132+
/// Deserialize JSON string as a `GatewayAgent`
133+
fn deserialize(ga_json: &str) -> Result<GatewayAgent, ValidateError> {
134+
let crd = serde_json::from_str::<GatewayAgent>(ga_json).map_err(|e| {
135+
let deserr = DeserializeError {
136+
hint: e.to_string(),
137+
subtype: format!("{:?}", e.classify()),
138+
};
139+
ValidateError::DeserializeError(deserr)
85140
})?;
86141

87142
Ok(crd)
88143
}
89144

90145
/// Validate metadata
91-
fn validate_metadata(crd: &GatewayAgent) -> Result<&str, ValidateReply> {
92-
let genid = crd.metadata.generation.ok_or(ValidateReply::MetadataError(
146+
fn validate_metadata(crd: &GatewayAgent) -> Result<&str, ValidateError> {
147+
let genid = crd.metadata.generation.ok_or(ValidateError::MetadataError(
93148
"Missing generation Id".to_string(),
94149
))?;
95150
if genid == 0 {
96-
return Err(ValidateReply::MetadataError(
151+
return Err(ValidateError::MetadataError(
97152
"Invalid generation Id".to_string(),
98153
));
99154
}
100155
let gwname = crd
101156
.metadata
102157
.name
103158
.as_ref()
104-
.ok_or(ValidateReply::MetadataError(
159+
.ok_or(ValidateError::MetadataError(
105160
"Missing gateway name".to_string(),
106161
))?;
107162
if gwname.is_empty() {
108-
return Err(ValidateReply::MetadataError(
163+
return Err(ValidateError::MetadataError(
109164
"Invalid gateway name".to_string(),
110165
));
111166
}
112167
let namespace = crd
113168
.metadata
114169
.namespace
115170
.as_ref()
116-
.ok_or(ValidateReply::MetadataError(
171+
.ok_or(ValidateError::MetadataError(
117172
"Missing namespace".to_string(),
118173
))?;
119174
if namespace.as_str() != "fab" {
120-
return Err(ValidateReply::MetadataError(format!(
175+
return Err(ValidateError::MetadataError(format!(
121176
"Invalid namespace {namespace}"
122177
)));
123178
}
124179

125180
Ok(gwname.as_str())
126181
}
127182

128-
fn validate(gwagent_json: &str) -> Result<ValidateReply, ValidateReply> {
183+
/// Main validation function
184+
fn validate(gwagent_json: &str) -> Result<(), ValidateError> {
129185
let crd = deserialize(gwagent_json)?;
130186
let gwname = validate_metadata(&crd)?;
131187

132188
let external = ExternalConfig::try_from(&crd)
133-
.map_err(|e| ValidateReply::ConversionError(e.to_string()))?;
189+
.map_err(|e| ValidateError::ConversionError(e.to_string()))?;
134190

135191
let mut gwconfig = GwConfig::new(gwname, external);
136192
gwconfig.validate().map_err(|e| {
137-
let mut errors = ConfigErrors::default();
138-
errors.errors.push(e.to_string());
139-
ValidateReply::Configuration(errors)
193+
let mut config = ConfigErrors::default();
194+
config.errors.push(e.to_string());
195+
ValidateError::Configuration(config)
140196
})?;
141197

142-
Ok(ValidateReply::Success)
198+
Ok(())
143199
}
144200

145-
fn validate_from_stdin() -> Result<ValidateReply, ValidateReply> {
201+
/// Read from stdin, deserialize as JSON and validate
202+
fn validate_from_stdin() -> Result<(), ValidateError> {
146203
let mut input = String::new();
147204
io::stdin()
148205
.read_to_string(&mut input)
149-
.map_err(|e| ValidateReply::EnvironmentError(format!("Failed to read from stdin: {e}")))?;
206+
.map_err(|e| ValidateError::EnvironmentError(format!("Failed to read from stdin: {e}")))?;
150207

151208
validate(&input)
152209
}
153210

154-
fn main() {
155-
match validate_from_stdin() {
156-
Ok(reply) => println!("{}", serde_json::to_string_pretty(&reply).unwrap()),
157-
Err(reply) => {
158-
let json = serde_json::to_string_pretty(&reply).unwrap();
159-
println!("{json}");
160-
}
211+
/// Build a validation reply to be output as JSON
212+
fn build_reply(result: Result<(), ValidateError>) -> ValidateReply {
213+
match result {
214+
Ok(()) => ValidateReply::success(),
215+
Err(e) => ValidateReply::from(&e),
161216
}
162217
}
218+
219+
fn main() {
220+
let result = validate_from_stdin();
221+
let reply = build_reply(result);
222+
println!("{}", serde_json::to_string_pretty(&reply).unwrap());
223+
}

0 commit comments

Comments
 (0)