1use std::{io, path::Path};
4
5pub mod application;
6pub mod global;
7
8use crate::{
9 localization::{LocalizationValue, Localizer},
10 path_output,
11};
12use eska_project_core::{
13 config::{
14 InvalidMemberPathReason, InvalidSourceReason, ManifestConfigError, ProjectConfigError,
15 WorkspaceConfigError,
16 },
17 project::{
18 InvalidPathReason, ProjectPathError,
19 build::{BuildSettingsError, InvalidArtifactsDirectoryReason},
20 discovery::{ContextDiscoveryError, DiscoveryError},
21 onboarding::WorkspaceEnrollmentError,
22 selection::SelectionError,
23 },
24 workflow::PolicyError,
25};
26
27#[must_use]
29pub fn present_workspace_enrollment_error(
30 error: &WorkspaceEnrollmentError,
31 localizer: &Localizer,
32) -> String {
33 present_workspace_enrollment_error_with_links(error, localizer, false)
34}
35
36#[must_use]
38pub fn present_project_error(error: &DiscoveryError, localizer: &Localizer) -> String {
39 present_project_error_with_links(error, localizer, false)
40}
41
42#[must_use]
44pub fn present_context_error(error: &ContextDiscoveryError, localizer: &Localizer) -> String {
45 present_context_error_with_links(error, localizer, false)
46}
47
48#[must_use]
50pub fn present_workspace_enrollment_error_with_links(
51 error: &WorkspaceEnrollmentError,
52 localizer: &Localizer,
53 hyperlinks: bool,
54) -> String {
55 match error {
56 WorkspaceEnrollmentError::ApplicationRequired { available } => localizer.format(
57 "workspace-onboarding-application-required",
58 &[("names", LocalizationValue::Text(&available.join(", ")))],
59 ),
60 WorkspaceEnrollmentError::InvalidApplication { name } => localizer.format(
61 "workspace-onboarding-application-invalid",
62 &[("name", LocalizationValue::Text(name))],
63 ),
64 WorkspaceEnrollmentError::ConfigurationAlreadySet { application } => localizer.format(
65 "workspace-onboarding-configuration-set",
66 &[("name", LocalizationValue::Text(application))],
67 ),
68 WorkspaceEnrollmentError::Io { path, source } => {
69 io_message(localizer, path, source, hyperlinks)
70 }
71 WorkspaceEnrollmentError::Config(_)
72 | WorkspaceEnrollmentError::TomlEdit(_)
73 | WorkspaceEnrollmentError::InvalidComposition => {
74 localizer.text("workspace-onboarding-config-invalid")
75 }
76 WorkspaceEnrollmentError::NonUtf8MemberPath { path } => path_message(
77 localizer,
78 "workspace-onboarding-path-utf8",
79 path,
80 hyperlinks,
81 ),
82 WorkspaceEnrollmentError::OutsideWorkspace { workspace, member } => localizer.format(
83 "workspace-member-outside-root",
84 &[
85 (
86 "workspace",
87 LocalizationValue::Text(&path_output::render(workspace, hyperlinks)),
88 ),
89 (
90 "member",
91 LocalizationValue::Text(&path_output::render(member, hyperlinks)),
92 ),
93 ],
94 ),
95 WorkspaceEnrollmentError::DuplicateMember { path } => {
96 path_message(localizer, "workspace-member-duplicate", path, hyperlinks)
97 }
98 WorkspaceEnrollmentError::NestedMembers { first, second } => localizer.format(
99 "workspace-members-nested",
100 &[
101 (
102 "first",
103 LocalizationValue::Text(&path_output::render(first, hyperlinks)),
104 ),
105 (
106 "second",
107 LocalizationValue::Text(&path_output::render(second, hyperlinks)),
108 ),
109 ],
110 ),
111 WorkspaceEnrollmentError::DuplicateName { name } => localizer.format(
112 "workspace-member-name-duplicate",
113 &[("name", LocalizationValue::Text(name.as_str()))],
114 ),
115 WorkspaceEnrollmentError::ManifestChanged { path } => path_message(
116 localizer,
117 "workspace-onboarding-manifest-changed",
118 path,
119 hyperlinks,
120 ),
121 }
122}
123
124#[must_use]
126pub fn present_selection_error(error: &SelectionError, localizer: &Localizer) -> String {
127 match error {
128 SelectionError::InvalidName(error) => localizer.format(
129 "project-selector-name-invalid",
130 &[("name", LocalizationValue::Text(error.value()))],
131 ),
132 SelectionError::DuplicateSelector { name } => localizer.format(
133 "project-selector-duplicate",
134 &[("name", LocalizationValue::Text(name.as_str()))],
135 ),
136 SelectionError::UnknownProject { name } => localizer.format(
137 "project-selector-unknown",
138 &[("name", LocalizationValue::Text(name.as_str()))],
139 ),
140 SelectionError::WorkspaceSelectorForStandalone => {
141 localizer.text("project-selector-standalone")
142 }
143 SelectionError::ConflictingSelectors => localizer.text("project-selector-conflict"),
144 SelectionError::ExplicitProjectRequired | SelectionError::SingleProjectRequired => {
145 localizer.text("project-selector-single-required")
146 }
147 }
148}
149
150#[must_use]
152pub fn present_project_error_with_links(
153 error: &DiscoveryError,
154 localizer: &Localizer,
155 hyperlinks: bool,
156) -> String {
157 match error {
158 DiscoveryError::NotFound { start } => {
159 path_message(localizer, "project-not-found", start, hyperlinks)
160 }
161 DiscoveryError::Io { path, source } => io_message(localizer, path, source, hyperlinks),
162 DiscoveryError::StartNotDirectory { path } => {
163 path_message(localizer, "project-start-not-directory", path, hyperlinks)
164 }
165 DiscoveryError::ConfigNotFile { path } => {
166 path_message(localizer, "project-config-not-file", path, hyperlinks)
167 }
168 DiscoveryError::SourceNotDirectory { path } => {
169 path_message(localizer, "project-source-not-directory", path, hyperlinks)
170 }
171 DiscoveryError::Config { path, source } => {
172 config_message(localizer, path, source, hyperlinks)
173 }
174 }
175}
176
177#[must_use]
179pub fn present_context_error_with_links(
180 error: &ContextDiscoveryError,
181 localizer: &Localizer,
182 hyperlinks: bool,
183) -> String {
184 match error {
185 ContextDiscoveryError::Project(error) => {
186 present_project_error_with_links(error, localizer, hyperlinks)
187 }
188 ContextDiscoveryError::Manifest { path, source } => {
189 manifest_message(localizer, path, source, hyperlinks)
190 }
191 ContextDiscoveryError::MemberNotDirectory { path } => path_message(
192 localizer,
193 "workspace-member-not-directory",
194 path,
195 hyperlinks,
196 ),
197 ContextDiscoveryError::MemberOutsideWorkspace { workspace, member } => localizer.format(
198 "workspace-member-outside-root",
199 &[
200 (
201 "workspace",
202 LocalizationValue::Text(&path_output::render(workspace, hyperlinks)),
203 ),
204 (
205 "member",
206 LocalizationValue::Text(&path_output::render(member, hyperlinks)),
207 ),
208 ],
209 ),
210 ContextDiscoveryError::DuplicateMember { path } => {
211 path_message(localizer, "workspace-member-duplicate", path, hyperlinks)
212 }
213 ContextDiscoveryError::NestedMembers { first, second } => localizer.format(
214 "workspace-members-nested",
215 &[
216 (
217 "first",
218 LocalizationValue::Text(&path_output::render(first, hyperlinks)),
219 ),
220 (
221 "second",
222 LocalizationValue::Text(&path_output::render(second, hyperlinks)),
223 ),
224 ],
225 ),
226 ContextDiscoveryError::MemberManifestNotProject { path } => path_message(
227 localizer,
228 "workspace-member-project-required",
229 path,
230 hyperlinks,
231 ),
232 ContextDiscoveryError::MemberNameMissing { path } => path_message(
233 localizer,
234 "workspace-member-name-required",
235 path,
236 hyperlinks,
237 ),
238 ContextDiscoveryError::DuplicateName { name } => localizer.format(
239 "workspace-member-name-duplicate",
240 &[("name", LocalizationValue::Text(name.as_str()))],
241 ),
242 ContextDiscoveryError::MemberWorkflowUnsupported { path } => path_message(
243 localizer,
244 "workspace-member-workflow-unsupported",
245 path,
246 hyperlinks,
247 ),
248 ContextDiscoveryError::MemberArtifactsDirectoryUnsupported { path } => path_message(
249 localizer,
250 "workspace-member-artifacts-directory-unsupported",
251 path,
252 hyperlinks,
253 ),
254 ContextDiscoveryError::UnlistedProject { project, workspace } => localizer.format(
255 "workspace-project-unlisted",
256 &[
257 (
258 "project",
259 LocalizationValue::Text(&path_output::render(project, hyperlinks)),
260 ),
261 (
262 "workspace",
263 LocalizationValue::Text(&path_output::render(workspace, hyperlinks)),
264 ),
265 ],
266 ),
267 ContextDiscoveryError::ResolvedBuild(_) => localizer.text("workspace-build-invalid"),
268 }
269}
270
271fn manifest_message(
273 localizer: &Localizer,
274 path: &Path,
275 error: &ManifestConfigError,
276 hyperlinks: bool,
277) -> String {
278 match error {
279 ManifestConfigError::Io { path, source } => io_message(localizer, path, source, hyperlinks),
280 ManifestConfigError::Toml(_) => {
281 path_message(localizer, "project-config-invalid", path, hyperlinks)
282 }
283 ManifestConfigError::KindMissing => {
284 path_message(localizer, "manifest-kind-missing", path, hyperlinks)
285 }
286 ManifestConfigError::KindAmbiguous => {
287 path_message(localizer, "manifest-kind-ambiguous", path, hyperlinks)
288 }
289 ManifestConfigError::Project(error) => config_message(localizer, path, error, hyperlinks),
290 ManifestConfigError::Workspace(error) => {
291 workspace_config_message(localizer, path, error, hyperlinks)
292 }
293 }
294}
295
296fn workspace_config_message(
298 localizer: &Localizer,
299 manifest_path: &Path,
300 error: &WorkspaceConfigError,
301 hyperlinks: bool,
302) -> String {
303 match error {
304 WorkspaceConfigError::InvalidAnalysis(error) => {
305 application::present(localizer, manifest_path, error, hyperlinks)
306 }
307 WorkspaceConfigError::Io { path, source } => {
308 io_message(localizer, path, source, hyperlinks)
309 }
310 WorkspaceConfigError::Toml(_) => path_message(
311 localizer,
312 "workspace-config-invalid",
313 manifest_path,
314 hyperlinks,
315 ),
316 WorkspaceConfigError::InvalidBuild(_) => path_message(
317 localizer,
318 "workspace-build-invalid",
319 manifest_path,
320 hyperlinks,
321 ),
322 WorkspaceConfigError::InvalidWorkflow(error) => {
323 config_message(localizer, manifest_path, error, hyperlinks)
324 }
325 WorkspaceConfigError::InvalidMemberPath { path, reason } => {
326 let key = match reason {
327 InvalidMemberPathReason::Empty => "workspace-member-path-empty",
328 InvalidMemberPathReason::Absolute => "workspace-member-path-relative-required",
329 InvalidMemberPathReason::ContainsParentTraversal => {
330 "workspace-member-path-parent-traversal"
331 }
332 };
333 configured_path_message(localizer, key, path, manifest_path, hyperlinks)
334 }
335 }
336}
337
338pub fn config_message(
340 localizer: &Localizer,
341 path: &Path,
342 error: &ProjectConfigError,
343 hyperlinks: bool,
344) -> String {
345 let manifest_path = path;
346 match error {
347 ProjectConfigError::InvalidAnalysis(error) => {
348 application::present(localizer, path, error, hyperlinks)
349 }
350 ProjectConfigError::InvalidName(error) => localizer.format(
351 "project-name-invalid",
352 &[
353 (
354 "path",
355 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
356 ),
357 ("value", LocalizationValue::Text(error.value())),
358 ],
359 ),
360 ProjectConfigError::InvalidBuild(error) => match error {
361 BuildSettingsError::InvalidPlatformVersion { value } => localizer.format(
362 "project-build-version-invalid",
363 &[
364 (
365 "path",
366 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
367 ),
368 ("value", LocalizationValue::Text(value)),
369 ],
370 ),
371 BuildSettingsError::InvalidArtifactsDirectory { path, reason } => {
372 let key = match reason {
373 InvalidArtifactsDirectoryReason::Empty => "project-build-path-empty",
374 InvalidArtifactsDirectoryReason::Absolute => {
375 "project-build-path-relative-required"
376 }
377 InvalidArtifactsDirectoryReason::ContainsParentTraversal => {
378 "project-build-path-parent-traversal"
379 }
380 };
381 configured_path_message(localizer, key, path, manifest_path, hyperlinks)
382 }
383 },
384 ProjectConfigError::InvalidWorkflow(error) => {
385 workflow_message(localizer, path, error, hyperlinks)
386 }
387 ProjectConfigError::UnknownWorkflow { value } => value_message(
388 localizer,
389 "project-workflow-unknown",
390 path,
391 value,
392 hyperlinks,
393 ),
394 ProjectConfigError::Io { path, source } => io_message(localizer, path, source, hyperlinks),
395 ProjectConfigError::Toml(_) => {
398 path_message(localizer, "project-config-invalid", path, hyperlinks)
399 }
400 ProjectConfigError::UnknownProjectType { value } => {
401 value_message(localizer, "project-type-unknown", path, value, hyperlinks)
402 }
403 ProjectConfigError::UnknownSourceFormat { value } => {
404 value_message(localizer, "project-format-unknown", path, value, hyperlinks)
405 }
406 ProjectConfigError::InvalidSource { path, reason } => {
407 let key = match reason {
408 InvalidSourceReason::Empty => "project-path-empty",
409 InvalidSourceReason::Absolute => "project-path-relative-required",
410 InvalidSourceReason::ContainsParentTraversal => "project-path-parent-traversal",
411 };
412 configured_path_message(localizer, key, path, manifest_path, hyperlinks)
413 }
414 ProjectConfigError::ProjectPath(error) => match error {
415 ProjectPathError::InvalidPath { path, reason, .. } => {
416 let key = match reason {
417 InvalidPathReason::NotAbsolute => "project-path-absolute-required",
418 InvalidPathReason::ContainsParentTraversal => "project-path-parent-traversal",
419 };
420 configured_path_message(localizer, key, path, manifest_path, hyperlinks)
421 }
422 ProjectPathError::SourceOutsideRoot { root, source } => localizer.format(
423 "project-source-outside-root",
424 &[
425 (
426 "root",
427 LocalizationValue::Text(&path_output::render(root, hyperlinks)),
428 ),
429 (
430 "source",
431 LocalizationValue::Text(&path_output::render(source, hyperlinks)),
432 ),
433 ],
434 ),
435 },
436 }
437}
438
439fn workflow_message(
441 localizer: &Localizer,
442 path: &Path,
443 error: &PolicyError,
444 hyperlinks: bool,
445) -> String {
446 match error {
447 PolicyError::InvalidValue { field, value } => localizer.format(
448 "project-workflow-value-invalid",
449 &[
450 (
451 "path",
452 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
453 ),
454 ("field", LocalizationValue::Text(field.as_str())),
455 ("value", LocalizationValue::Text(value)),
456 ],
457 ),
458 PolicyError::MissingField { field } => localizer.format(
459 "project-workflow-field-missing",
460 &[
461 (
462 "path",
463 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
464 ),
465 ("field", LocalizationValue::Text(field.as_str())),
466 ],
467 ),
468 error => path_message(
469 localizer,
470 match error {
471 PolicyError::ExtendsRequiresCustom => "project-workflow-custom-required",
472 PolicyError::CustomBase => "project-workflow-custom-base",
473 PolicyError::PublishRequired => "project-workflow-publish-required",
474 PolicyError::IntegrationRequiredForDeletion => {
475 "project-workflow-integration-required"
476 }
477 _ => "project-workflow-invalid",
478 },
479 path,
480 hyperlinks,
481 ),
482 }
483}
484
485#[must_use]
487pub fn io_message(
488 localizer: &Localizer,
489 path: &Path,
490 error: &io::Error,
491 hyperlinks: bool,
492) -> String {
493 let reason = localizer.text(match error.kind() {
494 io::ErrorKind::NotFound => "project-io-not-found",
495 io::ErrorKind::PermissionDenied => "project-io-permission-denied",
496 io::ErrorKind::InvalidData => "project-io-invalid-data",
497 io::ErrorKind::NotADirectory => "project-io-not-directory",
498 _ => "project-io-other",
499 });
500 localizer.format(
501 "project-io-error",
502 &[
503 (
504 "path",
505 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
506 ),
507 ("reason", LocalizationValue::Text(&reason)),
508 ],
509 )
510}
511
512#[must_use]
514pub fn path_message(localizer: &Localizer, key: &str, path: &Path, hyperlinks: bool) -> String {
515 localizer.format(
516 key,
517 &[(
518 "path",
519 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
520 )],
521 )
522}
523
524fn configured_path_message(
526 localizer: &Localizer,
527 key: &str,
528 path: &Path,
529 manifest: &Path,
530 hyperlinks: bool,
531) -> String {
532 let root = manifest.parent().unwrap_or_else(|| Path::new("."));
533 localizer.format(
534 key,
535 &[(
536 "path",
537 LocalizationValue::Text(&path_output::render_link(
538 path,
539 &root.join(path),
540 hyperlinks,
541 )),
542 )],
543 )
544}
545
546fn value_message(
548 localizer: &Localizer,
549 key: &str,
550 path: &Path,
551 value: &str,
552 hyperlinks: bool,
553) -> String {
554 localizer.format(
555 key,
556 &[
557 (
558 "path",
559 LocalizationValue::Text(&path_output::render(path, hyperlinks)),
560 ),
561 ("value", LocalizationValue::Text(value)),
562 ],
563 )
564}