Skip to content

Commit

Permalink
Add some context to code gen errors
Browse files Browse the repository at this point in the history
A bit of a rough way to do it, but it's relatively easy and not that
intrusive. It's very nice to get some general context about where in the
process an error occurred when generating code, so that you can CTRL-F
into the offending XML file and find where the code gen gave up.

This adds some basic context to `CodeGenError` that lets us capture this
information and pass it along.
  • Loading branch information
einarmo committed Jan 23, 2025
1 parent 9790a36 commit 636f533
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 108 deletions.
80 changes: 74 additions & 6 deletions async-opcua-codegen/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::{
fmt::Display,
num::{ParseFloatError, ParseIntError},
str::ParseBoolError,
};

use thiserror::Error;

#[derive(Error, Debug)]
pub enum CodeGenError {
pub enum CodeGenErrorKind {
#[error("Failed to load XML: {0}")]
XML(#[from] opcua_xml::XmlError),
Xml(#[from] opcua_xml::XmlError),
#[error("Missing required field: {0}")]
MissingRequiredValue(&'static str),
#[error("Wrong format on field. Expected {0}, got {1}")]
Expand All @@ -27,26 +28,93 @@ pub enum CodeGenError {
Io(String, std::io::Error),
}

#[derive(Error, Debug)]
pub struct CodeGenError {
#[source]
pub kind: Box<CodeGenErrorKind>,
pub context: Option<String>,
pub file: Option<String>,
}

impl Display for CodeGenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Code generation failed: {}", self.kind)?;
if let Some(context) = &self.context {
write!(f, ", while {context}")?;
}
if let Some(file) = &self.file {
write!(f, ", while loading file {file}")?;
}
Ok(())
}
}

impl From<ParseIntError> for CodeGenError {
fn from(value: ParseIntError) -> Self {
Self::ParseInt("content".to_owned(), value)
Self::new(CodeGenErrorKind::ParseInt("content".to_owned(), value))
}
}

impl From<ParseBoolError> for CodeGenError {
fn from(value: ParseBoolError) -> Self {
Self::ParseBool("content".to_owned(), value)
Self::new(CodeGenErrorKind::ParseBool("content".to_owned(), value))
}
}

impl From<ParseFloatError> for CodeGenError {
fn from(value: ParseFloatError) -> Self {
Self::ParseFloat("content".to_owned(), value)
Self::new(CodeGenErrorKind::ParseFloat("content".to_owned(), value))
}
}

impl From<opcua_xml::XmlError> for CodeGenError {
fn from(value: opcua_xml::XmlError) -> Self {
Self::new(value.into())
}
}

impl From<syn::Error> for CodeGenError {
fn from(value: syn::Error) -> Self {
Self::new(value.into())
}
}

impl CodeGenError {
pub fn io(msg: &str, e: std::io::Error) -> Self {
Self::Io(msg.to_owned(), e)
Self::new(CodeGenErrorKind::Io(msg.to_owned(), e))
}

pub fn other(msg: impl Into<String>) -> Self {
Self::new(CodeGenErrorKind::Other(msg.into()))
}

pub fn parse_int(field: impl Into<String>, error: ParseIntError) -> Self {
Self::new(CodeGenErrorKind::ParseInt(field.into(), error))
}

pub fn wrong_format(format: impl Into<String>, value: impl Into<String>) -> Self {
Self::new(CodeGenErrorKind::WrongFormat(format.into(), value.into()))
}

pub fn missing_required_value(name: &'static str) -> Self {
Self::new(CodeGenErrorKind::MissingRequiredValue(name))
}

pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}

pub fn in_file(mut self, file: impl Into<String>) -> Self {
self.file = Some(file.into());
self
}

pub fn new(kind: CodeGenErrorKind) -> Self {
Self {
kind: Box::new(kind),
context: None,
file: None,
}
}
}
4 changes: 2 additions & 2 deletions async-opcua-codegen/src/ids/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn parse(
let vals: Vec<_> = line.split(",").collect();
if vals.len() == 2 {
let Some(type_name) = type_name else {
return Err(CodeGenError::Other(format!("CSV file {file_name} has only two columns, but no type name fallback was specified")));
return Err(CodeGenError::other(format!("CSV file {file_name} has only two columns, but no type name fallback was specified")));
};
types
.entry(type_name.to_owned())
Expand All @@ -50,7 +50,7 @@ pub fn parse(
.variants
.push((vals[1].parse()?, vals[0].to_owned()));
} else {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"CSV file {file_name} is on incorrect format. Expected two or three columns, got {}",
vals.len()
)));
Expand Down
50 changes: 33 additions & 17 deletions async-opcua-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ pub fn run_codegen(config: &CodeGenConfig, root_path: &str) -> Result<(), CodeGe
match target {
CodeGenTarget::Types(t) => {
println!("Running data type code generation for {}", t.file_path);
let (types, target_namespace) = generate_types(t, root_path)?;
let (types, target_namespace) =
generate_types(t, root_path).map_err(|e| e.in_file(&t.file_path))?;
println!("Writing {} types to {}", types.len(), t.output_dir);

let header = make_header(&t.file_path, &[&config.extra_header, &t.extra_header]);
Expand All @@ -132,13 +133,15 @@ pub fn run_codegen(config: &CodeGenConfig, root_path: &str) -> Result<(), CodeGe
}
}

let modules = write_to_directory(&t.output_dir, root_path, &header, types)?;
let modules = write_to_directory(&t.output_dir, root_path, &header, types)
.map_err(|e| e.in_file(&t.file_path))?;
let mut module_file = create_module_file(modules);
module_file
.items
.extend(type_loader_impl(&object_ids, &target_namespace).into_iter());

write_module_file(&t.output_dir, root_path, &header, module_file)?;
write_module_file(&t.output_dir, root_path, &header, module_file)
.map_err(|e| e.in_file(&t.file_path))?;
}
CodeGenTarget::Nodes(n) => {
println!("Running node set code generation for {}", n.file_path);
Expand All @@ -148,13 +151,19 @@ pub fn run_codegen(config: &CodeGenConfig, root_path: &str) -> Result<(), CodeGe
CodeGenError::io(&format!("Failed to read file {}", n.file_path), e)
})?;
let node_set = load_nodeset2_file(&node_set)?;
let nodes = node_set.node_set.as_ref().ok_or_else(|| {
CodeGenError::Other("Missing UANodeSet in xml schema".to_owned())
})?;
let nodes = node_set
.node_set
.as_ref()
.ok_or_else(|| {
CodeGenError::other("Missing UANodeSet in xml schema".to_owned())
})
.map_err(|e| e.in_file(&n.file_path))?;
println!("Found {} nodes in node set", nodes.nodes.len());

