bgpfu-netconf 0.1.0

A toolset for working with IRR data
Documentation
use std::{fmt::Debug, io::Write};

use quick_xml::{events::BytesText, Writer};

use crate::{
    capabilities::{Capability, Requirements},
    message::WriteError,
    session::Context,
    Error,
};

use super::{params::Required, Datastore, EmptyReply, Operation, Url, WriteXml};

#[derive(Debug, Clone)]
pub struct EditConfig<D> {
    target: Datastore,
    source: Source<D>,
    default_operation: DefaultOperation,
    error_option: ErrorOption,
    test_option: TestOption,
}

impl<D> Operation for EditConfig<D>
where
    D: WriteXml + Debug + Send + Sync,
{
    const NAME: &'static str = "edit-config";
    const REQUIRED_CAPABILITIES: Requirements = Requirements::None;
    type Builder<'a> = Builder<'a, D>;
    type Reply = EmptyReply;
}

impl<D> WriteXml for EditConfig<D>
where
    D: WriteXml + Debug + Send + Sync,
{
    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
        writer
            .create_element(Self::NAME)
            .write_inner_content(|writer| {
                _ = writer
                    .create_element("target")
                    .write_inner_content(|writer| self.target.write_xml(writer))?;
                if self.default_operation.is_non_default() {
                    _ = writer
                        .create_element("default-operation")
                        .write_text_content(BytesText::new(self.default_operation.as_str()))?;
                };
                if self.error_option.is_non_default() {
                    _ = writer
                        .create_element("error-option")
                        .write_text_content(BytesText::new(self.error_option.as_str()))?;
                };
                if self.test_option.is_non_default() {
                    _ = writer
                        .create_element("test-option")
                        .write_text_content(BytesText::new(self.test_option.as_str()))?;
                };
                self.source.write_xml(writer)?;
                Ok(())
            })
            .map(|_| ())
    }
}

#[derive(Debug, Clone)]
#[must_use]
pub struct Builder<'a, D> {
    ctx: &'a Context,
    target: Required<Datastore>,
    source: Required<Source<D>>,
    default_operation: DefaultOperation,
    error_option: ErrorOption,
    test_option: TestOption,
}

impl<D> Builder<'_, D>
where
    D: WriteXml + Debug + Send + Sync,
{
    pub fn target(mut self, target: Datastore) -> Result<Self, Error> {
        target.try_as_target(self.ctx).map(|target| {
            self.target.set(target);
            self
        })
    }

    pub fn config(mut self, config: D) -> Self {
        self.source.set(Source::Config(config));
        self
    }

    pub fn url<S: AsRef<str>>(mut self, url: S) -> Result<Self, Error> {
        Url::try_new(url, self.ctx).map(|url| {
            self.source.set(Source::Url(url));
            self
        })
    }

    pub const fn default_operation(mut self, default_operation: DefaultOperation) -> Self {
        self.default_operation = default_operation;
        self
    }

    pub fn error_option(mut self, error_option: ErrorOption) -> Result<Self, Error> {
        error_option.try_use::<D>(self.ctx).map(|error_option| {
            self.error_option = error_option;
            self
        })
    }

    pub fn test_option(mut self, test_option: TestOption) -> Result<Self, Error> {
        test_option.try_use::<D>(self.ctx).map(|test_option| {
            self.test_option = test_option;
            self
        })
    }
}

impl<'a, D> super::Builder<'a, EditConfig<D>> for Builder<'a, D>
where
    D: WriteXml + Debug + Send + Sync,
{
    fn new(ctx: &'a Context) -> Self {
        Self {
            ctx,
            target: Required::init(),
            source: Required::init(),
            default_operation: DefaultOperation::default(),
            error_option: ErrorOption::default(),
            test_option: TestOption::default(),
        }
    }

    fn finish(self) -> Result<EditConfig<D>, Error> {
        Ok(EditConfig {
            target: self.target.require::<EditConfig<D>>("target")?,
            source: self.source.require::<EditConfig<D>>("config")?,
            default_operation: self.default_operation,
            error_option: self.error_option,
            test_option: self.test_option,
        })
    }
}

