Skip to main content

eska_presentation/diagnostics/
application.rs

1//! Localized application configuration errors; core retains only structured values.
2
3use std::path::Path;
4
5use crate::{
6    localization::{LocalizationValue, Localizer},
7    path_output,
8};
9use eska_project_core::application::{ContextError, SettingsError};
10use serde_json::{Value, json};
11
12/// Preserve actionable library coordinates in locale-independent CLI and RPC error details.
13#[must_use]
14pub fn context_data(error: &ContextError) -> Value {
15    let path = |value: &Path| {
16        let (value, encoding) = crate::encoding::json_path(value.as_os_str());
17        json!({"value":value,"encoding":encoding})
18    };
19    match error {
20        ContextError::LibraryConfiguration { name, source } => json!({
21            "reason":"library_configuration","library":name,
22            "configuration":source.path().map(path),"cause":source.code()
23        }),
24        ContextError::LibrarySource {
25            name,
26            config,
27            path: source_path,
28            source,
29        } => json!({
30            "reason":"library_source","library":name,"configuration":path(config),
31            "path":path(source_path),"cause":context_data(source)
32        }),
33        ContextError::UnknownApplication {
34            requested,
35            available,
36        } => json!({"reason":"unknown_application","requested":requested,"available":available}),
37        ContextError::AmbiguousApplication { available } => {
38            json!({"reason":"ambiguous_application","available":available})
39        }
40        ContextError::EmptyWorkspace => json!({"reason":"empty_workspace"}),
41        ContextError::Discovery(_) => json!({"reason":"project_discovery"}),
42        ContextError::Source(_) => json!({"reason":"source"}),
43        ContextError::Settings(_) => json!({"reason":"application_settings"}),
44    }
45}
46
47/// Explain catalog failures without exposing Rust debug output or translated JSON fields.
48#[must_use]
49pub fn library_message(error: &ContextError, localizer: &Localizer) -> Option<String> {
50    match error {
51        ContextError::LibraryConfiguration { name, source } => {
52            let detail = super::global::present_global_config_error(source, localizer);
53            Some(localizer.format(
54                "analysis-library-config-error",
55                &[
56                    ("name", LocalizationValue::Text(name)),
57                    ("detail", LocalizationValue::Text(&detail)),
58                ],
59            ))
60        }
61        ContextError::LibrarySource {
62            name, config, path, ..
63        } => Some(localizer.format(
64            "analysis-library-source-error",
65            &[
66                ("name", LocalizationValue::Text(name)),
67                (
68                    "config",
69                    LocalizationValue::Text(&path_output::display_path(config)),
70                ),
71                (
72                    "path",
73                    LocalizationValue::Text(&path_output::display_path(path)),
74                ),
75            ],
76        )),
77        _ => None,
78    }
79}
80
81/// Preserve the manifest location while rendering a specific configuration failure.
82pub fn present(
83    localizer: &Localizer,
84    path: &Path,
85    error: &SettingsError,
86    hyperlinks: bool,
87) -> String {
88    let detail = reason(localizer, error);
89    localizer.format(
90        "analysis-config-invalid",
91        &[
92            (
93                "path",
94                LocalizationValue::Text(&path_output::render(path, hyperlinks)),
95            ),
96            ("detail", LocalizationValue::Text(&detail)),
97        ],
98    )
99}
100
101/// Describe the rejected relationship without turning machine-facing names into translated keys.
102fn reason(localizer: &Localizer, error: &SettingsError) -> String {
103    match error {
104        SettingsError::ExtensionScopeLocation => {
105            localizer.text("analysis-extension-scope-location")
106        }
107        SettingsError::EmptyConfigurationSource => localizer.text("analysis-reference-empty"),
108        SettingsError::EmptyConfigurationLibrary => localizer.text("analysis-library-empty"),
109        SettingsError::ConfigurationReferenceScope => localizer.text("analysis-reference-scope"),
110        SettingsError::ManagedAnalyzerField { field } => localizer.format(
111            "analysis-managed-field",
112            &[("field", LocalizationValue::Text(field))],
113        ),
114        SettingsError::InvalidAnalyzerPolicy => localizer.text("analysis-policy-invalid"),
115        SettingsError::InvalidName { field, value } => localizer.format(
116            "analysis-name-invalid",
117            &[
118                ("field", LocalizationValue::Text(field)),
119                ("value", LocalizationValue::Text(value)),
120            ],
121        ),
122        SettingsError::InvalidPlatformVersion { value } => localizer.format(
123            "analysis-platform-invalid",
124            &[("value", LocalizationValue::Text(value))],
125        ),
126        SettingsError::UnknownDefaultApplication { name } => localizer.format(
127            "analysis-default-unknown",
128            &[("name", LocalizationValue::Text(name.as_str()))],
129        ),
130        SettingsError::DuplicateMember {
131            application,
132            member,
133        } => member_reason(
134            localizer,
135            "analysis-member-duplicate",
136            application.as_str(),
137            member.as_str(),
138        ),
139        SettingsError::DependencyOutsideApplication {
140            application,
141            member,
142        } => member_reason(
143            localizer,
144            "analysis-dependency-outside",
145            application.as_str(),
146            member.as_str(),
147        ),
148        SettingsError::DependencyCycle { application } => localizer.format(
149            "analysis-dependency-cycle",
150            &[("application", LocalizationValue::Text(application.as_str()))],
151        ),
152        SettingsError::UnknownMember {
153            application,
154            member,
155        } => member_reason(
156            localizer,
157            "analysis-member-unknown",
158            application.as_str(),
159            member.as_str(),
160        ),
161        SettingsError::WrongMemberType {
162            application,
163            member,
164            expected,
165            actual,
166        } => {
167            let expected = expected
168                .project_types()
169                .iter()
170                .map(|kind| kind.as_str())
171                .collect::<Vec<_>>()
172                .join("|");
173            localizer.format(
174                "analysis-member-type",
175                &[
176                    ("application", LocalizationValue::Text(application.as_str())),
177                    ("member", LocalizationValue::Text(member.as_str())),
178                    ("expected", LocalizationValue::Text(&expected)),
179                    ("actual", LocalizationValue::Text(actual.as_str())),
180                ],
181            )
182        }
183        SettingsError::MemberAnalysisUnsupported { member } => localizer.format(
184            "analysis-member-settings",
185            &[("member", LocalizationValue::Text(member.as_str()))],
186        ),
187        SettingsError::TargetPlatformScope => localizer.text("analysis-platform-scope"),
188    }
189}
190
191/// Reuse the same application/member coordinates for duplicate, unknown and invalid edges.
192fn member_reason(localizer: &Localizer, key: &str, application: &str, member: &str) -> String {
193    localizer.format(
194        key,
195        &[
196            ("application", LocalizationValue::Text(application)),
197            ("member", LocalizationValue::Text(member)),
198        ],
199    )
200}