Skip to main content

ito_core/validate/
issue.rs

1//! Helpers for constructing validation issues.
2//!
3//! This module provides a fluent API for creating and enriching `ValidationIssue` instances.
4//! It is the primary way to generate issues during validation logic.
5//!
6//! # Usage
7//!
8//! ```no_run
9//! use ito_core::validate::{error, warning, with_loc};
10//!
11//! let err = error("path/to/file", "Something went wrong");
12//! let warn = with_loc(warning("path/to/file", "Check this"), 10, 5);
13//! ```
14
15use super::{LEVEL_ERROR, LEVEL_INFO, LEVEL_WARNING, ValidationIssue, ValidationLevel};
16
17/// Construct a [`ValidationIssue`] with a fixed `level`, `path`, and message.
18///
19/// This is the low-level constructor. Prefer using [`error`], [`warning`], or [`info`]
20/// for better readability.
21pub fn issue(
22    level: ValidationLevel,
23    path: impl AsRef<str>,
24    message: impl Into<String>,
25) -> ValidationIssue {
26    ValidationIssue {
27        level: level.to_string(),
28        path: path.as_ref().to_string(),
29        message: message.into(),
30        line: None,
31        column: None,
32        metadata: None,
33    }
34}
35
36/// Creates an `ERROR` level issue.
37///
38/// Use this for validation failures that must prevent the operation from succeeding.
39pub fn error(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
40    issue(LEVEL_ERROR, path, message)
41}
42
43/// Creates a `WARNING` level issue.
44///
45/// Use this for potential problems that should be fixed but do not strictly prevent
46/// the operation from succeeding (unless strict mode is enabled).
47pub fn warning(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
48    issue(LEVEL_WARNING, path, message)
49}
50
51/// Creates an `INFO` level issue.
52///
53/// Use this for informational messages, successful checks, or context that helps
54/// the user understand the validation state.
55pub fn info(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
56    issue(LEVEL_INFO, path, message)
57}
58
59/// Attach a 1-based line number to an existing issue.
60///
61/// Use this when the issue can be pinpointed to a specific line.
62pub fn with_line(mut i: ValidationIssue, line: u32) -> ValidationIssue {
63    i.line = Some(line);
64    i
65}
66
67/// Attach a 1-based line + column location to an existing issue.
68///
69/// Use this when precise location information is available.
70pub fn with_loc(mut i: ValidationIssue, line: u32, column: u32) -> ValidationIssue {
71    i.line = Some(line);
72    i.column = Some(column);
73    i
74}
75
76/// Attach structured metadata to an existing issue.
77///
78/// Use this to attach extra JSON-serializable context (e.g., "expected" vs "actual" values)
79/// that can be used by machine-readable output formats.
80pub fn with_metadata(mut i: ValidationIssue, metadata: serde_json::Value) -> ValidationIssue {
81    i.metadata = Some(metadata);
82    i
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn constructors_set_expected_fields() {
91        let err = error("spec.md", "missing requirement");
92        let warn = warning("spec.md", "brief purpose");
93        let info_issue = info("spec.md", "looks good");
94
95        assert_eq!(err.level, LEVEL_ERROR);
96        assert_eq!(err.path, "spec.md");
97        assert_eq!(err.message, "missing requirement");
98        assert_eq!(err.line, None);
99        assert_eq!(err.column, None);
100        assert_eq!(err.metadata, None);
101
102        assert_eq!(warn.level, LEVEL_WARNING);
103        assert_eq!(info_issue.level, LEVEL_INFO);
104    }
105
106    #[test]
107    fn location_helpers_set_line_and_column() {
108        let base = issue(LEVEL_WARNING, "tasks.md", "task warning");
109
110        let with_line_only = with_line(base.clone(), 8);
111        assert_eq!(with_line_only.line, Some(8));
112        assert_eq!(with_line_only.column, None);
113
114        let with_both = with_loc(base, 11, 3);
115        assert_eq!(with_both.line, Some(11));
116        assert_eq!(with_both.column, Some(3));
117    }
118
119    #[test]
120    fn metadata_helper_attaches_json_context() {
121        let base = issue(LEVEL_ERROR, "config.json", "invalid value");
122        let metadata = serde_json::json!({ "expected": "string", "actual": 42 });
123
124        let enriched = with_metadata(base, metadata.clone());
125
126        assert_eq!(enriched.metadata, Some(metadata));
127    }
128}