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::{
16    LEVEL_ERROR, LEVEL_INFO, LEVEL_WARNING, ValidationIssue, ValidationLevel,
17    format_specs::FormatSpecRef,
18};
19
20/// Construct a [`ValidationIssue`] with a fixed `level`, `path`, and message.
21///
22/// This is the low-level constructor. Prefer using [`error`], [`warning`], or [`info`]
23/// for better readability.
24pub fn issue(
25    level: ValidationLevel,
26    path: impl AsRef<str>,
27    message: impl Into<String>,
28) -> ValidationIssue {
29    ValidationIssue {
30        level: level.to_string(),
31        path: path.as_ref().to_string(),
32        message: message.into(),
33        line: None,
34        column: None,
35        rule_id: None,
36        metadata: None,
37    }
38}
39
40/// Creates an `ERROR` level issue.
41///
42/// Use this for validation failures that must prevent the operation from succeeding.
43pub fn error(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
44    issue(LEVEL_ERROR, path, message)
45}
46
47/// Creates a `WARNING` level issue.
48///
49/// Use this for potential problems that should be fixed but do not strictly prevent
50/// the operation from succeeding (unless strict mode is enabled).
51pub fn warning(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
52    issue(LEVEL_WARNING, path, message)
53}
54
55/// Creates an `INFO` level issue.
56///
57/// Use this for informational messages, successful checks, or context that helps
58/// the user understand the validation state.
59pub fn info(path: impl AsRef<str>, message: impl Into<String>) -> ValidationIssue {
60    issue(LEVEL_INFO, path, message)
61}
62
63/// Attach a 1-based line number to an existing issue.
64///
65/// Use this when the issue can be pinpointed to a specific line.
66pub fn with_line(mut i: ValidationIssue, line: u32) -> ValidationIssue {
67    i.line = Some(line);
68    i
69}
70
71/// Attach a 1-based line + column location to an existing issue.
72///
73/// Use this when precise location information is available.
74pub fn with_loc(mut i: ValidationIssue, line: u32, column: u32) -> ValidationIssue {
75    i.line = Some(line);
76    i.column = Some(column);
77    i
78}
79
80/// Attach structured metadata to an existing issue.
81///
82/// Use this to attach extra JSON-serializable context (e.g., "expected" vs "actual" values)
83/// that can be used by machine-readable output formats.
84pub fn with_metadata(mut i: ValidationIssue, metadata: serde_json::Value) -> ValidationIssue {
85    i.metadata = Some(metadata);
86    i
87}
88
89/// Attach a stable rule id to an existing issue.
90pub fn with_rule_id(mut i: ValidationIssue, rule_id: impl Into<String>) -> ValidationIssue {
91    i.rule_id = Some(rule_id.into());
92    i
93}
94
95/// Attach a stable validator id and spec path reference.
96pub(crate) fn with_format_spec(mut i: ValidationIssue, spec: FormatSpecRef) -> ValidationIssue {
97    let mut obj = match i.metadata.take() {
98        Some(serde_json::Value::Object(map)) => map,
99        Some(other) => {
100            let mut map = serde_json::Map::new();
101            map.insert("original_metadata".to_string(), other);
102            map
103        }
104        None => serde_json::Map::new(),
105    };
106    obj.insert(
107        "validator_id".to_string(),
108        serde_json::Value::String(spec.validator_id.to_string()),
109    );
110    obj.insert(
111        "spec_path".to_string(),
112        serde_json::Value::String(spec.spec_path.to_string()),
113    );
114    if let Some(rule_id) = i.rule_id.as_ref() {
115        obj.insert(
116            "rule_id".to_string(),
117            serde_json::Value::String(rule_id.clone()),
118        );
119    }
120    i.metadata = Some(serde_json::Value::Object(obj));
121
122    if !i.message.contains(spec.validator_id) {
123        i.message = format!(
124            "{} (validator: {})",
125            i.message.trim_end(),
126            spec.validator_id
127        );
128    }
129    i
130}
131
132#[cfg(test)]
133#[path = "issue_tests.rs"]
134mod issue_tests;