weavatrix_edit/
validation.rs1mod files;
2
3use std::collections::BTreeMap;
4
5use blazingly_json::Value;
6
7use crate::{
8 error::{EditError, ErrorCode},
9 limits::{MAX_PLAN_OPERATION_BYTES, PlanLimits},
10 model::{Completeness, EDIT_PLAN_SCHEMA, EditPlan, FileEdit, TextEdit},
11};
12
13pub(crate) use files::validate_text_edit;
14
15pub const FILE_EDIT_RESERVED_EXTENSION_KEYS: &[&str] = &["path", "sha256", "edits"];
17
18#[derive(Clone, Copy, Debug)]
20pub struct BorrowedFileEdit<'file> {
21 pub path: &'file str,
22 pub sha256: &'file str,
23 pub edits: &'file [TextEdit],
24 pub extensions: &'file BTreeMap<String, Value>,
25 pub reserved_extension_keys: &'file [&'file str],
27}
28
29impl<'file> From<&'file FileEdit> for BorrowedFileEdit<'file> {
30 fn from(file: &'file FileEdit) -> Self {
31 Self {
32 path: &file.path,
33 sha256: &file.sha256,
34 edits: &file.edits,
35 extensions: &file.extensions,
36 reserved_extension_keys: FILE_EDIT_RESERVED_EXTENSION_KEYS,
37 }
38 }
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct EditValidationStats {
44 total_edits: usize,
45 total_text_bytes: usize,
46}
47
48impl EditValidationStats {
49 #[must_use]
50 pub const fn total_edits(self) -> usize {
51 self.total_edits
52 }
53
54 #[must_use]
55 pub const fn total_text_bytes(self) -> usize {
56 self.total_text_bytes
57 }
58}
59
60#[derive(Clone, Copy, Debug)]
62pub struct ValidatedEditPlan<'plan> {
63 plan: &'plan EditPlan,
64 stats: EditValidationStats,
65}
66
67impl<'plan> ValidatedEditPlan<'plan> {
68 #[must_use]
69 pub const fn plan(self) -> &'plan EditPlan {
70 self.plan
71 }
72
73 #[must_use]
74 pub const fn total_edits(self) -> usize {
75 self.stats.total_edits()
76 }
77
78 #[must_use]
79 pub const fn total_text_bytes(self) -> usize {
80 self.stats.total_text_bytes()
81 }
82}
83
84pub fn validate_edit_plan(
86 plan: &EditPlan,
87 limits: PlanLimits,
88) -> Result<ValidatedEditPlan<'_>, EditError> {
89 if plan.schema_version != EDIT_PLAN_SCHEMA {
90 return Err(EditError::new(
91 ErrorCode::SchemaMismatch,
92 format!("schemaVersion must be {EDIT_PLAN_SCHEMA}"),
93 ));
94 }
95 files::validate_extension_keys(
96 &plan.extensions,
97 &["schemaVersion", "operation", "files", "completeness"],
98 ErrorCode::InvalidPlan,
99 )?;
100 validate_collection(&plan.operation, plan.files.len(), limits)?;
101 validate_completeness(plan.completeness.as_ref())?;
102 let stats = files::validate_file_views(plan.files.iter().map(BorrowedFileEdit::from), limits)?;
103 Ok(ValidatedEditPlan { plan, stats })
104}
105
106pub fn validate_file_edits(
112 operation: &str,
113 files: &[BorrowedFileEdit<'_>],
114 limits: PlanLimits,
115) -> Result<EditValidationStats, EditError> {
116 validate_collection(operation, files.len(), limits)?;
117 files::validate_file_views(files.iter().copied(), limits)
118}
119
120fn validate_collection(
121 operation: &str,
122 file_count: usize,
123 limits: PlanLimits,
124) -> Result<(), EditError> {
125 if operation.is_empty() {
126 return Err(EditError::new(
127 ErrorCode::InvalidPlan,
128 "plan.operation is required",
129 ));
130 }
131 if operation.len() > MAX_PLAN_OPERATION_BYTES {
132 return Err(too_large(format!(
133 "plan.operation exceeds the {MAX_PLAN_OPERATION_BYTES}-byte limit"
134 )));
135 }
136 if file_count == 0 {
137 return Err(EditError::new(
138 ErrorCode::InvalidPlan,
139 "plan.files must be non-empty",
140 ));
141 }
142 if file_count > limits.max_files {
143 return Err(too_large(format!(
144 "plan touches more than {} files",
145 limits.max_files
146 )));
147 }
148 Ok(())
149}
150
151fn validate_completeness(completeness: Option<&Completeness>) -> Result<(), EditError> {
152 let Some(completeness) = completeness else {
153 return Ok(());
154 };
155 if !matches!(
156 completeness.as_str(),
157 Completeness::COMPLETE | Completeness::PARTIAL
158 ) {
159 return Err(EditError::new(
160 ErrorCode::InvalidPlan,
161 "plan.completeness must be COMPLETE or PARTIAL",
162 ));
163 }
164 Ok(())
165}
166
167pub(super) fn too_large(message: impl Into<String>) -> EditError {
168 EditError::new(ErrorCode::PlanTooLarge, message)
169}