odata_client_codegen 0.1.0

Strongly-typed OData client code generation
Documentation
use std::{collections::HashMap, fmt};

use anyhow::anyhow;

use crate::entity_data_model_parse::edm;

/// Namespace & unqualified name, excluding the final `.`. See odata-csdl-json-v4.01 sec 15.3
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct QualifiedName<'a> {
    pub qualifier: &'a str,
    pub uq_name: &'a str,
}

impl<'a> QualifiedName<'a> {
    pub fn from_parsed(t_qualified_name: &'a edm::TQualifiedName) -> Result<Self, anyhow::Error> {
        let raw = t_qualified_name.0.0.as_str();

        match CategorisedIdentifier::from_raw(raw) {
            CategorisedIdentifier::NamespaceOrQualifiedName(qualifier, uq_name) => {
                Ok(QualifiedName { qualifier, uq_name })
            }
            _ => Err(anyhow!("Invalid qualified name: {}", raw)),
        }
    }

    /// Converts an alias-qualified identifier to a namespace-qualified one, using the provided
    /// alias-to-namespace lookup. Assumed to already be namespace-qualified if a matching alias is
    /// not found.
    pub fn with_namespace(self, namespace_alias_lookup: &HashMap<&str, &'a str>) -> Self {
        if let Some(ns) = namespace_alias_lookup.get(self.qualifier) {
            QualifiedName {
                qualifier: ns,
                uq_name: self.uq_name,
            }
        } else {
            self
        }
    }

    pub fn with_path(self, path: &'a str) -> TargetPath<'a> {
        let QualifiedName { qualifier, uq_name } = self;
        TargetPath {
            qualifier,
            uq_name,
            path,
        }
    }
}

impl<'a> fmt::Display for QualifiedName<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("{}.{}", self.qualifier, self.uq_name))
    }
}

/// Namespace, unqualified name, and container descendant path. Excludes final `.` and first `/`.
/// See  odata-csdl-json-v4.01 sec 15.4
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TargetPath<'a> {
    pub qualifier: &'a str,
    pub uq_name: &'a str,
    pub path: &'a str,
}

impl<'a> TargetPath<'a> {
    /// Constructs from a parsed `TPath`. If the path omits the the member name and/or qualifier,
    /// these are instead taken from `unqualified_name` and `qualifier` args respectively.
    pub fn from_parsed(
        t_path: &'a edm::TPath,
        qualified_name: QualifiedName<'a>,
    ) -> Result<Self, anyhow::Error> {
        let raw = t_path.0.0.as_str();

        match CategorisedIdentifier::from_raw(raw) {
            // TODO: it may be wrong to allow simple identifiers and/or unqualified names in `TPath`
            // position. Fix if so.
            CategorisedIdentifier::SimpleIdentifer(path) => Ok(qualified_name.with_path(path)),
            CategorisedIdentifier::UnqualifiedTargetPath(uq_name, path) => Ok(TargetPath {
                qualifier: qualified_name.qualifier,
                uq_name,
                path,
            }),
            CategorisedIdentifier::QualifiedTargetPath(qualifier, uq_name, path) => {
                Ok(TargetPath {
                    qualifier,
                    uq_name,
                    path,
                })
            }
            _ => Err(anyhow!("Invalid target path: {}", raw)),
        }
    }

    /// Converts an alias-qualified identifier to a namespace-qualified one, using the provided
    /// alias-to-namespace lookup. Assumed to already be namespace-qualified if a matching alias is
    /// not found.
    pub fn with_namespace(self, namespace_alias_lookup: &HashMap<&str, &'a str>) -> Self {
        if let Some(ns) = namespace_alias_lookup.get(self.qualifier) {
            TargetPath {
                qualifier: ns,
                uq_name: self.uq_name,
                path: self.path,
            }
        } else {
            self
        }
    }

    pub fn parent_qualified_name(self) -> QualifiedName<'a> {
        QualifiedName {
            qualifier: self.qualifier,
            uq_name: self.uq_name,
        }
    }
}

impl<'a> fmt::Display for TargetPath<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!(
            "{}.{}/{}",
            self.qualifier, self.uq_name, self.path
        ))
    }
}

enum CategorisedIdentifier<'a> {
    /// (`UnqualifiedName`)
    SimpleIdentifer(&'a str),
    /// (`Qualifier.Segments`, `UnqualifiedName`)
    NamespaceOrQualifiedName(&'a str, &'a str),
    /// (`UnqualifiedName`, `Some/Path`)
    UnqualifiedTargetPath(&'a str, &'a str),
    /// (`Qualifier.Segments`, `UnqualifiedName`, `Some/Path`)
    QualifiedTargetPath(&'a str, &'a str, &'a str),
    /// Dots appear after slashes or some other invalid pattern
    Invalid,
}

impl<'a> CategorisedIdentifier<'a> {
    fn from_raw(raw: &'a str) -> Self {
        let (name, path) = match raw.find('/') {
            Some(slash_start) => {
                let slash_end = slash_start + unicode_char_size('/');
                (&raw[..slash_start], Some(&raw[slash_end..]))
            }
            None => (raw, None),
        };

        if let Some(path) = path {
            if path.contains('.') {
                return CategorisedIdentifier::Invalid;
            }
        }

        let (qualifier, unqualified_name) = match name.rfind('.') {
            Some(dot_start) => {
                let dot_end = dot_start + unicode_char_size('.');
                (Some(&name[..dot_start]), &name[dot_end..])
            }
            None => (None, name),
        };

        match (qualifier, path) {
            (None, None) => CategorisedIdentifier::SimpleIdentifer(unqualified_name),
            (Some(qualifier), None) => {
                CategorisedIdentifier::NamespaceOrQualifiedName(qualifier, unqualified_name)
            }
            (None, Some(path)) => CategorisedIdentifier::UnqualifiedTargetPath(name, path),
            (Some(qualifier), Some(path)) => {
                CategorisedIdentifier::QualifiedTargetPath(qualifier, unqualified_name, path)
            }
        }
    }
}

fn unicode_char_size(c: char) -> usize {
    let mut utf8_encoded = [0; 4];
    c.encode_utf8(&mut utf8_encoded).len()
}