let chunks = generate_target(n, nodes, &config.preferred_locale, root_path)?;
let module_file = make_root_module(&chunks, n)?;
let chunks = generate_target(n, nodes, &config.preferred_locale, root_path)
.map_err(|e| e.in_file(&n.file_path))?;
let module_file =
make_root_module(&chunks, n).map_err(|e| e.in_file(&n.file_path))?;

println!("Writing {} files to {}", chunks.len() + 1, n.output_dir);

Expand All @@ -176,17 +185,22 @@ pub fn run_codegen(config: &CodeGenConfig, root_path: &str) -> Result<(), CodeGe
&format!("Failed to read file {}", n.file_path),
e,
)
})?;
})
.map_err(|e| e.in_file(&n.file_path))?;
let node_set = load_nodeset2_file(&node_set)?;
p_sets.push((node_set, nodeset_file.import_path.as_str()));
}
for set in &p_sets {
sets.push((
set.0.node_set.as_ref().ok_or_else(|| {
CodeGenError::Other(
"Missing UANodeSet in dependent xml schema".to_owned(),
)
})?,
set.0
.node_set
.as_ref()
.ok_or_else(|| {
CodeGenError::other(
"Missing UANodeSet in dependent xml schema".to_owned(),
)
})
.map_err(|e| e.in_file(&n.file_path))?,
set.1,
));
}
Expand All @@ -199,19 +213,21 @@ pub fn run_codegen(config: &CodeGenConfig, root_path: &str) -> Result<(), CodeGe
&[&config.extra_header, &events_target.extra_header],
);
let modules =
write_to_directory(&events_target.output_dir, root_path, &header, events)?;
write_to_directory(&events_target.output_dir, root_path, &header, events)
.map_err(|e| e.in_file(&n.file_path))?;
write_module_file(
&events_target.output_dir,
root_path,
&header,
create_module_file(modules),
)?;
)
.map_err(|e| e.in_file(&n.file_path))?;
println!("Created {} event types", cnt);
}
}
CodeGenTarget::Ids(n) => {
println!("Running node ID code generation for {}", n.file_path);
let gen = generate_node_ids(n, root_path)?;
let gen = generate_node_ids(n, root_path).map_err(|e| e.in_file(&n.file_path))?;
let mut file = std::fs::File::options()
.create(true)
.truncate(true)
Expand Down
25 changes: 15 additions & 10 deletions async-opcua-codegen/src/nodeset/events/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ impl<'a> TypeCollector<'a> {
) -> Result<(), CodeGenError> {
// Type must exist, otherwise it's going to cause trouble.
let Some(node) = self.nodes.get(type_id) else {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"Referenced type with id {type_id} not found."
)));
};
Expand Down Expand Up @@ -227,39 +227,44 @@ impl<'a> TypeCollector<'a> {
}

let Some(target_node) = self.nodes.get(target) else {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"Node {target} not found in node dict"
)));
))
.with_context(format!("collecting type {type_id}")));
};

let kind = match &target_node.node {
UANode::Object(_) => {
let Some(type_def) = type_def else {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"Property {target} is missing type definition"
)));
))
.with_context(format!("collecting type {type_id}")));
};
FieldKind::Object(type_def)
}
UANode::Variable(v) => {
let Some(type_def) = type_def else {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"Property {target} is missing type definition"
)));
))
.with_context(format!("collecting type {type_id}")));
};
data_type_id = Some(target_node.lookup_node_id(v.data_type.0.as_str()));
FieldKind::Variable(type_def)
}
UANode::Method(_) => FieldKind::Method,
_ => {
return Err(CodeGenError::Other(format!(
return Err(CodeGenError::other(format!(
"Property {target} has unexpected node class"
)))
))
.with_context(format!("collecting type {type_id}")))
}
};

let browse_name = target_node.node.base().browse_name.0.as_str();
let (name, _) = split_qualified_name(browse_name)?;
let (name, _) = split_qualified_name(browse_name)
.map_err(|e| e.with_context(format!("collecting type {type_id}")))?;

fields.insert(
name,
Expand Down
Loading

0 comments on commit 636f533

Please sign in to comment.