netconf/message/rpc/operation/
mod.rs

1use std::{
2    fmt::{self, Debug, Display},
3    io::Write,
4    ops::Deref,
5    sync::Arc,
6    time::Duration,
7};
8
9use iri_string::types::UriStr;
10use quick_xml::{
11    events::{BytesStart, BytesText},
12    NsReader, Writer,
13};
14use uuid::Uuid;
15
16use crate::{
17    capabilities::{Capability, Requirements},
18    message::{ReadError, ReadXml, WriteError, WriteXml},
19    session::Context,
20    Error,
21};
22
23use super::{DataReply, EmptyReply, IntoResult};
24
25pub trait Operation: Debug + WriteXml + Send + Sync + Sized {
26    const NAME: &'static str;
27    const REQUIRED_CAPABILITIES: Requirements;
28
29    type Builder<'a>: Builder<'a, Self>;
30    type Reply: Debug + ReadXml + IntoResult;
31
32    fn new<'a, F>(ctx: &'a Context, build_fn: F) -> Result<Self, Error>
33    where
34        F: FnOnce(Self::Builder<'a>) -> Result<Self, Error>,
35    {
36        Self::REQUIRED_CAPABILITIES
37            .check(ctx.server_capabilities())
38            .then(|| Self::Builder::new(ctx).build(build_fn))
39            .ok_or(Error::UnsupportedOperation {
40                operation_name: Self::NAME,
41                required_capabilities: Self::REQUIRED_CAPABILITIES,
42            })?
43    }
44}
45
46pub trait Builder<'a, O: Operation>: Debug + Sized {
47    fn new(ctx: &'a Context) -> Self;
48
49    fn finish(self) -> Result<O, Error>;
50
51    fn build<F>(self, build_fn: F) -> Result<O, Error>
52    where
53        F: FnOnce(Self) -> Result<O, Error>,
54    {
55        build_fn(self)
56    }
57}
58
59mod params;
60
61pub mod get;
62#[doc(inline)]
63pub use self::get::Get;
64
65pub mod get_config;
66#[doc(inline)]
67pub use self::get_config::GetConfig;
68
69pub mod edit_config;
70#[doc(inline)]
71pub use self::edit_config::EditConfig;
72
73pub mod copy_config;
74#[doc(inline)]
75pub use self::copy_config::CopyConfig;
76
77pub mod delete_config;
78#[doc(inline)]
79pub use self::delete_config::DeleteConfig;
80
81pub mod lock;
82#[doc(inline)]
83pub use self::lock::{Lock, Unlock};
84
85pub mod kill_session;
86#[doc(inline)]
87pub use self::kill_session::KillSession;
88
89pub mod commit;
90#[doc(inline)]
91pub use self::commit::Commit;
92
93pub mod cancel_commit;
94#[doc(inline)]
95pub use self::cancel_commit::CancelCommit;
96
97pub mod discard_changes;
98#[doc(inline)]
99pub use self::discard_changes::DiscardChanges;
100
101pub mod validate;
102#[doc(inline)]
103pub use self::validate::Validate;
104
105pub(crate) mod close_session;
106pub(crate) use self::close_session::CloseSession;
107
108#[cfg(feature = "junos")]
109pub mod junos;
110
111#[derive(Debug, Default, Copy, Clone)]
112pub enum Datastore {
113    #[default]
114    Running,
115    Candidate,
116    Startup,
117}
118
119impl Datastore {
120    fn try_as_source(self, ctx: &Context) -> Result<Self, Error> {
121        let required_capabilities = match self {
122            Self::Running => Requirements::None,
123            Self::Candidate => Requirements::One(Capability::Candidate),
124            Self::Startup => Requirements::One(Capability::Startup),
125        };
126        if required_capabilities.check(ctx.server_capabilities()) {
127            Ok(self)
128        } else {
129            Err(Error::UnsupportedSource {
130                datastore: self,
131                required_capabilities,
132            })
133        }
134    }
135
136    fn try_as_target(self, ctx: &Context) -> Result<Self, Error> {
137        let required_capabilities = match self {
138            Self::Running => Requirements::One(Capability::WritableRunning),
139            Self::Candidate => Requirements::One(Capability::Candidate),
140            Self::Startup => Requirements::One(Capability::Startup),
141        };
142        if required_capabilities.check(ctx.server_capabilities()) {
143            Ok(self)
144        } else {
145            Err(Error::UnsupportedTarget {
146                datastore: self,
147                required_capabilities,
148            })
149        }
150    }
151
152    fn try_as_lock_target(self, ctx: &Context) -> Result<Self, Error> {
153        let required_capabilities = match self {
154            Self::Running => Requirements::None,
155            Self::Candidate => Requirements::One(Capability::Candidate),
156            Self::Startup => Requirements::One(Capability::Startup),
157        };
158        if required_capabilities.check(ctx.server_capabilities()) {
159            Ok(self)
160        } else {
161            Err(Error::UnsupportedLockTarget {
162                datastore: self,
163                required_capabilities,
164            })
165        }
166    }
167
168    const fn as_str(self) -> &'static str {
169        match self {
170            Self::Running => "running",
171            Self::Candidate => "candidate",
172            Self::Startup => "startup",
173        }
174    }
175}
176
177impl WriteXml for Datastore {
178    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
179        _ = writer.create_element(self.as_str()).write_empty()?;
180        Ok(())
181    }
182}
183
184#[derive(Debug, Clone)]
185pub enum Source {
186    Datastore(Datastore),
187    Config(String),
188    Url(Url),
189}
190
191impl WriteXml for Source {
192    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
193        match self {
194            Self::Datastore(datastore) => datastore.write_xml(writer)?,
195            Self::Config(config) => {
196                _ = writer
197                    .create_element("config")
198                    .write_inner_content(|writer| {
199                        writer
200                            .get_mut()
201                            .write_all(config.as_bytes())
202                            .map_err(|err| WriteError::Other(err.into()))
203                    })?;
204            }
205            Self::Url(url) => url.write_xml(writer)?,
206        };
207        Ok(())
208    }
209}
210
211#[derive(Debug, Clone)]
212pub enum Filter {
213    Subtree(String),
214    XPath(String),
215}
216
217impl Filter {
218    const fn as_str(&self) -> &'static str {
219        match self {
220            Self::Subtree(_) => "subtree",
221            Self::XPath(_) => "xpath",
222        }
223    }
224
225    fn try_use(self, ctx: &Context) -> Result<Self, Error> {
226        let required_capabilities = match self {
227            Self::Subtree(_) => Requirements::None,
228            Self::XPath(_) => Requirements::One(Capability::XPath),
229        };
230        if required_capabilities.check(ctx.server_capabilities()) {
231            Ok(self)
232        } else {
233            Err(Error::UnsupportedFilterType {
234                filter: self.as_str(),
235                required_capabilities,
236            })
237        }
238    }
239}
240
241impl WriteXml for Filter {
242    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
243        let elem = writer
244            .create_element("filter")
245            .with_attribute(("type", self.as_str()));
246        _ = match self {
247            Self::Subtree(filter) => elem.write_inner_content(|writer| {
248                writer
249                    .get_mut()
250                    .write_all(filter.as_bytes())
251                    .map_err(|err| WriteError::Other(err.into()))
252            })?,
253            Self::XPath(select) => elem
254                .with_attribute(("select", select.as_str()))
255                .write_empty()?,
256        };
257        Ok(())
258    }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct Opaque {
263    inner: Arc<str>,
264}
265
266impl<A: AsRef<str>> From<A> for Opaque {
267    fn from(value: A) -> Self {
268        let inner = value.as_ref().into();
269        Self { inner }
270    }
271}
272
273impl ReadXml for Opaque {
274    #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
275    fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
276        let end = start.to_end();
277        let inner = reader.read_text(end.name())?.into();
278        Ok(Self { inner })
279    }
280}
281
282impl WriteXml for Opaque {
283    #[tracing::instrument(skip(writer))]
284    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
285        writer
286            .get_mut()
287            .write_all(self.as_bytes())
288            .map_err(|err| WriteError::Other(err.into()))
289    }
290}
291
292impl Display for Opaque {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        Display::fmt(&self.inner, f)
295    }
296}
297
298impl Deref for Opaque {
299    type Target = str;
300
301    fn deref(&self) -> &Self::Target {
302        &self.inner
303    }
304}
305// impl AsRef<str> for Opaque {
306//     fn as_ref(&self) -> &str {
307//         self.inner.as_ref()
308//     }
309// }
310
311#[derive(Debug, Clone)]
312pub struct Url {
313    inner: Arc<UriStr>,
314}
315
316impl Url {
317    fn try_new<S: AsRef<str>>(s: S, ctx: &Context) -> Result<Self, Error> {
318        let url = UriStr::new(s.as_ref())?;
319        ctx.server_capabilities()
320            .iter()
321            .filter_map(|capability| {
322                if let Capability::Url(schemes) = capability {
323                    Some(schemes.iter())
324                } else {
325                    None
326                }
327            })
328            .flatten()
329            .find(|&scheme| url.scheme_str() == scheme.as_ref())
330            .ok_or_else(|| Error::UnsupportedUrlScheme { url: url.into() })
331            .map(|_| Self { inner: url.into() })
332    }
333}
334
335impl AsRef<str> for Url {
336    fn as_ref(&self) -> &str {
337        self.inner.as_str()
338    }
339}
340
341impl Display for Url {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        Display::fmt(&self.inner.as_ref(), f)
344    }
345}
346
347impl WriteXml for Url {
348    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
349        _ = writer
350            .create_element("url")
351            .write_text_content(BytesText::new(self.inner.as_str()))?;
352        Ok(())
353    }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct Token {
358    inner: Arc<str>,
359}
360
361impl Token {
362    pub fn new<S: AsRef<str>>(token: S) -> Self {
363        let inner = token.as_ref().into();
364        Self { inner }
365    }
366
367    #[must_use]
368    pub fn generate() -> Self {
369        let inner = Arc::from(
370            &*Uuid::new_v4()
371                .urn()
372                .encode_lower(&mut Uuid::encode_buffer()),
373        );
374        Self { inner }
375    }
376}
377
378impl Display for Token {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        Display::fmt(&self.inner, f)
381    }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385struct Timeout(Duration);
386
387impl Timeout {
388    fn seconds(&self) -> BytesText<'static> {
389        BytesText::new(&self.0.as_secs().to_string()).into_owned()
390    }
391
392    #[cfg(feature = "junos")]
393    fn minutes(&self) -> BytesText<'static> {
394        BytesText::new(&self.0.as_secs().div_ceil(60).to_string()).into_owned()
395    }
396}
397
398impl Default for Timeout {
399    fn default() -> Self {
400        Self(Duration::from_secs(600))
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    use quick_xml::events::Event;
409
410    #[test]
411    fn reply_from_xml() {
412        let reply = "<configuration><top/></configuration>";
413        let expect = Opaque {
414            inner: reply.into(),
415        };
416        let msg = format!("<data>{reply}</data>");
417        let mut reader = NsReader::from_str(msg.as_str());
418        _ = reader.trim_text(true);
419        if let Event::Start(start) = reader.read_event().unwrap() {
420            assert_eq!(Opaque::read_xml(&mut reader, &start).unwrap(), expect);
421        } else {
422            panic!("missing <data> tag")
423        }
424    }
425}