rototo 0.1.0-alpha.8

Control plane for runtime configuration of your application.
Documentation
use std::collections::BTreeMap;
use std::path::Path;

use crate::diagnostics::{
    DiagnosticCatalogEntry, LintDiagnostic, RototoRuleId, SemanticEntity, SourcePosition,
};
use crate::error::{Result, RototoError};
use crate::model::{CatalogLint, PackageDiff, PackageLint, VariableLint};

mod builtins;
mod catalog_schema;
mod custom;
mod diff;
mod engine;
mod evaluation_context;
mod index;
pub(crate) mod input;
mod inspect;
mod output;
mod project;
mod references;
mod runtime;
mod semantic_model;
mod source;
mod stages;
mod symbols;
mod syntax;
mod upcoming;

pub(crate) use evaluation_context::EvaluationContextCompatibility;
pub(crate) use index::parse_arm_buckets;
use index::*;
pub(crate) use input::{LintInput, OverlayDocument};
pub(crate) use inspect::inspect_snapshot;
use references::ReferenceIndex;
pub(crate) use runtime::{
    RuntimeAllocation, RuntimePackage, RuntimeQuery, RuntimeResolution, RuntimeRule,
    RuntimeSelectedValue, compile_runtime_package, compile_runtime_package_from_snapshot,
};
pub use semantic_model::{
    CatalogEntryModel, CatalogModel, DeclarationModel, EvaluationContextModel,
    EvaluationContextSampleModel, LinterModel, LinterRuleModel, ListModel, ModelEntityRef,
    ModelField, ModelLocation, ModelReferenceVia, ModelValueField, PackageExtendModel,
    PackageSemanticModel, ReferenceModel, ResolveModel, RuleModel, ValueModel,
    VariableEvaluationContextModel, VariableModel,
};
pub(crate) use symbols::{
    PackageCompletionItem, PackageCompletionItemKind, PackageDefinition, PackageDocumentSymbol,
    PackageDocumentSymbolKind, PackageHover, PackageReference,
};
pub use upcoming::{UpcomingChange, UpcomingChangeSite};

const PACKAGE_MANIFEST: &str = "rototo-package.toml";
/// Lints the package and projects its semantic and reference indexes into
/// the serializable model that tools consume instead of parsing files.
pub async fn package_semantic_model(package_root: &Path) -> Result<PackageSemanticModel> {
    let snapshot = lint_package_snapshot(LintInput::new(package_root.to_path_buf())).await?;
    Ok(snapshot.semantic_model())
}

pub async fn lint_package(package_root: &Path) -> Result<PackageLint> {
    lint_package_with_input(LintInput::new(package_root.to_path_buf())).await
}

/// Behavior scheduled to change after `now` (an RFC3339 instant): every rule
/// and query expression comparing `env.now` against a literal instant that
/// has not passed yet. These changes happen with no commit and no deploy,
/// which is exactly why tools surface them ahead of time.
pub async fn upcoming_changes(package_root: &Path, now: &str) -> Result<Vec<UpcomingChange>> {
    let snapshot = lint_package_snapshot(LintInput::new(package_root.to_path_buf())).await?;
    upcoming::upcoming_changes(&snapshot, now)
}

pub async fn diff_packages(
    before_root: &Path,
    after_root: &Path,
    context: Option<&serde_json::Value>,
) -> Result<PackageDiff> {
    diff::diff_packages(before_root, after_root, context).await
}

/// The diff evaluated under several labeled contexts at once: one set of
/// semantic changes plus lenient per-context resolution impacts. This is the
/// review-panel shape; `diff_packages` with one merged context remains the
/// CLI's.
pub async fn diff_packages_with_contexts(
    before_root: &Path,
    after_root: &Path,
    contexts: &[crate::model::LabeledContext],
) -> Result<crate::model::PackageDiffWithContexts> {
    diff::diff_packages_with_contexts(before_root, after_root, contexts).await
}

pub async fn lint_variable(package_root: &Path, id: &str) -> Result<VariableLint> {
    let lint = lint_package(package_root).await?;
    let path = format!("variables/{id}.toml");
    if !lint.documents.iter().any(|document| document.path == path) {
        return Err(RototoError::new(format!(
            "variable not found: variable://{id}"
        )));
    }

    Ok(VariableLint {
        root: lint.root,
        id: id.to_owned(),
        diagnostics: lint
            .diagnostics
            .into_iter()
            .filter(|diagnostic| diagnostic_belongs_to_variable(diagnostic, id, &path))
            .collect(),
    })
}

