diff --git a/netconf/src/capabilities.rs b/netconf/src/capabilities.rs index 64cefe5..4f5fc80 100644 --- a/netconf/src/capabilities.rs +++ b/netconf/src/capabilities.rs @@ -106,6 +106,8 @@ mod uri { pub(super) const WRITABLE_RUNNING_V1_0: &str = "urn:ietf:params:netconf:capability:writable-running:1.0"; pub(super) const CANDIDATE_V1_0: &str = "urn:ietf:params:netconf:capability:candidate:1.0"; + pub(super) const CONFIRMED_COMMIT_V1_0: &str = + "urn:ietf:params:netconf:capability:confirmed-commit:1.0"; } #[allow(variant_size_differences)] @@ -114,6 +116,7 @@ pub enum Capability { Base(Base), WritableRunning, Candidate, + ConfirmedCommit, Unknown(String), } @@ -125,6 +128,7 @@ impl Capability { uri::BASE_V1_1 => Ok(Self::Base(Base::V1_1)), uri::WRITABLE_RUNNING_V1_0 => Ok(Self::WritableRunning), uri::CANDIDATE_V1_0 => Ok(Self::Candidate), + uri::CONFIRMED_COMMIT_V1_0 => Ok(Self::ConfirmedCommit), _ => Ok(Self::Unknown(uri.to_string())), } } @@ -135,6 +139,7 @@ impl Capability { Self::Base(base) => base.uri(), Self::WritableRunning => uri::WRITABLE_RUNNING_V1_0, Self::Candidate => uri::CANDIDATE_V1_0, + Self::ConfirmedCommit => uri::CONFIRMED_COMMIT_V1_0, Self::Unknown(uri) => uri.as_str(), } } diff --git a/netconf/src/error.rs b/netconf/src/error.rs index 81223c8..339b1c9 100644 --- a/netconf/src/error.rs +++ b/netconf/src/error.rs @@ -111,6 +111,9 @@ pub enum Error { #[error("unsupported rpc operation '{0}' (requires capability '{1:?}')")] UnsupportedOperation(&'static str, Capability), + #[error("unsupported parameter '{1}' for rpc operation '{0}' (requires capability '{2:?}')")] + UnsupportedOperationParameter(&'static str, &'static str, Capability), + #[error("unsupported source datastore '{0:?}' (requires capability '{1:?}')")] UnsupportedSource(Datastore, Option), diff --git a/netconf/src/message/rpc/operation/commit.rs b/netconf/src/message/rpc/operation/commit.rs index 84c068f..8cf5d84 100644 --- a/netconf/src/message/rpc/operation/commit.rs +++ b/netconf/src/message/rpc/operation/commit.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{io::Write, time::Duration}; use quick_xml::Writer; @@ -8,7 +8,8 @@ use super::{Operation, WriteXml}; #[derive(Debug, Clone, Copy)] pub struct Commit { - _inner: (), + confirmed: bool, + confirm_timeout: Timeout, } impl Operation for Commit { @@ -20,29 +21,34 @@ impl WriteXml for Commit { type Error = Error; fn write_xml(&self, writer: &mut W) -> Result<(), Self::Error> { - _ = Writer::new(writer).create_element("commit").write_empty()?; + let mut writer = Writer::new(writer); + let elem = writer.create_element("commit"); + if self.confirmed { + _ = elem.write_inner_content(|writer| { + _ = writer.create_element("confirmed").write_empty()?; + if self.confirm_timeout != Timeout::default() { + _ = writer + .create_element("confirm-timeout") + .write_inner_content(|writer| { + write!(writer.get_mut(), "{}", self.confirm_timeout.0.as_secs())?; + Ok::<_, Error>(()) + })?; + }; + Ok::<_, Error>(()) + })?; + } else { + _ = elem.write_empty()?; + }; Ok(()) } } -#[derive(Debug, Clone, Copy)] -pub struct DiscardChanges { - _inner: (), -} - -impl Operation for DiscardChanges { - type Builder<'a> = Builder<'a>; - type ReplyData = Empty; -} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Timeout(Duration); -impl WriteXml for DiscardChanges { - type Error = Error; - - fn write_xml(&self, writer: &mut W) -> Result<(), Self::Error> { - _ = Writer::new(writer) - .create_element("discard-changes") - .write_empty()?; - Ok(()) +impl Default for Timeout { + fn default() -> Self { + Self(Duration::from_secs(600)) } } @@ -50,37 +56,61 @@ impl WriteXml for DiscardChanges { #[must_use] pub struct Builder<'a> { ctx: &'a Context, + confirmed: bool, + confirm_timeout: Timeout, } impl Builder<'_> { - fn check_capabilities(&self, operation_name: &'static str) -> Result<(), Error> { - self.ctx - .server_capabilities() - .contains(&Capability::Candidate) - .then_some(()) - .ok_or_else(|| Error::UnsupportedOperation(operation_name, Capability::Candidate)) + pub fn confirmed(mut self, confirmed: bool) -> Result { + if confirmed && !self.can_use_confirmed() { + Err(Error::UnsupportedOperationParameter( + "", + "", + Capability::ConfirmedCommit, + )) + } else { + self.confirmed = confirmed; + Ok(self) + } } -} -impl<'a> super::Builder<'a, Commit> for Builder<'a> { - fn new(ctx: &'a Context) -> Self { - Self { ctx } + pub fn confirm_timeout(mut self, timeout: Duration) -> Result { + if self.can_use_confirmed() { + self.confirm_timeout = Timeout(timeout); + Ok(self) + } else { + Err(Error::UnsupportedOperationParameter( + "", + "", + Capability::ConfirmedCommit, + )) + } } - fn finish(self) -> Result { - self.check_capabilities("") - .map(|()| Commit { _inner: () }) + fn can_use_confirmed(&self) -> bool { + self.ctx + .server_capabilities() + .contains(&Capability::ConfirmedCommit) } } -impl<'a> super::Builder<'a, DiscardChanges> for Builder<'a> { +impl<'a> super::Builder<'a, Commit> for Builder<'a> { fn new(ctx: &'a Context) -> Self { - Self { ctx } + Self { + ctx, + confirmed: false, + confirm_timeout: Timeout::default(), + } } - fn finish(self) -> Result { - self.check_capabilities("") - .map(|()| DiscardChanges { _inner: () }) + fn finish(self) -> Result { + self.ctx + .try_operation(Capability::Candidate, "", || { + Ok(Commit { + confirmed: self.confirmed, + confirm_timeout: self.confirm_timeout, + }) + }) } } @@ -93,22 +123,41 @@ mod tests { }; #[test] - fn commit_request_to_xml() { + fn unconfirmed_request_to_xml() { let req = Request { message_id: MessageId(101), - operation: Commit { _inner: () }, + operation: Commit { + confirmed: false, + confirm_timeout: Timeout::default(), + }, }; let expect = r#"]]>]]>"#; assert_eq!(req.to_xml().unwrap(), expect); } #[test] - fn discard_changes_request_to_xml() { + fn confirmed_request_to_xml() { + let req = Request { + message_id: MessageId(101), + operation: Commit { + confirmed: true, + confirm_timeout: Timeout::default(), + }, + }; + let expect = r#"]]>]]>"#; + assert_eq!(req.to_xml().unwrap(), expect); + } + + #[test] + fn confirmed_with_timeout_request_to_xml() { let req = Request { message_id: MessageId(101), - operation: DiscardChanges { _inner: () }, + operation: Commit { + confirmed: true, + confirm_timeout: Timeout(Duration::from_secs(60)), + }, }; - let expect = r#"]]>]]>"#; + let expect = r#"60]]>]]>"#; assert_eq!(req.to_xml().unwrap(), expect); } } diff --git a/netconf/src/message/rpc/operation/discard_changes.rs b/netconf/src/message/rpc/operation/discard_changes.rs new file mode 100644 index 0000000..3b3cbae --- /dev/null +++ b/netconf/src/message/rpc/operation/discard_changes.rs @@ -0,0 +1,67 @@ +use std::io::Write; + +use quick_xml::Writer; + +use crate::{capabilities::Capability, message::rpc::Empty, session::Context, Error}; + +use super::{Operation, WriteXml}; + +#[derive(Debug, Clone, Copy)] +pub struct DiscardChanges { + // zero-sized private field to prevent direct construction + _inner: (), +} + +impl Operation for DiscardChanges { + type Builder<'a> = Builder<'a>; + type ReplyData = Empty; +} + +impl WriteXml for DiscardChanges { + type Error = Error; + + fn write_xml(&self, writer: &mut W) -> Result<(), Self::Error> { + _ = Writer::new(writer) + .create_element("discard-changes") + .write_empty()?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +#[must_use] +pub struct Builder<'a> { + ctx: &'a Context, +} + +impl<'a> super::Builder<'a, DiscardChanges> for Builder<'a> { + fn new(ctx: &'a Context) -> Self { + Self { ctx } + } + + fn finish(self) -> Result { + self.ctx + .try_operation(Capability::Candidate, "", || { + Ok(DiscardChanges { _inner: () }) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::message::{ + rpc::{MessageId, Request}, + ClientMsg, + }; + + #[test] + fn request_to_xml() { + let req = Request { + message_id: MessageId(101), + operation: DiscardChanges { _inner: () }, + }; + let expect = r#"]]>]]>"#; + assert_eq!(req.to_xml().unwrap(), expect); + } +} diff --git a/netconf/src/message/rpc/operation/mod.rs b/netconf/src/message/rpc/operation/mod.rs index cb6196d..40049a7 100644 --- a/netconf/src/message/rpc/operation/mod.rs +++ b/netconf/src/message/rpc/operation/mod.rs @@ -67,7 +67,11 @@ pub use self::kill_session::KillSession; pub mod commit; #[doc(inline)] -pub use self::commit::{Commit, DiscardChanges}; +pub use self::commit::Commit; + +pub mod discard_changes; +#[doc(inline)] +pub use self::discard_changes::DiscardChanges; pub(crate) mod close_session; pub(crate) use self::close_session::CloseSession; diff --git a/netconf/src/session.rs b/netconf/src/session.rs index f0178c5..55ecd64 100644 --- a/netconf/src/session.rs +++ b/netconf/src/session.rs @@ -96,6 +96,21 @@ impl Context { pub const fn server_capabilities(&self) -> &Capabilities { &self.server_capabilities } + + pub(crate) fn try_operation( + &self, + required_capability: Capability, + operation_name: &'static str, + finish: F, + ) -> Result + where + F: FnOnce() -> Result, + { + self.server_capabilities() + .contains(&required_capability) + .then(finish) + .ok_or_else(|| Error::UnsupportedOperation(operation_name, required_capability))? + } } #[derive(Debug)]