Skip to content

Commit 12836bc

Browse files
committed
Support dependent nodesets for types codegen
This is a relatively simple implementation of this. Let users list dependent nodesets with import paths, like we allow for events. When actually generating the types, we store this as a map from namespace to import path, and during loading we just get the namespace of the type being loaded. This only really works for nodeset2 files, but since there exists a workaround, this isn't critical. We can improve on this in the future. With the codegen tests we can actually test stuff like this, which is quite nice.
1 parent 3b418a7 commit 12836bc

File tree

11 files changed

+232
-39
lines changed

11 files changed

+232
-39
lines changed

async-opcua-codegen/sample_codegen_config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ targets:
9292
# This is useful if the nodeset lacks node ID CSV files, or those files are incomplete.
9393
node_ids_from_nodeset: false
9494

95+
dependent_nodesets:
96+
# This can be a path, filename, or primary namespace URI.
97+
- file: Another.Namespace.Uri
98+
import_path: crate::generated::another_namespace
99+
95100
# This target generates code to generate nodes that are added to the server address space.
96101
# Each node in the NodeSet2 file creates a function, which is then called from
97102
# a large iterator that can be used as a a node set source.

async-opcua-codegen/src/input/nodeset.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub struct RawEncodingIds {
3131
#[derive(Debug, Clone)]
3232
pub struct TypeInfo {
3333
pub name: String,
34+
pub namespace: String,
3435
pub is_abstract: bool,
3536
pub definition: Option<DataTypeDefinition>,
3637
pub encoding_ids: RawEncodingIds,
@@ -346,6 +347,7 @@ impl NodeSetInput {
346347
is_abstract: data_type.base.is_abstract,
347348
definition: data_type.definition.clone(),
348349
encoding_ids,
350+
namespace: self.uri.clone(),
349351
},
350352
);
351353
}

async-opcua-codegen/src/types/gen.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ pub struct CodeGenerator {
9595
target_namespace: String,
9696
native_types: HashSet<String>,
9797
id_path: String,
98+
namespace_to_import_path: HashMap<String, String>,
9899
}
99100

