osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use crate::completion::model::{CommandLine, CompletionContext, CompletionNode, CompletionTree};
use std::collections::BTreeSet;

pub(crate) struct ResolvedNodes<'a> {
    pub(crate) context_node: &'a CompletionNode,
    pub(crate) flag_scope_node: &'a CompletionNode,
}

pub(crate) struct TreeResolver<'a> {
    tree: &'a CompletionTree,
}

impl<'a> TreeResolver<'a> {
    pub(crate) fn new(tree: &'a CompletionTree) -> Self {
        Self { tree }
    }

    pub(crate) fn matched_command_len_tokens(&self, tokens: &[String]) -> usize {
        let mut node = &self.tree.root;
        let mut matched = 0usize;

        for token in tokens {
            if token == "|" || token.starts_with('-') {
                break;
            }
            let Some(child) = node.children.get(token) else {
                break;
            };
            matched += 1;
            if child.value_key || child.value_leaf {
                break;
            }
            node = child;
        }

        matched
    }

    pub(crate) fn resolved_nodes(&self, context: &CompletionContext) -> ResolvedNodes<'a> {
        ResolvedNodes {
            context_node: self.resolve_exact_or_root(&context.matched_path),
            flag_scope_node: self.resolve_exact_or_root(&context.flag_scope_path),
        }
    }

    pub(crate) fn resolve_exact_or_root(&self, path: &[String]) -> &'a CompletionNode {
        self.resolve_exact(path).unwrap_or(&self.tree.root)
    }

    pub(crate) fn resolve_flag_scope_path(&self, matched_path: &[String]) -> Vec<String> {
        let floor = if matched_path.is_empty() { 0 } else { 1 };
        for i in (floor..=matched_path.len()).rev() {
            let prefix = &matched_path[..i];
            let Some(node) = self.resolve_exact(prefix) else {
                continue;
            };
            if !node.flags.is_empty() {
                return prefix.to_vec();
            }
        }
        if matched_path.is_empty() {
            Vec::new()
        } else {
            matched_path.to_vec()
        }
    }

    pub(crate) fn scoped_flag_names(&self, matched_path: &[String]) -> BTreeSet<String> {
        let mut scoped_flags = BTreeSet::new();
        for i in (0..=matched_path.len()).rev() {
            let path = &matched_path[..i];
            if let Some(node) = self.resolve_exact(path) {
                scoped_flags.extend(node.flags.keys().cloned());
            }
        }
        scoped_flags
    }

    pub(crate) fn resolve_context(&self, path: &[String]) -> (&'a CompletionNode, Vec<String>) {
        let mut node = &self.tree.root;
        let mut matched = Vec::new();

        for segment in path {
            let Some(next) = node.children.get(segment) else {
                break;
            };
            node = next;
            matched.push(segment.clone());
            if node.value_leaf {
                break;
            }
        }

        (node, matched)
    }

    pub(crate) fn resolve_exact(&self, path: &[String]) -> Option<&'a CompletionNode> {
        let (node, matched) = self.resolve_context(path);
        (matched.len() == path.len()).then_some(node)
    }
}

pub(crate) struct ProviderSelection<'a> {
    provider: Option<&'a str>,
    normalized_os: Option<String>,
}

impl<'a> ProviderSelection<'a> {
    pub(crate) fn from_command(cmd: &'a CommandLine) -> Self {
        let provider = cmd
            .flag_values("--provider")
            .and_then(|values| values.first())
            .map(String::as_str)
            .filter(|value| !value.trim().is_empty())
            .or_else(|| cmd.has_flag("--nrec").then_some("nrec"))
            .or_else(|| cmd.has_flag("--vmware").then_some("vmware"));
        let normalized_os = cmd
            .flag_values("--os")
            .and_then(|values| values.first())
            .map(|value| normalize_token(value));

        Self {
            provider,
            normalized_os,
        }
    }

    pub(crate) fn name(&self) -> Option<&'a str> {
        self.provider
    }

    pub(crate) fn normalized_os(&self) -> Option<&str> {
        self.normalized_os.as_deref()
    }
}

fn normalize_token(value: &str) -> String {
    value
        .trim()
        .chars()
        .flat_map(char::to_lowercase)
        .collect::<String>()
        .replace([' ', '-', '_'], "")
}