Skip to content

Commit

Permalink
implement :confirmed-commit capability
Browse files Browse the repository at this point in the history
  • Loading branch information
benmaddison committed Dec 20, 2023
1 parent e752c7e commit 032a46f
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 44 deletions.
5 changes: 5 additions & 0 deletions netconf/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -114,6 +116,7 @@ pub enum Capability {
Base(Base),
WritableRunning,
Candidate,
ConfirmedCommit,
Unknown(String),
}

Expand All @@ -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())),
}
}
Expand All @@ -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(),
}
}
Expand Down
3 changes: 3 additions & 0 deletions netconf/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Capability>),

Expand Down
135 changes: 92 additions & 43 deletions netconf/src/message/rpc/operation/commit.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Write;
use std::{io::Write, time::Duration};

use quick_xml::Writer;

Expand All @@ -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 {
Expand All @@ -20,67 +21,96 @@ impl WriteXml for Commit {
type Error = Error;

fn write_xml<W: Write>(&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<W: Write>(&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))
}
}

#[derive(Debug, Clone)]
#[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<Self, Error> {
if confirmed && !self.can_use_confirmed() {
Err(Error::UnsupportedOperationParameter(
"<commit>",
"<confirmed/>",
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<Self, Error> {
if self.can_use_confirmed() {
self.confirm_timeout = Timeout(timeout);
Ok(self)
} else {
Err(Error::UnsupportedOperationParameter(
"<commit>",
"<confirm-timeout>",
Capability::ConfirmedCommit,
))
}
}

fn finish(self) -> Result<Commit, Error> {
self.check_capabilities("<commit/>")
.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<DiscardChanges, Error> {
self.check_capabilities("<discard-changes/>")
.map(|()| DiscardChanges { _inner: () })
fn finish(self) -> Result<Commit, Error> {
self.ctx
.try_operation(Capability::Candidate, "<commit/>", || {
Ok(Commit {
confirmed: self.confirmed,
confirm_timeout: self.confirm_timeout,
})
})
}
}

Expand All @@ -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#"<rpc message-id="101"><commit/></rpc>]]>]]>"#;
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#"<rpc message-id="101"><commit><confirmed/></commit></rpc>]]>]]>"#;
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#"<rpc message-id="101"><discard-changes/></rpc>]]>]]>"#;
let expect = r#"<rpc message-id="101"><commit><confirmed/><confirm-timeout>60</confirm-timeout></commit></rpc>]]>]]>"#;
assert_eq!(req.to_xml().unwrap(), expect);
}
}
67 changes: 67 additions & 0 deletions netconf/src/message/rpc/operation/discard_changes.rs
Original file line number Diff line number Diff line change
@@ -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<W: Write>(&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<DiscardChanges, Error> {
self.ctx
.try_operation(Capability::Candidate, "<discard-changes/>", || {
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#"<rpc message-id="101"><discard-changes/></rpc>]]>]]>"#;
assert_eq!(req.to_xml().unwrap(), expect);
}
}
6 changes: 5 additions & 1 deletion netconf/src/message/rpc/operation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions netconf/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ impl Context {
pub const fn server_capabilities(&self) -> &Capabilities {
&self.server_capabilities
}

pub(crate) fn try_operation<O, F>(
&self,
required_capability: Capability,
operation_name: &'static str,
finish: F,
) -> Result<O, Error>
where
F: FnOnce() -> Result<O, Error>,
{
self.server_capabilities()
.contains(&required_capability)
.then(finish)
.ok_or_else(|| Error::UnsupportedOperation(operation_name, required_capability))?
}
}

#[derive(Debug)]
Expand Down

0 comments on commit 032a46f

Please sign in to comment.