eska-presentation 0.22.1

Shared localization and path presentation for ESKA tools
Documentation
//! Localized application configuration errors; core retains only structured values.

use std::path::Path;

use crate::{
    localization::{LocalizationValue, Localizer},
    path_output,
};
use eska_project_core::application::{ContextError, SettingsError};
use serde_json::{Value, json};

/// Preserve actionable library coordinates in locale-independent CLI and RPC error details.
#[must_use]
pub fn context_data(error: &ContextError) -> Value {
    let path = |value: &Path| {
        let (value, encoding) = crate::encoding::json_path(value.as_os_str());
        json!({"value":value,"encoding":encoding})
    };
    match error {
        ContextError::LibraryConfiguration { name, source } => json!({
            "reason":"library_configuration","library":name,
            "configuration":source.path().map(path),"cause":source.code()
        }),
        ContextError::LibrarySource {
            name,
            config,
            path: source_path,
            source,
        } => json!({
            "reason":"library_source","library":name,"configuration":path(config),
            "path":path(source_path),"cause":context_data(source)
        }),
        ContextError::UnknownApplication {
            requested,
            available,
        } => json!({"reason":"unknown_application","requested":requested,"available":available}),
        ContextError::AmbiguousApplication { available } => {
            json!({"reason":"ambiguous_application","available":available})
        }
        ContextError::EmptyWorkspace => json!({"reason":"empty_workspace"}),
        ContextError::Discovery(_) => json!({"reason":"project_discovery"}),
        ContextError::Source(_) => json!({"reason":"source"}),
        ContextError::Settings(_) => json!({"reason":"application_settings"}),
    }
}

/// Explain catalog failures without exposing Rust debug output or translated JSON fields.
#[must_use]
pub fn library_message(error: &ContextError, localizer: &Localizer) -> Option<String> {
    match error {
        ContextError::LibraryConfiguration { name, source } => {
            let detail = super::global::present_global_config_error(source, localizer);
            Some(localizer.format(
                "analysis-library-config-error",
                &[
                    ("name", LocalizationValue::Text(name)),
                    ("detail", LocalizationValue::Text(&detail)),
                ],
            ))
        }
        ContextError::LibrarySource {
            name, config, path, ..
        } => Some(localizer.format(
            "analysis-library-source-error",
            &[
                ("name", LocalizationValue::Text(name)),
                (
                    "config",
                    LocalizationValue::Text(&path_output::display_path(config)),
                ),
                (
                    "path",
                    LocalizationValue::Text(&path_output::display_path(path)),
                ),
            ],
        )),
        _ => None,
    }
}

/// Preserve the manifest location while rendering a specific configuration failure.
pub fn present(
    localizer: &Localizer,
    path: &Path,
    error: &SettingsError,
    hyperlinks: bool,
) -> String {
    let detail = reason(localizer, error);
    localizer.format(
        "analysis-config-invalid",
        &[
            (
                "path",
                LocalizationValue::Text(&path_output::render(path, hyperlinks)),
            ),
            ("detail", LocalizationValue::Text(&detail)),
        ],
    )
}

/// Describe the rejected relationship without turning machine-facing names into translated keys.
fn reason(localizer: &Localizer, error: &SettingsError) -> String {
    match error {
        SettingsError::ExtensionScopeLocation => {
            localizer.text("analysis-extension-scope-location")
        }
        SettingsError::EmptyConfigurationSource => localizer.text("analysis-reference-empty"),
        SettingsError::EmptyConfigurationLibrary => localizer.text("analysis-library-empty"),
        SettingsError::ConfigurationReferenceScope => localizer.text("analysis-reference-scope"),
        SettingsError::ManagedAnalyzerField { field } => localizer.format(
            "analysis-managed-field",
            &[("field", LocalizationValue::Text(field))],
        ),
        SettingsError::InvalidAnalyzerPolicy => localizer.text("analysis-policy-invalid"),
        SettingsError::InvalidName { field, value } => localizer.format(
            "analysis-name-invalid",
            &[
                ("field", LocalizationValue::Text(field)),
                ("value", LocalizationValue::Text(value)),
            ],
        ),
        SettingsError::InvalidPlatformVersion { value } => localizer.format(
            "analysis-platform-invalid",
            &[("value", LocalizationValue::Text(value))],
        ),
        SettingsError::UnknownDefaultApplication { name } => localizer.format(
            "analysis-default-unknown",
            &[("name", LocalizationValue::Text(name.as_str()))],
        ),
        SettingsError::DuplicateMember {
            application,
            member,
        } => member_reason(
            localizer,
            "analysis-member-duplicate",
            application.as_str(),
            member.as_str(),
        ),
        SettingsError::DependencyOutsideApplication {
            application,
            member,
        } => member_reason(
            localizer,
            "analysis-dependency-outside",
            application.as_str(),
            member.as_str(),
        ),
        SettingsError::DependencyCycle { application } => localizer.format(
            "analysis-dependency-cycle",
            &[("application", LocalizationValue::Text(application.as_str()))],
        ),
        SettingsError::UnknownMember {
            application,
            member,
        } => member_reason(
            localizer,
            "analysis-member-unknown",
            application.as_str(),
            member.as_str(),
        ),
        SettingsError::WrongMemberType {
            application,
            member,
            expected,
            actual,
        } => {
            let expected = expected
                .project_types()
                .iter()
                .map(|kind| kind.as_str())
                .collect::<Vec<_>>()
                .join("|");
            localizer.format(
                "analysis-member-type",
                &[
                    ("application", LocalizationValue::Text(application.as_str())),
                    ("member", LocalizationValue::Text(member.as_str())),
                    ("expected", LocalizationValue::Text(&expected)),
                    ("actual", LocalizationValue::Text(actual.as_str())),
                ],
            )
        }
        SettingsError::MemberAnalysisUnsupported { member } => localizer.format(
            "analysis-member-settings",
            &[("member", LocalizationValue::Text(member.as_str()))],
        ),
        SettingsError::TargetPlatformScope => localizer.text("analysis-platform-scope"),
    }
}

/// Reuse the same application/member coordinates for duplicate, unknown and invalid edges.
fn member_reason(localizer: &Localizer, key: &str, application: &str, member: &str) -> String {
    localizer.format(
        key,
        &[
            ("application", LocalizationValue::Text(application)),
            ("member", LocalizationValue::Text(member)),
        ],
    )
}