Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 21 additions & 58 deletions crates/ironposh-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1291,10 +1291,10 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 {
.map(|field| {
let field_name = field.ident.as_ref().unwrap().clone();
let is_optional = is_option_type(&field.ty);
let tag_name_type = extract_tag_name_type(&field.ty);
let value_type = inner_value_type(&field.ty);
SimpleFieldEntry {
field_name,
tag_name_type,
value_type,
is_optional,
}
})
Expand All @@ -1305,19 +1305,19 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 {
quote! { let mut #f = None; }
});

// One namespace-correct match per field: identity is (URI, local-name).
let matchers = entries.iter().filter_map(|e| {
// One namespace-correct match per field: identity is (URI, local-name),
// read from the field's tag type via `NamedTag` so it works through aliases.
let matchers = entries.iter().map(|e| {
let f = &e.field_name;
e.tag_name_type.as_ref().map(|n| {
quote! {
if child.is_element_named(
<crate::cores::#n as crate::cores::TagName>::NAMESPACE,
<crate::cores::#n as crate::cores::TagName>::TAG_NAME,
) {
#f = Some(ironposh_xml::mapping::FromXml::from_xml(child)?);
}
let ty = &e.value_type;
quote! {
if child.is_element_named(
<#ty as crate::cores::NamedTag>::NAMESPACE,
<#ty as crate::cores::NamedTag>::TAG_NAME,
) {
#f = Some(ironposh_xml::mapping::FromXml::from_xml(child)?);
}
})
}
});

let construct = entries.iter().map(|e| {
Expand Down Expand Up @@ -1419,7 +1419,7 @@ fn impl_simple_tag_value(input: &DeriveInput) -> TokenStream2 {

struct SimpleFieldEntry {
field_name: Ident,
tag_name_type: Option<Ident>,
value_type: Type,
is_optional: bool,
}

Expand All @@ -1437,55 +1437,18 @@ fn is_option_type(ty: &Type) -> bool {
false
}

fn extract_tag_name_type(ty: &Type) -> Option<Ident> {
// Try to extract TagName from Tag<'a, ValueType, TagName> or Option<Tag<'a, ValueType, TagName>>
if let Type::Path(TypePath { path, .. }) = ty {
for segment in &path.segments {
if segment.ident == "Tag" || segment.ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
// For Option<Tag<...>>, we need to look at the inner type
for arg in &args.args {
if let syn::GenericArgument::Type(inner_type) = arg {
if let Some(tag_name) = extract_tag_name_from_tag_type(inner_type) {
return Some(tag_name);
}
}
}

// For Tag<'a, ValueType, TagName>, the third argument is TagName
if segment.ident == "Tag" && args.args.len() >= 3 {
if let syn::GenericArgument::Type(Type::Path(TypePath { path, .. })) =
&args.args[2]
{
if let Some(segment) = path.segments.last() {
return Some(segment.ident.clone());
}
}
}
}
}
}
}
None
}

fn extract_tag_name_from_tag_type(ty: &Type) -> Option<Ident> {
/// The value a field carries: `Option<T>` -> `T`, otherwise the type itself.
fn inner_value_type(ty: &Type) -> Type {
if let Type::Path(TypePath { path, .. }) = ty {
for segment in &path.segments {
if segment.ident == "Tag" {
if let Some(segment) = path.segments.last() {
if segment.ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if args.args.len() >= 3 {
if let syn::GenericArgument::Type(Type::Path(TypePath { path, .. })) =
&args.args[2]
{
if let Some(segment) = path.segments.last() {
return Some(segment.ident.clone());
}
}
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return inner.clone();
}
}
}
}
}
None
ty.clone()
}
147 changes: 0 additions & 147 deletions crates/ironposh-winrm/src/cores/anytag.rs

This file was deleted.

3 changes: 0 additions & 3 deletions crates/ironposh-winrm/src/cores/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
pub mod anytag;
pub mod attribute;
pub mod namespace;
pub mod tag;
pub mod tag_list;
pub mod tag_name;
pub mod tag_value;

pub use attribute::*;
pub use namespace::*;
pub use tag::*;
pub use tag_list::*;
pub use tag_name::*;
pub use tag_value::*;
72 changes: 61 additions & 11 deletions crates/ironposh-winrm/src/cores/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,25 +166,31 @@ where
{
fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result<Self, ironposh_xml::XmlError> {
// Identity is the (namespace-URI, local-name) pair; the prefix is never
// consulted. A dispatcher (a derived struct, AnyTag, or a TagList) has
// usually already matched this, so the check is a cheap self-validation.
if !node.is_element_named(N::NAMESPACE, N::TAG_NAME) {
return Err(ironposh_xml::XmlError::XmlInvalidTag {
expected: N::TAG_NAME.to_string(),
found: node.tag_name().name().to_string(),
});
}
// consulted. Usually a dispatcher already matched this element, so `node`
// *is* this tag. When a parent tag carries another tag as its value
// (`Tag<Tag<..>, _>`), we're handed the parent instead — descend to the
// single N-named child.
let element = if node.is_element_named(N::NAMESPACE, N::TAG_NAME) {
node
} else {
node.children()
.find(|child| child.is_element_named(N::NAMESPACE, N::TAG_NAME))
.ok_or_else(|| ironposh_xml::XmlError::XmlInvalidTag {
expected: N::TAG_NAME.to_string(),
found: node.tag_name().name().to_string(),
})?
};

let value = V::from_xml(node)?;
let attributes = node
let value = V::from_xml(element)?;
let attributes = element
.attributes()
.filter_map(|attr| {
Attribute::from_name_and_value(attr.name(), attr.value())
.ok()
.flatten()
})
.collect();
let namespaces_declaration = NamespaceDeclaration::from_xml(node)?;
let namespaces_declaration = NamespaceDeclaration::from_xml(element)?;

Ok(Tag {
value,
Expand All @@ -196,6 +202,25 @@ where
}
}

/// A tag type's XML identity (name + namespace) exposed at the type level.
///
/// `Tag<'a, V, N>` forwards to its `N: TagName`. Reading identity through this
/// trait — rather than naming `N` syntactically — lets `#[derive(FromXml)]`
/// work through type aliases like `pub type Get<'a> = Tag<'a, Text<'a>, GetTag>`.
pub trait NamedTag {
const TAG_NAME: &'static str;
const NAMESPACE: Option<&'static str>;
}

impl<'a, V, N> NamedTag for Tag<'a, V, N>
where
V: TagValue<'a>,
N: TagName,
{
const TAG_NAME: &'static str = N::TAG_NAME;
const NAMESPACE: Option<&'static str> = N::NAMESPACE;
}

impl<'a, V, N> AsRef<V> for Tag<'a, V, N>
where
V: TagValue<'a>,
Expand Down Expand Up @@ -231,3 +256,28 @@ impl_tag_from!(&'a str => Tag<'a, Text<'a>, N>);
impl_tag_from!(String => Tag<'a, Text<'a>, N>);
impl_tag_from!(u32 => Tag<'a, U32, N>);
impl_tag_from!(uuid::Uuid => Tag<'a, WsUuid, N>);

#[cfg(test)]
mod tests {
use super::*;
use crate::cores::{CommandId, CommandResponse};
use ironposh_xml::parser::parse;

const RSP: &str = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell";

/// A `Tag` whose value is itself a `Tag` (`<CommandResponse>` wrapping a
/// `<CommandId>` child). `from_xml` must descend to the named child rather
/// than parse the wrapper as the inner tag. Regression for the SSPI e2e.
#[test]
fn nested_tag_value_descends_to_child() {
let uuid = "2D6534D0-6B12-40E3-B773-CBA26459CFA8";
let xml = format!(
r#"<rsp:CommandResponse xmlns:rsp="{RSP}"><rsp:CommandId>{uuid}</rsp:CommandId></rsp:CommandResponse>"#
);
let doc = parse(&xml).unwrap();
let tag: Tag<'_, Tag<'_, WsUuid, CommandId>, CommandResponse> =
Tag::from_xml(doc.root_element())
.expect("nested CommandResponse/CommandId should parse");
assert_eq!(tag.value.value.0.to_string().to_uppercase(), uuid);
}
}
Loading