#[derive(Debug, Clone)]
pub enum Source<D> {
    Config(D),
    Url(Url),
}

impl<D> WriteXml for Source<D>
where
    D: WriteXml,
{
    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
        match self {
            Self::Config(config) => writer
                .create_element("config")
                .write_inner_content(|writer| config.write_xml(writer))
                .map(|_| ()),
            Self::Url(url) => url.write_xml(writer),
        }
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DefaultOperation {
    #[default]
    Merge,
    Replace,
    None,
}

impl DefaultOperation {
    fn is_non_default(self) -> bool {
        self != Self::default()
    }

    const fn as_str(self) -> &'static str {
        match self {
            Self::Merge => "merge",
            Self::Replace => "replace",
            Self::None => "none",
        }
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TestOption {
    #[default]
    TestThenSet,
    Set,
    TestOnly,
}

impl TestOption {
    fn is_non_default(self) -> bool {
        self != Self::default()
    }

    const fn as_str(self) -> &'static str {
        match self {
            Self::TestThenSet => "test-then-set",
            Self::Set => "set",
            Self::TestOnly => "test-only",
        }
    }

    fn try_use<D>(self, ctx: &Context) -> Result<Self, Error>
    where
        D: WriteXml + Debug + Send + Sync,
    {
        let required_capabilities = match self {
            Self::TestThenSet | Self::Set => {
                Requirements::Any(&[Capability::ValidateV1_0, Capability::ValidateV1_1])
            }
            Self::TestOnly => Requirements::One(Capability::ValidateV1_1),
        };
        if required_capabilities.check(ctx.server_capabilities()) {
            Ok(self)
        } else {
            Err(Error::UnsupportedOperParameterValue {
                operation_name: EditConfig::<D>::NAME,
                param_name: "test-option",
                param_value: self.as_str(),
                required_capabilities,
            })
        }
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ErrorOption {
    #[default]
    StopOnError,
    ContinueOnError,
    RollbackOnError,
}

impl ErrorOption {
    fn is_non_default(self) -> bool {
        self != Self::default()
    }

    const fn as_str(self) -> &'static str {
        match self {
            Self::StopOnError => "stop-on-error",
            Self::ContinueOnError => "continue-on-error",
            Self::RollbackOnError => "rollback-on-error",
        }
    }

    fn try_use<D>(self, ctx: &Context) -> Result<Self, Error>
    where
        D: WriteXml + Debug + Send + Sync,
    {
        let required_capabilities = match self {
            Self::StopOnError | Self::ContinueOnError => Requirements::None,
            Self::RollbackOnError => Requirements::One(Capability::RollbackOnError),
        };
        if required_capabilities.check(ctx.server_capabilities()) {
            Ok(self)
        } else {
            Err(Error::UnsupportedOperParameterValue {
                operation_name: EditConfig::<D>::NAME,
                param_name: "error-option",
                param_value: self.as_str(),
                required_capabilities,
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::{
        rpc::{operation::Opaque, MessageId, Request},
        ClientMsg,
    };

    #[test]
    fn request_to_xml() {
        let req = Request {
            message_id: MessageId(101),
            operation: EditConfig {
                target: Datastore::Running,
                source: Source::<Opaque>::Config("<foo>bar</foo>".into()),
                default_operation: DefaultOperation::default(),
                error_option: ErrorOption::default(),
                test_option: TestOption::default(),
            },
        };
        let expect = r#"<rpc message-id="101"><edit-config><target><running/></target><config><foo>bar</foo></config></edit-config></rpc>]]>]]>"#;
        assert_eq!(req.to_xml().unwrap(), expect);
    }
}