100101
impl CodeGenerator {
102+
#[allow(clippy::too_many_arguments)]
101103
pub fn new(
102104
external_import_map: HashMap<String, ExternalType>,
103105
native_types: HashSet<String>,
@@ -106,6 +108,7 @@ impl CodeGenerator {
106108
config: CodeGenItemConfig,
107109
target_namespace: String,
108110
id_path: String,
111+
namespace_to_import_path: HashMap<String, String>,
109112
) -> Self {
110113
Self {
111114
import_map: external_import_map
@@ -119,7 +122,10 @@ impl CodeGenerator {
119122
Some("ExtensionObject" | "OptionSet") => {
120123
Some(FieldType::ExtensionObject(None))
121124
}
122-
Some(t) => Some(FieldType::Normal(t.to_owned())),
125+
Some(t) => Some(FieldType::Normal {
126+
name: t.to_owned(),
127+
namespace: None,
128+
}),
123129
None => None,
124130
},
125131
path: v.path,
@@ -137,6 +143,7 @@ impl CodeGenerator {
137143
target_namespace,
138144
native_types,
139145
id_path,
146+
namespace_to_import_path,
140147
}
141148
}
142149

@@ -159,7 +166,7 @@ impl CodeGenerator {
159166
}
160167

161168
let Some(it) = self.import_map.get(name) else {
162-
// Not in the import map means it's a builtin, we assume these have defaults for now.
169+
// Not in the import map means it's a builtin or external reference, we assume these have defaults for now.
163170
return true;
164171
};
165172

@@ -175,8 +182,8 @@ impl CodeGenerator {
175182
LoadedType::Struct(s) => {
176183
for k in &s.fields {
177184
let has_default = match &k.typ {
178-
StructureFieldType::Field(FieldType::Normal(f)) => {
179-
self.is_default_recursive(f)
185+
StructureFieldType::Field(FieldType::Normal { name, .. }) => {
186+
self.is_default_recursive(name)
180187
}
181188
StructureFieldType::Array(_) | StructureFieldType::Field(_) => true,
182189
};
@@ -279,7 +286,7 @@ impl CodeGenerator {
279286
}
280287

281288
/// Get the fully qualified path of a type, by looking it up in the import map.
282-
fn get_type_path(&self, name: &str) -> String {
289+
fn get_type_path(&self, name: &str, namespace: Option<&str>) -> String {
283290
// Type is known, use the external path.
284291
if let Some(ext) = self.import_map.get(name) {
285292
return format!("{}::{}", ext.path, name);
@@ -288,6 +295,12 @@ impl CodeGenerator {
288295
if self.native_types.contains(name) {
289296
return name.to_owned();
290297
}
298+
299+
if let Some(namespace) = namespace {
300+
if let Some(import_path) = self.namespace_to_import_path.get(namespace) {
301+
return format!("{}::{}", import_path, name);
302+
}
303+
}
291304
// Assume the type is a builtin.
292305
format!("opcua::types::{name}")
293306
}
@@ -548,7 +561,7 @@ impl CodeGenerator {
548561
fn is_extension_object(&self, typ: Option<&FieldType>) -> bool {
549562
let name = match &typ {
550563
Some(FieldType::Abstract(_)) | Some(FieldType::ExtensionObject(_)) => return true,
551-
Some(FieldType::Normal(s)) => s,
564+
Some(FieldType::Normal { name, .. }) => name,
552565
None => return false,
553566
};
554567
let name = match name.split_once(":") {
@@ -596,18 +609,22 @@ impl CodeGenerator {
596609

597610
for field in item.visible_fields() {
598611
let typ: Type = match &field.typ {
599-
StructureFieldType::Field(f) => {
600-
syn::parse_str(&self.get_type_path(f.as_type_str())).map_err(|e| {
601-
CodeGenError::from(e)
602-
.with_context(format!("Generating path for {}", f.as_type_str()))
603-
})?
604-
}
612+
StructureFieldType::Field(f) => syn::parse_str(
613+
&self.get_type_path(f.as_type_str(), f.namespace()),
614+
)
615+
.map_err(|e| {
616+
CodeGenError::from(e)
617+
.with_context(format!("Generating path for {}", f.as_type_str()))
618+
})?,
605619
StructureFieldType::Array(f) => {
606620
let path: Path =
607-
syn::parse_str(&self.get_type_path(f.as_type_str())).map_err(|e| {
608-
CodeGenError::from(e)
609-
.with_context(format!("Generating path for {}", f.as_type_str()))
610-
})?;
621+
syn::parse_str(&self.get_type_path(f.as_type_str(), f.namespace()))
622+
.map_err(|e| {
623+
CodeGenError::from(e).with_context(format!(
624+
"Generating path for {}",
625+
f.as_type_str()
626+
))
627+
})?;
611628
parse_quote! { Option<Vec<#path>> }
612629
}
613630
};

async-opcua-codegen/src/types/loaders/binary_schema.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ impl<'a> BsdTypeLoader<'a> {
5151
fn get_field_type(field: &str) -> FieldType {
5252
match field {
5353
"ExtensionObject" | "OptionSet" => FieldType::ExtensionObject(None),
54-
_ => FieldType::Normal(field.to_owned()),
54+
_ => FieldType::Normal {
55+
name: field.to_owned(),
56+
namespace: None,
57+
},
5558
}
5659
}
5760

@@ -114,7 +117,10 @@ impl<'a> BsdTypeLoader<'a> {
114117
Some("ua:ExtensionObject" | "ua:OptionSet") => {
115118
Some(FieldType::ExtensionObject(None))
116119
}
117-
Some(base) => Some(FieldType::Normal(self.massage_type_name(base))),
120+
Some(base) => Some(FieldType::Normal {
121+
name: self.massage_type_name(base),
122+
namespace: Some(self.target_namespace()),
123+
}),
118124
None => None,
119125
},
120126
is_union: false,

async-opcua-codegen/src/types/loaders/nodeset.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use opcua_xml::schema::ua_node_set::{DataTypeField, LocalizedText, UADataType, U
55
use crate::{
66
input::{NodeSetInput, SchemaCache, TypeInfo},
77
utils::{split_qualified_name, to_snake_case, NodeIdVariant, ParsedNodeId},
8-
CodeGenError,
8+
CodeGenError, BASE_NAMESPACE,
99
};
1010

1111
use super::{
@@ -67,7 +67,10 @@ impl<'a> NodeSetTypeLoader<'a> {
6767
} else if info.name == "Structure" || info.name == "OptionSet" {
6868
FieldType::ExtensionObject(Some(info.encoding_ids))
6969
} else {
70-
FieldType::Normal(info.name)
70+
FieldType::Normal {
71+
name: info.name,
72+
namespace: Some(info.namespace),
73+
}
7174
}
7275
}
7376

@@ -283,6 +286,7 @@ impl<'a> NodeSetTypeLoader<'a> {
283286
is_abstract: false,
284287
definition: None,
285288
encoding_ids: Default::default(),
289+
namespace: BASE_NAMESPACE.to_owned(),
286290
})
287291
} else {
288292
Ok(r)

async-opcua-codegen/src/types/loaders/types.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,24 @@ pub struct StructureField {
1818
pub enum FieldType {
1919
Abstract(#[allow(unused)] String),
2020
ExtensionObject(Option<RawEncodingIds>),
21-
Normal(String),
21+
Normal {
22+
name: String,
23+
namespace: Option<String>,
24+
},
2225
}
2326

2427
impl FieldType {
2528
pub fn as_type_str(&self) -> &str {
2629
match self {
2730
FieldType::Abstract(_) | FieldType::ExtensionObject(_) => "ExtensionObject",
28-
FieldType::Normal(s) => s,
31+
FieldType::Normal { name, .. } => name,
32+
}
33+
}
34+
35+
pub fn namespace(&self) -> Option<&str> {
36+
match self {
37+
FieldType::Normal { namespace, .. } => namespace.as_deref(),
38+
_ => None,
2939
}
3040
}
3141
}

async-opcua-codegen/src/types/mod.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use tracing::info;
2323

2424
use crate::{
2525
input::{BinarySchemaInput, NodeSetInput, SchemaCache},
26-
CodeGenError, BASE_NAMESPACE,
26+
CodeGenError, DependentNodeset, BASE_NAMESPACE,
2727
};
2828

2929
#[derive(Serialize, Deserialize, Debug)]
@@ -60,6 +60,9 @@ pub struct TypeCodeGenTarget {
6060
#[serde(default)]
6161
/// If true, instead of using `id_path` and ID enums, generate the node IDs from the nodeset file.
6262
pub node_ids_from_nodeset: bool,
63+
/// List of dependent nodesets to load types from. Only valid when using a NodeSet input.
64+
#[serde(default)]
65+
pub dependent_nodesets: Vec<DependentNodeset>,
6366
}
6467

6568
impl Default for TypeCodeGenTarget {
@@ -75,6 +78,7 @@ impl Default for TypeCodeGenTarget {
7578
extra_header: String::new(),
7679
id_path: defaults::id_path(),
7780
node_ids_from_nodeset: false,
81+
dependent_nodesets: Vec::new(),
7882
}
7983
}
8084
}
@@ -120,7 +124,7 @@ pub fn generate_types(
120124
.map_err(|e| e.in_file(&input.path))?;
121125
info!("Loaded {} types", types.len());
122126

123-
generate_types_inner(target, target_namespace, types)
127+
generate_types_inner(target, target_namespace, types, HashMap::new())
124128
}
125129

126130
/// Generate types from the given NodeSet file input.
@@ -149,13 +153,21 @@ pub fn generate_types_nodeset(
149153
let types = type_loader.load_types(cache)?;
150154
info!("Loaded {} types", types.len());
151155

152-
generate_types_inner(target, target_namespace, types)
156+
let mut namespace_to_import_path = HashMap::new();
157+
for dependent_nodeset in &target.dependent_nodesets {
158+
let dep_input = cache.get_nodeset(&dependent_nodeset.file)?;
159+
namespace_to_import_path
160+
.insert(dep_input.uri.clone(), dependent_nodeset.import_path.clone());
161+
}
162+
163+
generate_types_inner(target, target_namespace, types, namespace_to_import_path)
153164
}
154165

155166
fn generate_types_inner(
156167
target: &TypeCodeGenTarget,
157168
target_namespace: String,
158169
types: Vec<LoadedType>,
170+
namespace_to_import_path: HashMap<String, String>,
159171
) -> Result<(Vec<GeneratedItem>, String), CodeGenError> {
160172
let mut types_import_map = basic_types_import_map();
161173
for (k, v) in &target.types_import_map {
@@ -179,6 +191,7 @@ fn generate_types_inner(
179191
},
180192
target_namespace.clone(),
181193
target.id_path.clone(),
194+
namespace_to_import_path,
182195
);
183196

184197
Ok((generator.generate_types()?, target_namespace))

codegen-tests/build.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,34 @@ use opcua_codegen::{CodeGenConfig, CodeGenSource, CodeGenTarget, TypeCodeGenTarg
77
fn main() {
88
let out_dir = std::env::var("OUT_DIR").unwrap();
99
let target_dir = format!("{}/opcua_generated", out_dir);
10+
println!("cargo:rerun-if-changed=schemas/Async.Opcua.Test.NodeSet2.xml");
11+
println!("cargo:rerun-if-changed=schemas/Async.Opcua.Test.Ext.NodeSet2.xml");
1012
println!("cargo:rustc-env=OPCUA_GENERATED_DIR={}", target_dir);
1113
run_codegen(
1214
&CodeGenConfig {
13-
targets: vec![CodeGenTarget::Types(TypeCodeGenTarget {
14-
file: "Async.Opcua.Test.NodeSet2.xml".to_owned(),
15-
output_dir: target_dir,
16-
enums_single_file: true,
17-
structs_single_file: true,
18-
node_ids_from_nodeset: true,
19-
default_excluded: ["SimpleEnum".to_string()].into_iter().collect(),
20-
..Default::default()
21-
})],
15+
targets: vec![
16+
CodeGenTarget::Types(TypeCodeGenTarget {
17+
file: "Async.Opcua.Test.NodeSet2.xml".to_owned(),
18+
output_dir: format!("{}/base", target_dir),
19+
enums_single_file: true,
20+
structs_single_file: true,
21+
node_ids_from_nodeset: true,
22+
default_excluded: ["SimpleEnum".to_string()].into_iter().collect(),
23+
..Default::default()
24+
}),
25+
CodeGenTarget::Types(TypeCodeGenTarget {
26+
file: "Async.Opcua.Test.Ext.NodeSet2.xml".to_owned(),
27+
output_dir: format!("{}/ext", target_dir),
28+
enums_single_file: true,
29+
structs_single_file: true,
30+
node_ids_from_nodeset: true,
31+
dependent_nodesets: vec![opcua_codegen::DependentNodeset {
32+
file: "Async.Opcua.Test.NodeSet2.xml".to_owned(),
33+
import_path: "crate::generated::base".to_owned(),
34+
}],
35+
..Default::default()
36+
}),
37+
],
2238
sources: vec![
2339
CodeGenSource::Implicit("./schemas".to_owned()),
2440
CodeGenSource::Implicit("../schemas/1.05".to_owned()),

0 commit comments

Comments
 (0)