pub async fn lint_catalog(package_root: &Path, id: &str) -> Result<CatalogLint> {
    let lint = lint_package(package_root).await?;
    let path = format!("model/catalogs/{id}.schema.json");
    if !lint.documents.iter().any(|document| document.path == path) {
        return Err(RototoError::new(format!(
            "catalog not found: catalog://{id}"
        )));
    }

    Ok(CatalogLint {
        root: lint.root,
        id: id.to_owned(),
        diagnostics: lint
            .diagnostics
            .into_iter()
            .filter(|diagnostic| diagnostic_belongs_to_catalog(diagnostic, id, &path))
            .collect(),
    })
}

fn diagnostic_belongs_to_variable(diagnostic: &LintDiagnostic, id: &str, path: &str) -> bool {
    matches!(&diagnostic.target.entity, SemanticEntity::Variable { id: diagnostic_id } if diagnostic_id == id)
        || matches!(&diagnostic.target.entity, SemanticEntity::Value { variable, .. } if variable == id)
        || matches!(&diagnostic.target.entity, SemanticEntity::Rule { variable, .. } if variable == id)
        || diagnostic.primary.path == path
}

fn diagnostic_belongs_to_catalog(diagnostic: &LintDiagnostic, id: &str, path: &str) -> bool {
    let entries_prefix = format!("data/catalogs/{id}/");
    matches!(&diagnostic.target.entity, SemanticEntity::Catalog { id: diagnostic_id } if diagnostic_id == id)
        || matches!(&diagnostic.target.entity, SemanticEntity::CatalogEntry { catalog, .. } if catalog == id)
        || diagnostic.primary.path == path
        || diagnostic.primary.path.starts_with(&entries_prefix)
}

pub(crate) async fn lint_package_with_input(input: LintInput) -> Result<PackageLint> {
    Ok(lint_package_snapshot(input).await?.lint)
}

pub(crate) async fn lint_package_snapshot(input: LintInput) -> Result<PackageLintSnapshot> {
    engine::lint_package_snapshot(input).await
}

pub(crate) struct PackageLintSnapshot {
    pub(crate) lint: PackageLint,
    index: SemanticIndex,
    references: ReferenceIndex,
    source_texts: BTreeMap<String, String>,
}

impl PackageLintSnapshot {
    pub(crate) fn diagnostic_catalog_entries(&self) -> Vec<DiagnosticCatalogEntry> {
        let mut entries = RototoRuleId::iter()
            .map(DiagnosticCatalogEntry::from_rototo)
            .collect::<Vec<_>>();
        entries.extend(
            self.index
                .custom_lints
                .rules
                .values()
                .map(|rule| DiagnosticCatalogEntry::from_custom(&rule.definition)),
        );
        entries.sort_by(|left, right| left.rule.cmp(&right.rule));
        entries
    }

    pub(crate) fn document_symbols(&self, path: &str) -> Vec<PackageDocumentSymbol> {
        symbols::document_symbols(&self.index, path)
    }

    pub(crate) fn completion_items(
        &self,
        path: &str,
        position: SourcePosition,
    ) -> Vec<PackageCompletionItem> {
        symbols::completion_items(self, path, position)
    }

    pub(crate) fn hover(&self, path: &str, position: SourcePosition) -> Option<PackageHover> {
        symbols::hover(self, path, position)
    }

    pub(crate) fn definition(
        &self,
        path: &str,
        position: SourcePosition,
    ) -> Option<PackageDefinition> {
        symbols::definition(self, path, position)
    }

    pub(crate) fn references(
        &self,
        path: &str,
        position: SourcePosition,
        include_declaration: bool,
    ) -> Vec<PackageReference> {
        symbols::references(self, path, position, include_declaration)
    }

    pub(crate) fn evaluation_context_compatibility(&self) -> EvaluationContextCompatibility {
        evaluation_context::compatibility(self)
    }

    pub(crate) fn source_text(&self, path: &str) -> Option<&str> {
        self.source_texts.get(path).map(String::as_str)
    }
}