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]
fn test_delete_missing_required_attribute() {
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
let input = "<delete></delete>";
let mut validator = XmlSchemaValidator::new(&schema_text).unwrap();
let result = validator.append(input);
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 {
println!("Error message: {}", err);
assert!(err.to_string().contains("required"),
"Error should mention missing required attribute");
let error_str = err.to_string().to_lowercase();
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>,
/// Buffer containing the attribute name characters processed so far
buffer: String,
/// Track which attributes have already been set for this element
attributes_set: Vec<String>,
}
impl AttributeName {
@@ -16,14 +18,25 @@ impl AttributeName {
Self {
element,
buffer: String::new(),
attributes_set: Vec::new(),
}
}
/// 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 {
element,
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(
Arc::clone(&self.element),
self.buffer,
self.attributes_set,
))];
}
}
@@ -59,6 +73,7 @@ impl AttributeName {
return vec![Token::AttributeName(AttributeName::with_buffer(
Arc::clone(&self.element),
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
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,
/// Whether we've seen the closing quote
closed: bool,
/// Track which attributes have already been set for this element
pub attributes_set: Vec<String>,
}
impl AttributeValue {
/// Create a new AttributeValue
pub fn new(element: Arc<schema::XsElement>, attribute_name: String) -> Self {
/// Create a new AttributeValue with a list of attributes already set
pub fn new(element: Arc<schema::XsElement>, attribute_name: String, attributes_set: Vec<String>) -> Self {
Self {
element,
attribute_name,
buffer: String::new(),
in_quotes: false,
closed: false,
attributes_set,
}
}
@@ -32,28 +35,38 @@ impl AttributeValue {
pub fn append(self, c: char) -> Vec<Token> {
if self.closed {
// 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() {
// Check if there are more attributes to parse
if let Some(attributes) = &self.element.xs_complex_type.xs_attribute {
// If there are other required attributes that haven't been set yet
for attr in attributes {
if attr.name != self.attribute_name && attr.xs_attribute_use == "required" {
return vec![Token::AttributeName(AttributeName::new(Arc::clone(&self.element)))];
if attr.name != self.attribute_name && attr.xs_attribute_use == "required" && !new_attributes.contains(&attr.name) {
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
return vec![
Token::ElementOpen(ElementOpenEnd::new(Arc::clone(&self.element))),
Token::ElementSelfClose(ElementSelfClose::new())
Token::ElementOpen(ElementOpenEnd::with_attributes(Arc::clone(&self.element), new_attributes.clone())),
Token::ElementSelfClose(ElementSelfClose::new_with_attributes(Arc::clone(&self.element), new_attributes))
];
} else if c == '>' {
// 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 == '/' {
// 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 {
// Unexpected character after attribute value
return vec![];
@@ -69,6 +82,7 @@ impl AttributeValue {
buffer: self.buffer,
in_quotes: false,
closed: false,
attributes_set: self.attributes_set,
})];
} else if c == '"' || c == '\'' {
// Start of quotes
@@ -78,6 +92,7 @@ impl AttributeValue {
buffer: self.buffer,
in_quotes: true,
closed: false,
attributes_set: self.attributes_set,
})];
} else {
// Unexpected character before quotes
@@ -94,6 +109,7 @@ impl AttributeValue {
buffer: self.buffer,
in_quotes: true,
closed: true,
attributes_set: self.attributes_set,
})];
} else {
// Continue building the attribute value
@@ -104,6 +120,7 @@ impl AttributeValue {
buffer: new_buffer,
in_quotes: true,
closed: false,
attributes_set: self.attributes_set,
})];
}
}
@@ -139,7 +156,7 @@ mod attribute_value_tests {
#[test]
fn test_attribute_value_quotes() {
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
let next_tokens = token.append('"');
@@ -176,7 +193,7 @@ mod attribute_value_tests {
#[test]
fn test_after_attribute_value_closed() {
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.closed = true;
@@ -185,7 +202,7 @@ mod attribute_value_tests {
assert!(!next_tokens.is_empty(), "Should accept space after closed attribute");
// 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;
token.in_quotes = true;
token.closed = true;
@@ -199,7 +216,7 @@ mod attribute_value_tests {
}
// 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;
token.in_quotes = true;
token.closed = true;
@@ -212,4 +229,25 @@ mod attribute_value_tests {
_ => 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)]
pub struct ElementOpenEnd {
element: Arc<schema::XsElement>,
// Track which attributes have been set for this element
pub attributes_set: Vec<String>,
}
impl ElementOpenEnd {
pub fn new(element: Arc<schema::XsElement>) -> Self {
Self {
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> {
// 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
let allows_text = self.element.xs_complex_type.mixed
.as_ref()
@@ -97,6 +117,26 @@ mod element_open_end_tests {
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]
fn test_append_text_in_mixed_content() {
let element = create_test_element();
@@ -159,4 +199,22 @@ mod element_open_end_tests {
let next_tokens = token.append('<');
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
if self.element.name == self.buffer {
// 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)))];
} else {
// '>' before full element name is invalid
@@ -230,4 +242,16 @@ mod element_open_name_tests {
let next_tokens = element_name.append('>');
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::*;
/// Represents a self-closing XML element
#[derive(Clone, Debug)]
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 {
/// Create a new self-closing element
pub fn new() -> 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
pub fn append(self, c: char) -> Vec<Token> {
if c == '>' {
vec![Token::EndOfFile(EndOfFile::new())]
} else if c.is_whitespace() {
// Before we close, verify that all required attributes are present
if let Some(element) = &self.element {
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)]
} else {
vec![]
@@ -27,6 +58,28 @@ impl ElementSelfClose {
#[cfg(test)]
mod element_self_close_tests {
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]
fn test_self_close_with_greater_than() {
@@ -65,4 +118,27 @@ mod element_self_close_tests {
let next_tokens = token.append('a');
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]),
}
}
}