netconf/message/rpc/operation/
edit_config.rs

1use std::{fmt::Debug, io::Write};
2
3use quick_xml::{events::BytesText, Writer};
4
5use crate::{
6    capabilities::{Capability, Requirements},
7    message::WriteError,
8    session::Context,
9    Error,
10};
11
12use super::{params::Required, Datastore, EmptyReply, Operation, Url, WriteXml};
13
14#[derive(Debug, Clone)]
15pub struct EditConfig<D> {
16    target: Datastore,
17    source: Source<D>,
18    default_operation: DefaultOperation,
19    error_option: ErrorOption,
20    test_option: TestOption,
21}
22
23impl<D> Operation for EditConfig<D>
24where
25    D: WriteXml + Debug + Send + Sync,
26{
27    const NAME: &'static str = "edit-config";
28    const REQUIRED_CAPABILITIES: Requirements = Requirements::None;
29    type Builder<'a> = Builder<'a, D>;
30    type Reply = EmptyReply;
31}
32
33impl<D> WriteXml for EditConfig<D>
34where
35    D: WriteXml + Debug + Send + Sync,
36{
37    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
38        writer
39            .create_element(Self::NAME)
40            .write_inner_content(|writer| {
41                _ = writer
42                    .create_element("target")
43                    .write_inner_content(|writer| self.target.write_xml(writer))?;
44                if self.default_operation.is_non_default() {
45                    _ = writer
46                        .create_element("default-operation")
47                        .write_text_content(BytesText::new(self.default_operation.as_str()))?;
48                };
49                if self.error_option.is_non_default() {
50                    _ = writer
51                        .create_element("error-option")
52                        .write_text_content(BytesText::new(self.error_option.as_str()))?;
53                };
54                if self.test_option.is_non_default() {
55                    _ = writer
56                        .create_element("test-option")
57                        .write_text_content(BytesText::new(self.test_option.as_str()))?;
58                };
59                self.source.write_xml(writer)?;
60                Ok(())
61            })
62            .map(|_| ())
63    }
64}
65
66#[derive(Debug, Clone)]
67#[must_use]
68pub struct Builder<'a, D> {
69    ctx: &'a Context,
70    target: Required<Datastore>,
71    source: Required<Source<D>>,
72    default_operation: DefaultOperation,
73    error_option: ErrorOption,
74    test_option: TestOption,
75}
76
77impl<D> Builder<'_, D>
78where
79    D: WriteXml + Debug + Send + Sync,
80{
81    pub fn target(mut self, target: Datastore) -> Result<Self, Error> {
82        target.try_as_target(self.ctx).map(|target| {
83            self.target.set(target);
84            self
85        })
86    }
87
88    pub fn config(mut self, config: D) -> Self {
89        self.source.set(Source::Config(config));
90        self
91    }
92
93    pub fn url<S: AsRef<str>>(mut self, url: S) -> Result<Self, Error> {
94        Url::try_new(url, self.ctx).map(|url| {
95            self.source.set(Source::Url(url));
96            self
97        })
98    }
99
100    pub const fn default_operation(mut self, default_operation: DefaultOperation) -> Self {
101        self.default_operation = default_operation;
102        self
103    }
104
105    pub fn error_option(mut self, error_option: ErrorOption) -> Result<Self, Error> {
106        error_option.try_use::<D>(self.ctx).map(|error_option| {
107            self.error_option = error_option;
108            self
109        })
110    }
111
112    pub fn test_option(mut self, test_option: TestOption) -> Result<Self, Error> {
113        test_option.try_use::<D>(self.ctx).map(|test_option| {
114            self.test_option = test_option;
115            self
116        })
117    }
118}
119
120impl<'a, D> super::Builder<'a, EditConfig<D>> for Builder<'a, D>
121where
122    D: WriteXml + Debug + Send + Sync,
123{
124    fn new(ctx: &'a Context) -> Self {
125        Self {
126            ctx,
127            target: Required::init(),
128            source: Required::init(),
129            default_operation: DefaultOperation::default(),
130            error_option: ErrorOption::default(),
131            test_option: TestOption::default(),
132        }
133    }
134
135    fn finish(self) -> Result<EditConfig<D>, Error> {
136        Ok(EditConfig {
137            target: self.target.require::<EditConfig<D>>("target")?,
138            source: self.source.require::<EditConfig<D>>("config")?,
139            default_operation: self.default_operation,
140            error_option: self.error_option,
141            test_option: self.test_option,
142        })
143    }
144}
145
146#[derive(Debug, Clone)]
147pub enum Source<D> {
148    Config(D),
149    Url(Url),
150}
151
152impl<D> WriteXml for Source<D>
153where
154    D: WriteXml,
155{
156    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
157        match self {
158            Self::Config(config) => writer
159                .create_element("config")
160                .write_inner_content(|writer| config.write_xml(writer))
161                .map(|_| ()),
162            Self::Url(url) => url.write_xml(writer),
163        }
164    }
165}
166
167#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
168pub enum DefaultOperation {
169    #[default]
170    Merge,
171    Replace,
172    None,
173}
174
175impl DefaultOperation {
176    fn is_non_default(self) -> bool {
177        self != Self::default()
178    }
179
180    const fn as_str(self) -> &'static str {
181        match self {
182            Self::Merge => "merge",
183            Self::Replace => "replace",
184            Self::None => "none",
185        }
186    }
187}
188
189#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
190pub enum TestOption {
191    #[default]
192    TestThenSet,
193    Set,
194    TestOnly,
195}
196
197impl TestOption {
198    fn is_non_default(self) -> bool {
199        self != Self::default()
200    }
201
202    const fn as_str(self) -> &'static str {
203        match self {
204            Self::TestThenSet => "test-then-set",
205            Self::Set => "set",
206            Self::TestOnly => "test-only",
207        }
208    }
209
210    fn try_use<D>(self, ctx: &Context) -> Result<Self, Error>
211    where
212        D: WriteXml + Debug + Send + Sync,
213    {
214        let required_capabilities = match self {
215            Self::TestThenSet | Self::Set => {
216                Requirements::Any(&[Capability::ValidateV1_0, Capability::ValidateV1_1])
217            }
218            Self::TestOnly => Requirements::One(Capability::ValidateV1_1),
219        };
220        if required_capabilities.check(ctx.server_capabilities()) {
221            Ok(self)
222        } else {
223            Err(Error::UnsupportedOperParameterValue {
224                operation_name: EditConfig::<D>::NAME,
225                param_name: "test-option",
226                param_value: self.as_str(),
227                required_capabilities,
228            })
229        }
230    }
231}
232
233#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
234pub enum ErrorOption {
235    #[default]
236    StopOnError,
237    ContinueOnError,
238    RollbackOnError,
239}
240
241impl ErrorOption {
242    fn is_non_default(self) -> bool {
243        self != Self::default()
244    }
245
246    const fn as_str(self) -> &'static str {
247        match self {
248            Self::StopOnError => "stop-on-error",
249            Self::ContinueOnError => "continue-on-error",
250            Self::RollbackOnError => "rollback-on-error",
251        }
252    }
253
254    fn try_use<D>(self, ctx: &Context) -> Result<Self, Error>
255    where
256        D: WriteXml + Debug + Send + Sync,
257    {
258        let required_capabilities = match self {
259            Self::StopOnError | Self::ContinueOnError => Requirements::None,
260            Self::RollbackOnError => Requirements::One(Capability::RollbackOnError),
261        };
262        if required_capabilities.check(ctx.server_capabilities()) {
263            Ok(self)
264        } else {
265            Err(Error::UnsupportedOperParameterValue {
266                operation_name: EditConfig::<D>::NAME,
267                param_name: "error-option",
268                param_value: self.as_str(),
269                required_capabilities,
270            })
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::message::{
279        rpc::{operation::Opaque, MessageId, Request},
280        ClientMsg,
281    };
282
283    #[test]
284    fn request_to_xml() {
285        let req = Request {
286            message_id: MessageId(101),
287            operation: EditConfig {
288                target: Datastore::Running,
289                source: Source::<Opaque>::Config("<foo>bar</foo>".into()),
290                default_operation: DefaultOperation::default(),
291                error_option: ErrorOption::default(),
292                test_option: TestOption::default(),
293            },
294        };
295        let expect = r#"<rpc message-id="101"><edit-config><target><running/></target><config><foo>bar</foo></config></edit-config></rpc>]]>]]>"#;
296        assert_eq!(req.to_xml().unwrap(), expect);
297    }
298}