weavatrix_edit/
validation.rs1mod files;
2
3use std::collections::BTreeMap;
4
5use blazingly_json::Value;
6
7use crate::{
8 envelope::{EDIT_PLAN_FIELDS, FILE_EDIT_FIELDS},
9 error::{EditError, ErrorCode},
10 limits::{MAX_PLAN_OPERATION_BYTES, PlanLimits},
11 model::{Completeness, EDIT_PLAN_SCHEMA, EditPlan, FileEdit, TextEdit},
12};
13
14pub(crate) use files::validate_text_edit;
15
16pub const FILE_EDIT_RESERVED_EXTENSION_KEYS: &[&str] = &FILE_EDIT_FIELDS;
21
22#[derive(Clone, Copy, Debug)]
24pub struct BorrowedFileEdit<'file> {
25 pub path: &'file str,
26 pub sha256: &'file str,
27 pub edits: &'file [TextEdit],
28 pub extensions: &'file BTreeMap<String, Value>,
29 pub reserved_extension_keys: &'file [&'file str],
31}
32
33impl<'file> From<&'file FileEdit> for BorrowedFileEdit<'file> {
34 fn from(file: &'file FileEdit) -> Self {
35 Self {
36 path: &file.path,
37 sha256: &file.sha256,
38 edits: &file.edits,
39 extensions: &file.extensions,
40 reserved_extension_keys: FILE_EDIT_RESERVED_EXTENSION_KEYS,
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct EditValidationStats {
48 total_edits: usize,
49 total_text_bytes: usize,
50}
51
52impl EditValidationStats {
53 #[must_use]
54 pub const fn total_edits(self) -> usize {
55 self.total_edits
56 }
57
58 #[must_use]
59 pub const fn total_text_bytes(self) -> usize {
60 self.total_text_bytes
61 }
62}
63
64#[derive(Clone, Copy, Debug)]
66pub struct ValidatedEditPlan<'plan> {
67 plan: &'plan EditPlan,
68 stats: EditValidationStats,
69}
70
71impl<'plan> ValidatedEditPlan<'plan> {
72 #[must_use]
73 pub const fn plan(self) -> &'plan EditPlan {
74 self.plan
75 }
76
77 #[must_use]
78 pub const fn total_edits(self) -> usize {
79 self.stats.total_edits()
80 }
81
82 #[must_use]
83 pub const fn total_text_bytes(self) -> usize {
84 self.stats.total_text_bytes()
85 }
86}
87
88pub fn validate_edit_plan(
90 plan: &EditPlan,
91 limits: PlanLimits,
92) -> Result<ValidatedEditPlan<'_>, EditError> {
93 if plan.schema_version != EDIT_PLAN_SCHEMA {
94 return Err(EditError::new(
95 ErrorCode::SchemaMismatch,
96 format!("schemaVersion must be {EDIT_PLAN_SCHEMA}"),
97 ));
98 }
99 files::validate_extension_keys(&plan.extensions, &EDIT_PLAN_FIELDS, ErrorCode::InvalidPlan)?;
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}