tangled
alpha
login
or
join now
nonbinary.computer
/
jacquard
80
fork
atom
A better Rust ATProto crate
80
fork
atom
overview
issues
9
pulls
pipelines
further cleanup, simplified validation stuff
Orual
4 months ago
648de58f
cc815abb
+183
-127
5 changed files
expand all
collapse all
unified
split
crates
jacquard-lexicon
src
codegen
output.rs
types.rs
error.rs
validation
tests.rs
validation.rs
+6
-14
crates/jacquard-lexicon/src/codegen/output.rs
···
173
173
174
174
// Create parent directories
175
175
if let Some(parent) = full_path.parent() {
176
176
-
std::fs::create_dir_all(parent).map_err(|e| CodegenError::Other {
177
177
-
message: format!("Failed to create directory {:?}: {}", parent, e),
178
178
-
source: None,
179
179
-
})?;
176
176
+
std::fs::create_dir_all(parent)?;
180
177
}
181
178
182
179
// Format code
183
183
-
let file: syn::File = syn::parse2(tokens.clone()).map_err(|e| CodegenError::Other {
184
184
-
message: format!(
185
185
-
"Failed to parse tokens for {:?}: {}\nTokens: {}",
186
186
-
path, e, tokens
187
187
-
),
188
188
-
source: None,
180
180
+
let file: syn::File = syn::parse2(tokens.clone()).map_err(|e| CodegenError::TokenParseError {
181
181
+
path: path.clone(),
182
182
+
source: e,
183
183
+
tokens: tokens.to_string(),
189
184
})?;
190
185
let mut formatted = prettyplease::unparse(&file);
191
186
···
224
219
formatted = format!("{}{}", header, formatted);
225
220
226
221
// Write file
227
227
-
std::fs::write(&full_path, formatted).map_err(|e| CodegenError::Other {
228
228
-
message: format!("Failed to write file {:?}: {}", full_path, e),
229
229
-
source: None,
230
230
-
})?;
222
222
+
std::fs::write(&full_path, formatted)?;
231
223
}
232
224
233
225
Ok(())
+3
-3
crates/jacquard-lexicon/src/codegen/types.rs
···
244
244
join_path_parts(&[&self.root_module, &module_path, &file_module, &type_name])
245
245
};
246
246
247
247
-
let path: syn::Path = syn::parse_str(&path_str).map_err(|e| CodegenError::Other {
248
248
-
message: format!("Failed to parse path: {} {}", path_str, e),
249
249
-
source: None,
247
247
+
let path: syn::Path = syn::parse_str(&path_str).map_err(|e| CodegenError::PathParseError {
248
248
+
path_str: path_str.clone(),
249
249
+
source: e,
250
250
})?;
251
251
252
252
// Only add lifetime if the target type needs it
+16
-7
crates/jacquard-lexicon/src/error.rs
···
104
104
source: syn::Error,
105
105
},
106
106
107
107
-
/// Generic error with context
108
108
-
#[error("{message}")]
109
109
-
#[diagnostic(code(lexicon::error))]
110
110
-
Other {
111
111
-
message: String,
112
112
-
/// Optional source error
107
107
+
/// Failed to parse generated tokens back into syn AST
108
108
+
#[error("Failed to parse generated code for {path:?}")]
109
109
+
#[diagnostic(code(lexicon::token_parse_error))]
110
110
+
TokenParseError {
111
111
+
path: PathBuf,
112
112
+
#[source]
113
113
+
source: syn::Error,
114
114
+
tokens: String,
115
115
+
},
116
116
+
117
117
+
/// Failed to parse module path string
118
118
+
#[error("Failed to parse module path: {path_str}")]
119
119
+
#[diagnostic(code(lexicon::path_parse_error))]
120
120
+
PathParseError {
121
121
+
path_str: String,
113
122
#[source]
114
114
-
source: Option<Box<dyn std::error::Error + Send + Sync>>,
123
123
+
source: syn::Error,
115
124
},
116
125
}
117
126
+102
-103
crates/jacquard-lexicon/src/validation.rs
···
8
8
use crate::schema::SchemaRegistry;
9
9
use cid::Cid as IpldCid;
10
10
use dashmap::DashMap;
11
11
-
use jacquard_common::{
12
12
-
IntoStatic,
13
13
-
smol_str,
14
14
-
types::value::Data,
15
15
-
};
11
11
+
use jacquard_common::{smol_str, types::value::Data};
16
12
use sha2::{Digest, Sha256};
17
13
use smol_str::SmolStr;
18
18
-
use std::{
19
19
-
fmt,
20
20
-
sync::{Arc, LazyLock, OnceLock},
21
21
-
};
14
14
+
use std::{fmt, sync::{Arc, LazyLock}};
22
15
23
16
/// Path to a value within a data structure
24
17
///
···
293
286
/// Result of validating Data against a schema
294
287
///
295
288
/// Distinguishes between structural errors (type mismatches, missing fields) and
296
296
-
/// constraint violations (max_length, ranges, etc.). Constraint validation is lazy.
289
289
+
/// constraint violations (max_length, ranges, etc.).
297
290
#[derive(Debug, Clone)]
298
298
-
pub struct ValidationResult {
299
299
-
/// Structural errors (computed immediately)
300
300
-
structural: Vec<StructuralError>,
301
301
-
302
302
-
/// Constraint errors (computed on first access)
303
303
-
constraints: OnceLock<Vec<ConstraintError>>,
304
304
-
305
305
-
/// Context for lazy constraint validation
306
306
-
data: Option<Arc<Data<'static>>>,
307
307
-
schema_ref: Option<(SmolStr, SmolStr)>, // (nsid, def_name)
308
308
-
registry: Option<Arc<SchemaRegistry>>,
291
291
+
pub enum ValidationResult {
292
292
+
/// Only structural validation was performed (or data was structurally invalid)
293
293
+
StructuralOnly {
294
294
+
structural: Vec<StructuralError>,
295
295
+
},
296
296
+
/// Both structural and constraint validation were performed
297
297
+
Complete {
298
298
+
structural: Vec<StructuralError>,
299
299
+
constraints: Vec<ConstraintError>,
300
300
+
},
309
301
}
310
302
311
303
impl ValidationResult {
312
312
-
/// Create a validation result with no errors
313
313
-
pub fn valid() -> Self {
314
314
-
Self {
315
315
-
structural: Vec::new(),
316
316
-
constraints: OnceLock::new(),
317
317
-
data: None,
318
318
-
schema_ref: None,
319
319
-
registry: None,
320
320
-
}
321
321
-
}
322
322
-
323
323
-
/// Create a validation result with structural errors
324
324
-
pub fn with_structural_errors(errors: Vec<StructuralError>) -> Self {
325
325
-
Self {
326
326
-
structural: errors,
327
327
-
constraints: OnceLock::new(),
328
328
-
data: None,
329
329
-
schema_ref: None,
330
330
-
registry: None,
331
331
-
}
332
332
-
}
333
333
-
334
334
-
/// Create a validation result with context for lazy constraint validation
335
335
-
pub fn with_context(
336
336
-
structural: Vec<StructuralError>,
337
337
-
data: Arc<Data<'static>>,
338
338
-
nsid: SmolStr,
339
339
-
def_name: SmolStr,
340
340
-
registry: Arc<SchemaRegistry>,
341
341
-
) -> Self {
342
342
-
Self {
343
343
-
structural,
344
344
-
constraints: OnceLock::new(),
345
345
-
data: Some(data),
346
346
-
schema_ref: Some((nsid, def_name)),
347
347
-
registry: Some(registry),
348
348
-
}
349
349
-
}
350
350
-
351
304
/// Check if validation passed (no structural or constraint errors)
352
305
pub fn is_valid(&self) -> bool {
353
353
-
self.structural.is_empty() && self.constraint_errors().is_empty()
306
306
+
match self {
307
307
+
ValidationResult::StructuralOnly { structural } => structural.is_empty(),
308
308
+
ValidationResult::Complete {
309
309
+
structural,
310
310
+
constraints,
311
311
+
} => structural.is_empty() && constraints.is_empty(),
312
312
+
}
354
313
}
355
314
356
315
/// Check if structurally valid (ignoring constraint checks)
357
316
pub fn is_structurally_valid(&self) -> bool {
358
358
-
self.structural.is_empty()
317
317
+
match self {
318
318
+
ValidationResult::StructuralOnly { structural } => structural.is_empty(),
319
319
+
ValidationResult::Complete { structural, .. } => structural.is_empty(),
320
320
+
}
359
321
}
360
322
361
323
/// Get structural errors
362
324
pub fn structural_errors(&self) -> &[StructuralError] {
363
363
-
&self.structural
325
325
+
match self {
326
326
+
ValidationResult::StructuralOnly { structural } => structural,
327
327
+
ValidationResult::Complete { structural, .. } => structural,
328
328
+
}
364
329
}
365
330
366
366
-
/// Get constraint errors (computed lazily on first access)
331
331
+
/// Get constraint errors
367
332
pub fn constraint_errors(&self) -> &[ConstraintError] {
368
368
-
self.constraints.get_or_init(|| {
369
369
-
// If no context or structurally invalid, skip constraint validation
370
370
-
if !self.is_structurally_valid() || self.data.is_none() || self.schema_ref.is_none() {
371
371
-
return Vec::new();
372
372
-
}
373
373
-
374
374
-
let data = self.data.as_ref().unwrap();
375
375
-
let (nsid, def_name) = self.schema_ref.as_ref().unwrap();
376
376
-
377
377
-
let mut path = ValidationPath::new();
378
378
-
validate_constraints(
379
379
-
&mut path,
380
380
-
data,
381
381
-
nsid.as_str(),
382
382
-
def_name.as_str(),
383
383
-
self.registry.as_ref(),
384
384
-
)
385
385
-
})
333
333
+
match self {
334
334
+
ValidationResult::StructuralOnly { .. } => &[],
335
335
+
ValidationResult::Complete { constraints, .. } => constraints,
336
336
+
}
386
337
}
387
338
388
339
/// Check if there are any constraint violations
···
392
343
393
344
/// Get all errors (structural and constraint)
394
345
pub fn all_errors(&self) -> impl Iterator<Item = ValidationError> + '_ {
395
395
-
self.structural
346
346
+
self.structural_errors()
396
347
.iter()
397
348
.cloned()
398
349
.map(ValidationError::Structural)
···
431
382
}
432
383
}
433
384
434
434
-
/// Validate data against a schema
385
385
+
/// Validate data against a schema (structural and constraints)
435
386
///
436
436
-
/// Results are cached by content hash for efficiency.
387
387
+
/// Performs both structural validation (types, required fields) and constraint
388
388
+
/// validation (max_length, ranges, etc.). Results are cached by content hash.
437
389
pub fn validate<T: crate::schema::LexiconSchema>(
438
390
&self,
439
391
data: &Data,
···
455
407
Ok(result)
456
408
}
457
409
410
410
+
/// Validate only the structural aspects of data against a schema
411
411
+
///
412
412
+
/// Only checks types, required fields, and schema structure. Does not check
413
413
+
/// constraints like max_length, ranges, etc. This is faster when you only
414
414
+
/// care about type correctness.
415
415
+
pub fn validate_structural<T: crate::schema::LexiconSchema>(
416
416
+
&self,
417
417
+
data: &Data,
418
418
+
) -> ValidationResult {
419
419
+
self.validate_structural_uncached::<T>(data)
420
420
+
}
421
421
+
458
422
/// Validate without caching (internal)
459
423
fn validate_uncached<T: crate::schema::LexiconSchema>(&self, data: &Data) -> ValidationResult {
460
424
let def = match self.registry.get_def(T::nsid(), T::def_name()) {
461
425
Some(d) => d,
462
426
None => {
463
427
// Schema not found - this is a structural error
464
464
-
return ValidationResult::with_structural_errors(vec![
465
465
-
StructuralError::UnresolvedRef {
428
428
+
return ValidationResult::StructuralOnly {
429
429
+
structural: vec![StructuralError::UnresolvedRef {
430
430
+
path: ValidationPath::new(),
431
431
+
ref_nsid: format!("{}#{}", T::nsid(), T::def_name()).into(),
432
432
+
}],
433
433
+
};
434
434
+
}
435
435
+
};
436
436
+
437
437
+
let mut path = ValidationPath::new();
438
438
+
let mut ctx = ValidationContext::new(T::nsid(), T::def_name());
439
439
+
440
440
+
let structural_errors = validate_def(&mut path, data, &def, &self.registry, &mut ctx);
441
441
+
442
442
+
// If structurally invalid, return structural errors only
443
443
+
if !structural_errors.is_empty() {
444
444
+
return ValidationResult::StructuralOnly {
445
445
+
structural: structural_errors,
446
446
+
};
447
447
+
}
448
448
+
449
449
+
// Structurally valid - compute constraints eagerly
450
450
+
let mut path = ValidationPath::new();
451
451
+
let constraint_errors = validate_constraints(
452
452
+
&mut path,
453
453
+
data,
454
454
+
T::nsid(),
455
455
+
T::def_name(),
456
456
+
Some(&Arc::new(self.registry.clone())),
457
457
+
);
458
458
+
459
459
+
ValidationResult::Complete {
460
460
+
structural: structural_errors,
461
461
+
constraints: constraint_errors,
462
462
+
}
463
463
+
}
464
464
+
465
465
+
/// Validate structural aspects only without caching (internal)
466
466
+
fn validate_structural_uncached<T: crate::schema::LexiconSchema>(
467
467
+
&self,
468
468
+
data: &Data,
469
469
+
) -> ValidationResult {
470
470
+
let def = match self.registry.get_def(T::nsid(), T::def_name()) {
471
471
+
Some(d) => d,
472
472
+
None => {
473
473
+
// Schema not found - this is a structural error
474
474
+
return ValidationResult::StructuralOnly {
475
475
+
structural: vec![StructuralError::UnresolvedRef {
466
476
path: ValidationPath::new(),
467
477
ref_nsid: format!("{}#{}", T::nsid(), T::def_name()).into(),
468
468
-
},
469
469
-
]);
478
478
+
}],
479
479
+
};
470
480
}
471
481
};
472
482
473
483
let mut path = ValidationPath::new();
474
484
let mut ctx = ValidationContext::new(T::nsid(), T::def_name());
475
485
476
476
-
let errors = validate_def(&mut path, data, &def, &self.registry, &mut ctx);
486
486
+
let structural_errors = validate_def(&mut path, data, &def, &self.registry, &mut ctx);
477
487
478
478
-
// If structurally valid, create result with context for lazy constraint validation
479
479
-
if errors.is_empty() {
480
480
-
// Convert data to owned for constraint validation
481
481
-
let owned_data = Arc::new(data.clone().into_static());
482
482
-
ValidationResult::with_context(
483
483
-
errors,
484
484
-
owned_data,
485
485
-
SmolStr::new_static(T::nsid()),
486
486
-
SmolStr::new_static(T::def_name()),
487
487
-
Arc::new(self.registry.clone()),
488
488
-
)
489
489
-
} else {
490
490
-
ValidationResult::with_structural_errors(errors)
488
488
+
ValidationResult::StructuralOnly {
489
489
+
structural: structural_errors,
491
490
}
492
491
}
493
492
+56
crates/jacquard-lexicon/src/validation/tests.rs
···
1123
1123
assert_eq!(result.structural_errors().len(), 0);
1124
1124
assert!(result.constraint_errors().len() > 0);
1125
1125
}
1126
1126
+
1127
1127
+
#[test]
1128
1128
+
fn test_validate_structural_only() {
1129
1129
+
let validator = SchemaValidator::new();
1130
1130
+
validator.registry().insert(
1131
1131
+
"test.string.constraints".to_smolstr(),
1132
1132
+
StringConstraintSchema::lexicon_doc(),
1133
1133
+
);
1134
1134
+
1135
1135
+
// String too long (violates constraints)
1136
1136
+
let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([(
1137
1137
+
"text".into(),
1138
1138
+
data_string("this string is way too long"),
1139
1139
+
)])));
1140
1140
+
1141
1141
+
// Use structural validation only
1142
1142
+
let result = validator.validate_structural::<StringConstraintSchema>(&data);
1143
1143
+
1144
1144
+
// Structurally valid - type is correct, required field present
1145
1145
+
assert!(result.is_structurally_valid());
1146
1146
+
1147
1147
+
// No constraint errors computed
1148
1148
+
assert_eq!(result.constraint_errors().len(), 0);
1149
1149
+
1150
1150
+
// Result should be StructuralOnly variant
1151
1151
+
match result {
1152
1152
+
ValidationResult::StructuralOnly { .. } => {}
1153
1153
+
ValidationResult::Complete { .. } => panic!("Expected StructuralOnly variant"),
1154
1154
+
}
1155
1155
+
}
1156
1156
+
1157
1157
+
#[test]
1158
1158
+
fn test_validate_structural_only_with_errors() {
1159
1159
+
let validator = SchemaValidator::new();
1160
1160
+
validator.registry().insert(
1161
1161
+
"test.string.constraints".to_smolstr(),
1162
1162
+
StringConstraintSchema::lexicon_doc(),
1163
1163
+
);
1164
1164
+
1165
1165
+
// Structurally invalid: integer instead of string
1166
1166
+
let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([(
1167
1167
+
"text".into(),
1168
1168
+
Data::Integer(42),
1169
1169
+
)])));
1170
1170
+
1171
1171
+
let result = validator.validate_structural::<StringConstraintSchema>(&data);
1172
1172
+
1173
1173
+
// Not structurally valid
1174
1174
+
assert!(!result.is_structurally_valid());
1175
1175
+
1176
1176
+
// Structural errors should be present
1177
1177
+
assert_eq!(result.structural_errors().len(), 1);
1178
1178
+
1179
1179
+
// No constraint errors
1180
1180
+
assert_eq!(result.constraint_errors().len(), 0);
1181
1181
+
}