+36
-2
packages/cli/src/generated_client.ts
+36
-2
packages/cli/src/generated_client.ts
···
1
1
// Generated TypeScript client for AT Protocol records
2
-
// Generated at: 2025-09-28 21:51:06 UTC
3
-
// Lexicons: 41
2
+
// Generated at: 2025-09-28 23:51:18 UTC
3
+
// Lexicons: 42
4
4
5
5
/**
6
6
* @example Usage
···
1046
1046
connected: boolean;
1047
1047
}
1048
1048
1049
+
export interface NetworkSlicesSliceGetSyncSummaryParams {
1050
+
slice: string;
1051
+
collections?: string[];
1052
+
externalCollections?: string[];
1053
+
repos?: string[];
1054
+
maxRepos?: number;
1055
+
}
1056
+
1057
+
export interface NetworkSlicesSliceGetSyncSummaryOutput {
1058
+
totalRepos: number;
1059
+
cappedRepos: number;
1060
+
collectionsSummary: unknown[];
1061
+
wouldBeCapped: boolean;
1062
+
appliedLimit: number;
1063
+
}
1064
+
1065
+
export interface NetworkSlicesSliceGetSyncSummaryCollectionSummary {
1066
+
collection: string;
1067
+
estimatedRepos: number;
1068
+
isExternal: boolean;
1069
+
}
1070
+
1049
1071
export interface NetworkSlicesSlice {
1050
1072
/** Name of the slice */
1051
1073
name: string;
···
1552
1574
1553
1575
export interface NetworkSlicesSliceGetJobLogs {
1554
1576
readonly LogEntry: NetworkSlicesSliceGetJobLogsLogEntry;
1577
+
}
1578
+
1579
+
export interface NetworkSlicesSliceGetSyncSummary {
1580
+
readonly CollectionSummary: NetworkSlicesSliceGetSyncSummaryCollectionSummary;
1555
1581
}
1556
1582
1557
1583
export interface NetworkSlicesSliceGetJobStatus {
···
2243
2269
return await this.client.makeRequest<
2244
2270
NetworkSlicesSliceGetJetstreamStatusOutput
2245
2271
>("network.slices.slice.getJetstreamStatus", "GET", {});
2272
+
}
2273
+
2274
+
async getSyncSummary(
2275
+
params?: NetworkSlicesSliceGetSyncSummaryParams,
2276
+
): Promise<NetworkSlicesSliceGetSyncSummaryOutput> {
2277
+
return await this.client.makeRequest<
2278
+
NetworkSlicesSliceGetSyncSummaryOutput
2279
+
>("network.slices.slice.getSyncSummary", "GET", params);
2246
2280
}
2247
2281
2248
2282
async getJobStatus(
packages/lexicon-intellisense/wasm/slices_lexicon_bg.wasm
packages/lexicon-intellisense/wasm/slices_lexicon_bg.wasm
This is a binary file and will not be displayed.
+44
-2
packages/lexicon-rs/src/lib.rs
+44
-2
packages/lexicon-rs/src/lib.rs
···
655
655
use crate::validation::primitive::string::StringValidator;
656
656
use crate::StringFormat;
657
657
658
-
let format_enum = StringFormat::from_str(format)
659
-
.ok_or_else(|| JsValue::from_str(&format!("Unknown format: {}", format)))?;
658
+
let format_enum = format.parse::<StringFormat>()
659
+
.map_err(|_| JsValue::from_str(&format!("Unknown format: {}", format)))?;
660
660
661
661
let validator = StringValidator;
662
662
···
803
803
assert_eq!(total_errors, 5, "Should have collected all 5 errors total");
804
804
assert_eq!(errors.len(), 2, "Should have errors for exactly 2 lexicons");
805
805
}
806
+
}
807
+
}
808
+
809
+
#[test]
810
+
fn test_broken_reference_validation() {
811
+
let broken_lexicon = json!({
812
+
"lexicon": 1,
813
+
"id": "test.broken.ref",
814
+
"defs": {
815
+
"main": {
816
+
"type": "object",
817
+
"properties": {
818
+
"items": {
819
+
"type": "array",
820
+
"items": {
821
+
"type": "ref",
822
+
"ref": "#nonExistentDef"
823
+
}
824
+
}
825
+
}
826
+
},
827
+
"existingDef": {
828
+
"type": "string"
829
+
}
830
+
}
831
+
});
832
+
833
+
let result = validate(vec![broken_lexicon]);
834
+
835
+
// Should fail because #nonExistentDef doesn't exist
836
+
assert!(result.is_err(), "Validation should fail for broken reference");
837
+
838
+
if let Err(errors) = result {
839
+
assert!(errors.contains_key("test.broken.ref"), "Should have errors for the test lexicon");
840
+
let error_messages = errors.get("test.broken.ref").unwrap();
841
+
assert!(!error_messages.is_empty(), "Should have at least one error");
842
+
843
+
// Check that at least one error mentions the non-existent reference
844
+
let has_ref_error = error_messages.iter().any(|msg|
845
+
msg.contains("non-existent") || msg.contains("nonExistentDef")
846
+
);
847
+
assert!(has_ref_error, "Should have error about non-existent reference. Got: {:?}", error_messages);
806
848
}
807
849
}
808
850
}
+18
-14
packages/lexicon-rs/src/types.rs
+18
-14
packages/lexicon-rs/src/types.rs
···
29
29
RecordKey,
30
30
}
31
31
32
-
impl StringFormat {
33
-
pub fn from_str(s: &str) -> Option<Self> {
32
+
use std::str::FromStr;
33
+
34
+
impl FromStr for StringFormat {
35
+
type Err = String;
36
+
37
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
34
38
match s {
35
-
"datetime" => Some(Self::DateTime),
36
-
"uri" => Some(Self::Uri),
37
-
"at-uri" => Some(Self::AtUri),
38
-
"did" => Some(Self::Did),
39
-
"handle" => Some(Self::Handle),
40
-
"at-identifier" => Some(Self::AtIdentifier),
41
-
"nsid" => Some(Self::Nsid),
42
-
"cid" => Some(Self::Cid),
43
-
"language" => Some(Self::Language),
44
-
"tid" => Some(Self::Tid),
45
-
"record-key" => Some(Self::RecordKey),
46
-
_ => None,
39
+
"datetime" => Ok(Self::DateTime),
40
+
"uri" => Ok(Self::Uri),
41
+
"at-uri" => Ok(Self::AtUri),
42
+
"did" => Ok(Self::Did),
43
+
"handle" => Ok(Self::Handle),
44
+
"at-identifier" => Ok(Self::AtIdentifier),
45
+
"nsid" => Ok(Self::Nsid),
46
+
"cid" => Ok(Self::Cid),
47
+
"language" => Ok(Self::Language),
48
+
"tid" => Ok(Self::Tid),
49
+
"record-key" => Ok(Self::RecordKey),
50
+
_ => Err(format!("Unknown string format: {}", s)),
47
51
}
48
52
}
49
53
}
+2
-2
packages/lexicon-rs/src/validation/context.rs
+2
-2
packages/lexicon-rs/src/validation/context.rs
···
225
225
/// - Global: `com.example.ns#defName` -> uses specified lexicon ID
226
226
/// - Global main: `com.example.ns` -> uses "main" as def name
227
227
fn parse_reference(&self, reference: &str) -> Result<(String, String), ValidationError> {
228
-
if reference.starts_with('#') {
228
+
if let Some(def_name) = reference.strip_prefix('#') {
229
229
// Local reference - use current lexicon
230
230
let current_lexicon = self.current_lexicon_id.as_ref().ok_or_else(|| {
231
231
ValidationError::InvalidSchema(format!(
···
234
234
))
235
235
})?;
236
236
237
-
let def_name = reference[1..].to_string(); // Remove the '#'
237
+
let def_name = def_name.to_string(); // Remove the '#'
238
238
if def_name.is_empty() {
239
239
return Err(ValidationError::InvalidSchema(format!(
240
240
"Invalid local reference '{}': definition name cannot be empty",
+114
-21
packages/lexicon-rs/src/validation/field/reference.rs
+114
-21
packages/lexicon-rs/src/validation/field/reference.rs
···
60
60
return Err("Reference cannot be empty".to_string());
61
61
}
62
62
63
-
if ref_str.starts_with('#') {
63
+
if let Some(def_name) = ref_str.strip_prefix('#') {
64
64
// Local reference
65
-
let def_name = &ref_str[1..];
66
65
if def_name.is_empty() {
67
66
return Err("Local reference must have a definition name after #".to_string());
68
67
}
···
105
104
// First validate syntax
106
105
Self::validate_ref_syntax(ref_str)?;
107
106
108
-
if ref_str.starts_with('#') {
107
+
if let Some(def_name) = ref_str.strip_prefix('#') {
109
108
// Local reference
110
-
let def_name = ref_str[1..].to_string();
109
+
let def_name = def_name.to_string();
111
110
Ok(ParsedReference::Local(def_name))
112
111
} else if ref_str.contains('#') {
113
112
// Global reference with fragment
···
358
357
))
359
358
})?;
360
359
361
-
// TODO: Validate that the reference can be resolved
362
-
// This would require access to the reference resolution system
360
+
// Parse and validate that the reference can be resolved
361
+
let parsed_ref = Self::parse_reference(ref_str).map_err(|e| {
362
+
ValidationError::InvalidSchema(format!(
363
+
"Ref '{}' has invalid reference: {}",
364
+
def_name, e
365
+
))
366
+
})?;
367
+
368
+
// Attempt to resolve the reference to verify it exists
369
+
// Ensure we have a current lexicon context for resolving local references
370
+
ctx.current_lexicon_id()
371
+
.ok_or_else(|| {
372
+
ValidationError::InvalidSchema(format!(
373
+
"Cannot validate ref '{}': no current lexicon context",
374
+
def_name
375
+
))
376
+
})?;
363
377
364
-
Ok(())
378
+
match Self::resolve_reference(&parsed_ref, ctx) {
379
+
Ok(_) => Ok(()), // Reference resolved successfully
380
+
Err(ValidationError::DataValidation(msg)) if msg.contains("not found") => {
381
+
// Convert data validation error to schema validation error for definition validation
382
+
Err(ValidationError::InvalidSchema(format!(
383
+
"Ref '{}' references non-existent definition: {}",
384
+
def_name, ref_str
385
+
)))
386
+
}
387
+
Err(e) => Err(e), // Pass through other errors
388
+
}
365
389
}
366
390
367
391
/// Validates runtime data against a reference schema definition
···
456
480
.with_lexicons(vec![json!({
457
481
"lexicon": 1,
458
482
"id": "com.example.test",
459
-
"defs": { "main": ref_def.clone() }
483
+
"defs": {
484
+
"main": ref_def.clone(),
485
+
"commonObject": {
486
+
"type": "object",
487
+
"properties": {}
488
+
}
489
+
}
460
490
})])
461
491
.unwrap()
462
492
.build()
463
-
.unwrap();
493
+
.unwrap()
494
+
.with_current_lexicon("com.example.test");
464
495
465
496
let validator = RefValidator;
466
497
assert!(validator.validate(&ref_def, &ctx).is_ok());
···
474
505
});
475
506
476
507
let ctx = ValidationContext::builder()
477
-
.with_lexicons(vec![json!({
478
-
"lexicon": 1,
479
-
"id": "com.example.test",
480
-
"defs": { "main": ref_def.clone() }
481
-
})])
508
+
.with_lexicons(vec![
509
+
json!({
510
+
"lexicon": 1,
511
+
"id": "com.example.test",
512
+
"defs": { "main": ref_def.clone() }
513
+
}),
514
+
json!({
515
+
"lexicon": 1,
516
+
"id": "com.example.defs",
517
+
"defs": {
518
+
"main": { "type": "object" },
519
+
"commonObject": {
520
+
"type": "object",
521
+
"properties": {}
522
+
}
523
+
}
524
+
})
525
+
])
482
526
.unwrap()
483
527
.build()
484
-
.unwrap();
528
+
.unwrap()
529
+
.with_current_lexicon("com.example.test");
485
530
486
531
let validator = RefValidator;
487
532
assert!(validator.validate(&ref_def, &ctx).is_ok());
···
495
540
});
496
541
497
542
let ctx = ValidationContext::builder()
543
+
.with_lexicons(vec![
544
+
json!({
545
+
"lexicon": 1,
546
+
"id": "com.example.test",
547
+
"defs": { "main": ref_def.clone() }
548
+
}),
549
+
json!({
550
+
"lexicon": 1,
551
+
"id": "com.example.record",
552
+
"defs": {
553
+
"main": {
554
+
"type": "record",
555
+
"record": {
556
+
"type": "object",
557
+
"properties": {}
558
+
}
559
+
}
560
+
}
561
+
})
562
+
])
563
+
.unwrap()
564
+
.build()
565
+
.unwrap()
566
+
.with_current_lexicon("com.example.test");
567
+
568
+
let validator = RefValidator;
569
+
assert!(validator.validate(&ref_def, &ctx).is_ok());
570
+
}
571
+
572
+
#[test]
573
+
fn test_missing_ref_field() {
574
+
let ref_def = json!({
575
+
"type": "ref"
576
+
});
577
+
578
+
let ctx = ValidationContext::builder()
498
579
.with_lexicons(vec![json!({
499
580
"lexicon": 1,
500
581
"id": "com.example.test",
···
502
583
})])
503
584
.unwrap()
504
585
.build()
505
-
.unwrap();
586
+
.unwrap()
587
+
.with_current_lexicon("com.example.test");
506
588
507
589
let validator = RefValidator;
508
-
assert!(validator.validate(&ref_def, &ctx).is_ok());
590
+
assert!(validator.validate(&ref_def, &ctx).is_err());
509
591
}
510
592
511
593
#[test]
512
-
fn test_missing_ref_field() {
594
+
fn test_ref_to_nonexistent_definition() {
513
595
let ref_def = json!({
514
-
"type": "ref"
596
+
"type": "ref",
597
+
"ref": "#nonExistentDef"
515
598
});
516
599
517
600
let ctx = ValidationContext::builder()
···
522
605
})])
523
606
.unwrap()
524
607
.build()
525
-
.unwrap();
608
+
.unwrap()
609
+
.with_current_lexicon("com.example.test");
526
610
527
611
let validator = RefValidator;
528
-
assert!(validator.validate(&ref_def, &ctx).is_err());
612
+
let result = validator.validate(&ref_def, &ctx);
613
+
assert!(result.is_err());
614
+
615
+
// Check that the error message mentions the non-existent definition
616
+
if let Err(ValidationError::InvalidSchema(msg)) = result {
617
+
assert!(msg.contains("non-existent definition"));
618
+
assert!(msg.contains("#nonExistentDef"));
619
+
} else {
620
+
panic!("Expected InvalidSchema error for non-existent reference");
621
+
}
529
622
}
530
623
531
624
#[test]
+21
-8
packages/lexicon-rs/src/validation/field/union.rs
+21
-8
packages/lexicon-rs/src/validation/field/union.rs
···
46
46
}
47
47
48
48
// Handle local reference patterns (#ref)
49
-
if reference.starts_with('#') {
50
-
let ref_name = &reference[1..];
49
+
if let Some(ref_name) = reference.strip_prefix('#') {
51
50
// Match bare name against local ref
52
51
if type_name == ref_name {
53
52
return true;
···
59
58
}
60
59
61
60
// Handle implicit #main patterns
62
-
if type_name.ends_with("#main") {
63
-
let base_type = &type_name[..type_name.len() - 5]; // Remove "#main"
61
+
if let Some(base_type) = type_name.strip_suffix("#main") {
62
+
// Remove "#main"
64
63
if reference == base_type {
65
64
return true;
66
65
}
···
211
210
}
212
211
}
213
212
214
-
// TODO: Validate that each reference can be resolved
215
-
// This would require access to the reference resolution system
213
+
// Validate that each reference can be resolved
214
+
use crate::validation::field::reference::RefValidator;
215
+
let ref_validator = RefValidator;
216
+
217
+
for (i, ref_item) in refs_array.iter().enumerate() {
218
+
if let Some(ref_str) = ref_item.as_str() {
219
+
// Create a temporary ref schema for validation
220
+
let temp_ref_schema = serde_json::json!({
221
+
"type": "ref",
222
+
"ref": ref_str
223
+
});
224
+
225
+
let ref_ctx = ctx.with_path(&format!("{}.refs[{}]", def_name, i));
226
+
ref_validator.validate(&temp_ref_schema, &ref_ctx)?;
227
+
}
228
+
}
216
229
217
230
Ok(())
218
231
}
···
922
935
923
936
// String that should violate shortString maxLength constraint
924
937
// Currently passes because ObjectValidator doesn't validate property constraints yet
925
-
let invalid_short = json!({
938
+
let _invalid_short = json!({
926
939
"$type": "shortString",
927
940
"text": "This string is way too long for the short constraint"
928
941
});
929
942
// assert!(validator.validate_data(&invalid_short, &union_schema, &ctx).is_err()); // TODO: Enable when ObjectValidator supports constraints
930
943
931
944
// String that should violate longString minLength constraint
932
-
let invalid_long = json!({
945
+
let _invalid_long = json!({
933
946
"$type": "longString",
934
947
"text": "short"
935
948
});
+199
-1
packages/lexicon-rs/src/validation/primary/procedure.rs
+199
-1
packages/lexicon-rs/src/validation/primary/procedure.rs
···
5
5
use serde_json::Value;
6
6
use crate::errors::ValidationError;
7
7
use crate::validation::{Validator, ValidationContext};
8
+
use crate::validation::{
9
+
field::{object::ObjectValidator, array::ArrayValidator, union::UnionValidator, reference::RefValidator},
10
+
primitive::{string::StringValidator, integer::IntegerValidator, boolean::BooleanValidator, bytes::BytesValidator, blob::BlobValidator, cid_link::CidLinkValidator, null::NullValidator},
11
+
meta::{token::TokenValidator, unknown::UnknownValidator},
12
+
};
8
13
9
14
/// Validator for `procedure` type Lexicon definitions
10
15
///
···
24
29
pub struct ProcedureValidator;
25
30
26
31
impl ProcedureValidator {
32
+
/// Validates a schema by dispatching to the appropriate validator
33
+
fn validate_schema_by_type(
34
+
&self,
35
+
schema: &Value,
36
+
schema_type: &str,
37
+
ctx: &ValidationContext,
38
+
) -> Result<(), ValidationError> {
39
+
match schema_type {
40
+
// Field types
41
+
"object" => ObjectValidator.validate(schema, ctx),
42
+
"array" => ArrayValidator.validate(schema, ctx),
43
+
"union" => UnionValidator.validate(schema, ctx),
44
+
"ref" => RefValidator.validate(schema, ctx),
45
+
46
+
// Primitive types
47
+
"string" => StringValidator.validate(schema, ctx),
48
+
"integer" => IntegerValidator.validate(schema, ctx),
49
+
"boolean" => BooleanValidator.validate(schema, ctx),
50
+
"bytes" => BytesValidator.validate(schema, ctx),
51
+
"blob" => BlobValidator.validate(schema, ctx),
52
+
"cid-link" => CidLinkValidator.validate(schema, ctx),
53
+
"null" => NullValidator.validate(schema, ctx),
54
+
55
+
// Meta types
56
+
"token" => TokenValidator.validate(schema, ctx),
57
+
"unknown" => UnknownValidator.validate(schema, ctx),
58
+
59
+
_ => Err(ValidationError::InvalidSchema(format!(
60
+
"Unknown schema type '{}' in procedure", schema_type
61
+
))),
62
+
}
63
+
}
64
+
27
65
/// Validates procedure input data against input definition
28
66
///
29
67
/// This method performs validation of procedure input against its schema.
···
197
235
}
198
236
}
199
237
200
-
// TODO: Validate parameters, input, output, and errors
238
+
// Validate parameters if present (must be type "params")
239
+
if let Some(parameters) = value.get("parameters") {
240
+
let params_ctx = ctx.with_path(&format!("{}.parameters", def_name));
241
+
ObjectValidator.validate(parameters, ¶ms_ctx)?;
242
+
}
243
+
244
+
// Validate input if present (must have encoding, optional schema)
245
+
if let Some(input) = value.get("input") {
246
+
if let Some(input_obj) = input.as_object() {
247
+
// Validate required encoding field
248
+
if !input_obj.contains_key("encoding") {
249
+
return Err(ValidationError::InvalidSchema(format!(
250
+
"Procedure '{}' input missing required 'encoding' field",
251
+
def_name
252
+
)));
253
+
}
254
+
255
+
// Validate optional schema field
256
+
if let Some(schema) = input_obj.get("schema") {
257
+
if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
258
+
let schema_ctx = ctx.with_path(&format!("{}.input.schema", def_name));
259
+
self.validate_schema_by_type(schema, schema_type, &schema_ctx)?;
260
+
}
261
+
}
262
+
} else {
263
+
return Err(ValidationError::InvalidSchema(format!(
264
+
"Procedure '{}' input must be an object",
265
+
def_name
266
+
)));
267
+
}
268
+
}
269
+
270
+
// Validate output if present (must have encoding, optional schema)
271
+
if let Some(output) = value.get("output") {
272
+
if let Some(output_obj) = output.as_object() {
273
+
// Validate required encoding field
274
+
if !output_obj.contains_key("encoding") {
275
+
return Err(ValidationError::InvalidSchema(format!(
276
+
"Procedure '{}' output missing required 'encoding' field",
277
+
def_name
278
+
)));
279
+
}
280
+
281
+
// Validate optional schema field
282
+
if let Some(schema) = output_obj.get("schema") {
283
+
if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
284
+
let schema_ctx = ctx.with_path(&format!("{}.output.schema", def_name));
285
+
self.validate_schema_by_type(schema, schema_type, &schema_ctx)?;
286
+
}
287
+
}
288
+
} else {
289
+
return Err(ValidationError::InvalidSchema(format!(
290
+
"Procedure '{}' output must be an object",
291
+
def_name
292
+
)));
293
+
}
294
+
}
295
+
296
+
// TODO: Validate errors if present (array of objects with name field)
201
297
202
298
Ok(())
203
299
}
···
292
388
293
389
let invalid_data = json!({"content": "Content"});
294
390
assert!(validator.validate_data(&invalid_data, &schema, &ctx).is_err());
391
+
}
392
+
393
+
#[test]
394
+
fn test_procedure_with_broken_reference_in_output() {
395
+
let procedure = json!({
396
+
"type": "procedure",
397
+
"description": "Test procedure with broken reference in output",
398
+
"output": {
399
+
"encoding": "application/json",
400
+
"schema": {
401
+
"type": "object",
402
+
"properties": {
403
+
"result": {
404
+
"type": "ref",
405
+
"ref": "#nonExistentDef"
406
+
}
407
+
}
408
+
}
409
+
}
410
+
});
411
+
412
+
let ctx = ValidationContext::builder()
413
+
.with_lexicons(vec![json!({
414
+
"lexicon": 1,
415
+
"id": "test.broken.ref",
416
+
"defs": {
417
+
"main": procedure.clone(),
418
+
"existingDef": {
419
+
"type": "string"
420
+
}
421
+
}
422
+
})])
423
+
.unwrap()
424
+
.build()
425
+
.unwrap()
426
+
.with_current_lexicon("test.broken.ref");
427
+
428
+
let validator = ProcedureValidator;
429
+
let result = validator.validate(&procedure, &ctx);
430
+
431
+
// Should fail because #nonExistentDef doesn't exist
432
+
assert!(result.is_err(), "Procedure validation should fail for broken reference in output schema");
433
+
434
+
if let Err(error) = result {
435
+
let error_msg = format!("{:?}", error);
436
+
assert!(
437
+
error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"),
438
+
"Error should mention the non-existent reference. Got: {}",
439
+
error_msg
440
+
);
441
+
}
442
+
}
443
+
444
+
#[test]
445
+
fn test_procedure_with_broken_reference_in_input() {
446
+
let procedure = json!({
447
+
"type": "procedure",
448
+
"description": "Test procedure with broken reference in input",
449
+
"input": {
450
+
"encoding": "application/json",
451
+
"schema": {
452
+
"type": "object",
453
+
"properties": {
454
+
"data": {
455
+
"type": "ref",
456
+
"ref": "#nonExistentDef"
457
+
}
458
+
}
459
+
}
460
+
}
461
+
});
462
+
463
+
let ctx = ValidationContext::builder()
464
+
.with_lexicons(vec![json!({
465
+
"lexicon": 1,
466
+
"id": "test.broken.ref",
467
+
"defs": {
468
+
"main": procedure.clone(),
469
+
"existingDef": {
470
+
"type": "string"
471
+
}
472
+
}
473
+
})])
474
+
.unwrap()
475
+
.build()
476
+
.unwrap()
477
+
.with_current_lexicon("test.broken.ref");
478
+
479
+
let validator = ProcedureValidator;
480
+
let result = validator.validate(&procedure, &ctx);
481
+
482
+
// Should fail because #nonExistentDef doesn't exist
483
+
assert!(result.is_err(), "Procedure validation should fail for broken reference in input schema");
484
+
485
+
if let Err(error) = result {
486
+
let error_msg = format!("{:?}", error);
487
+
assert!(
488
+
error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"),
489
+
"Error should mention the non-existent reference. Got: {}",
490
+
error_msg
491
+
);
492
+
}
295
493
}
296
494
}
+121
-2
packages/lexicon-rs/src/validation/primary/query.rs
+121
-2
packages/lexicon-rs/src/validation/primary/query.rs
···
5
5
use serde_json::Value;
6
6
use crate::errors::ValidationError;
7
7
use crate::validation::{Validator, ValidationContext};
8
+
use crate::validation::{
9
+
field::{object::ObjectValidator, array::ArrayValidator, union::UnionValidator, reference::RefValidator},
10
+
primitive::{string::StringValidator, integer::IntegerValidator, boolean::BooleanValidator, bytes::BytesValidator, blob::BlobValidator, cid_link::CidLinkValidator, null::NullValidator},
11
+
meta::{token::TokenValidator, unknown::UnknownValidator},
12
+
};
8
13
9
14
/// Validator for `query` type Lexicon definitions
10
15
///
···
165
170
}
166
171
}
167
172
168
-
// TODO: Validate parameters if present (must be type "params")
169
-
// TODO: Validate output if present (must have encoding, optional schema)
173
+
// Validate parameters if present (must be type "params")
174
+
if let Some(parameters) = value.get("parameters") {
175
+
// Parameters should be validated as a "params" type schema
176
+
let params_ctx = ctx.with_path(&format!("{}.parameters", def_name));
177
+
ObjectValidator.validate(parameters, ¶ms_ctx)?;
178
+
}
179
+
180
+
// Validate output if present (must have encoding, optional schema)
181
+
if let Some(output) = value.get("output") {
182
+
let _output_ctx = ctx.with_path(&format!("{}.output", def_name));
183
+
184
+
if let Some(output_obj) = output.as_object() {
185
+
// Validate required encoding field
186
+
if !output_obj.contains_key("encoding") {
187
+
return Err(ValidationError::InvalidSchema(format!(
188
+
"Query '{}' output missing required 'encoding' field",
189
+
def_name
190
+
)));
191
+
}
192
+
193
+
// Validate optional schema field
194
+
if let Some(schema) = output_obj.get("schema") {
195
+
// Recursively validate the schema using the appropriate validator
196
+
if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
197
+
let schema_ctx = ctx.with_path(&format!("{}.output.schema", def_name));
198
+
199
+
let validation_result = match schema_type {
200
+
// Field types
201
+
"object" => ObjectValidator.validate(schema, &schema_ctx),
202
+
"array" => ArrayValidator.validate(schema, &schema_ctx),
203
+
"union" => UnionValidator.validate(schema, &schema_ctx),
204
+
"ref" => RefValidator.validate(schema, &schema_ctx),
205
+
206
+
// Primitive types
207
+
"string" => StringValidator.validate(schema, &schema_ctx),
208
+
"integer" => IntegerValidator.validate(schema, &schema_ctx),
209
+
"boolean" => BooleanValidator.validate(schema, &schema_ctx),
210
+
"bytes" => BytesValidator.validate(schema, &schema_ctx),
211
+
"blob" => BlobValidator.validate(schema, &schema_ctx),
212
+
"cid-link" => CidLinkValidator.validate(schema, &schema_ctx),
213
+
"null" => NullValidator.validate(schema, &schema_ctx),
214
+
215
+
// Meta types
216
+
"token" => TokenValidator.validate(schema, &schema_ctx),
217
+
"unknown" => UnknownValidator.validate(schema, &schema_ctx),
218
+
219
+
_ => Err(ValidationError::InvalidSchema(format!(
220
+
"Unknown schema type '{}' in query output", schema_type
221
+
))),
222
+
};
223
+
224
+
validation_result?;
225
+
}
226
+
}
227
+
} else {
228
+
return Err(ValidationError::InvalidSchema(format!(
229
+
"Query '{}' output must be an object",
230
+
def_name
231
+
)));
232
+
}
233
+
}
234
+
170
235
// TODO: Validate errors if present (array of objects with name field)
171
236
172
237
Ok(())
···
452
517
453
518
let empty_params = json!({});
454
519
assert!(validator.validate_data(&empty_params, &schema, &ctx).is_ok());
520
+
}
521
+
522
+
#[test]
523
+
fn test_query_with_broken_reference_in_output() {
524
+
let query = json!({
525
+
"type": "query",
526
+
"description": "Test query with broken reference in output",
527
+
"output": {
528
+
"encoding": "application/json",
529
+
"schema": {
530
+
"type": "object",
531
+
"properties": {
532
+
"items": {
533
+
"type": "array",
534
+
"items": {
535
+
"type": "ref",
536
+
"ref": "#nonExistentDef"
537
+
}
538
+
}
539
+
}
540
+
}
541
+
}
542
+
});
543
+
544
+
let ctx = ValidationContext::builder()
545
+
.with_lexicons(vec![json!({
546
+
"lexicon": 1,
547
+
"id": "test.broken.ref",
548
+
"defs": {
549
+
"main": query.clone(),
550
+
"existingDef": {
551
+
"type": "string"
552
+
}
553
+
}
554
+
})])
555
+
.unwrap()
556
+
.build()
557
+
.unwrap()
558
+
.with_current_lexicon("test.broken.ref");
559
+
560
+
let validator = QueryValidator;
561
+
let result = validator.validate(&query, &ctx);
562
+
563
+
// Should fail because #nonExistentDef doesn't exist
564
+
assert!(result.is_err(), "Query validation should fail for broken reference in output schema");
565
+
566
+
if let Err(error) = result {
567
+
let error_msg = format!("{:?}", error);
568
+
assert!(
569
+
error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"),
570
+
"Error should mention the non-existent reference. Got: {}",
571
+
error_msg
572
+
);
573
+
}
455
574
}
456
575
}
+64
packages/lexicon-rs/src/validation/primary/subscription.rs
+64
packages/lexicon-rs/src/validation/primary/subscription.rs
···
5
5
use serde_json::Value;
6
6
use crate::errors::ValidationError;
7
7
use crate::validation::{Validator, ValidationContext};
8
+
use crate::validation::{
9
+
field::{object::ObjectValidator, union::UnionValidator},
10
+
};
8
11
9
12
/// Validator for `subscription` type Lexicon definitions
10
13
///
···
164
167
}
165
168
}
166
169
170
+
// Validate parameters if present
171
+
if let Some(parameters) = value.get("parameters") {
172
+
let params_ctx = ctx.with_path(&format!("{}.parameters", def_name));
173
+
ObjectValidator.validate(parameters, ¶ms_ctx)?;
174
+
}
175
+
167
176
// Validate message schema must be union if present
168
177
if let Some(message) = value.get("message") {
169
178
if let Some(schema) = message.get("schema") {
···
173
182
def_name
174
183
)));
175
184
}
185
+
186
+
// Recursively validate the union schema
187
+
let schema_ctx = ctx.with_path(&format!("{}.message.schema", def_name));
188
+
UnionValidator.validate(schema, &schema_ctx)?;
176
189
} else {
177
190
return Err(ValidationError::InvalidSchema(format!(
178
191
"Subscription '{}' message must have a schema field",
···
466
479
467
480
let validator = SubscriptionValidator;
468
481
assert!(validator.validate(&subscription, &ctx).is_ok());
482
+
}
483
+
484
+
#[test]
485
+
fn test_subscription_with_broken_reference_in_union() {
486
+
let subscription = json!({
487
+
"type": "subscription",
488
+
"description": "Test subscription with broken reference in union",
489
+
"message": {
490
+
"schema": {
491
+
"type": "union",
492
+
"refs": [
493
+
"#nonExistentDef",
494
+
"#existingDef"
495
+
]
496
+
}
497
+
}
498
+
});
499
+
500
+
let ctx = ValidationContext::builder()
501
+
.with_lexicons(vec![json!({
502
+
"lexicon": 1,
503
+
"id": "test.broken.ref",
504
+
"defs": {
505
+
"main": subscription.clone(),
506
+
"existingDef": {
507
+
"type": "object",
508
+
"properties": {
509
+
"type": {"type": "string"}
510
+
}
511
+
}
512
+
}
513
+
})])
514
+
.unwrap()
515
+
.build()
516
+
.unwrap()
517
+
.with_current_lexicon("test.broken.ref");
518
+
519
+
let validator = SubscriptionValidator;
520
+
let result = validator.validate(&subscription, &ctx);
521
+
522
+
// Should fail because #nonExistentDef doesn't exist
523
+
assert!(result.is_err(), "Subscription validation should fail for broken reference in union");
524
+
525
+
if let Err(error) = result {
526
+
let error_msg = format!("{:?}", error);
527
+
assert!(
528
+
error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"),
529
+
"Error should mention the non-existent reference. Got: {}",
530
+
error_msg
531
+
);
532
+
}
469
533
}
470
534
}
+2
-2
packages/lexicon-rs/src/validation/primitive/string.rs
+2
-2
packages/lexicon-rs/src/validation/primitive/string.rs
···
47
47
/// * `Ok(StringFormat)` if the format is valid
48
48
/// * `Err(ValidationError)` if the format is unknown
49
49
fn validate_format_constraint(def_name: &str, format_str: &str) -> Result<StringFormat, ValidationError> {
50
-
StringFormat::from_str(format_str).ok_or_else(|| {
50
+
format_str.parse::<StringFormat>().map_err(|_| {
51
51
ValidationError::InvalidSchema(format!(
52
52
"String '{}' has unknown format '{}'. Valid formats: datetime, uri, at-uri, did, handle, at-identifier, nsid, cid, language, tid, record-key",
53
53
def_name, format_str
···
377
377
// Validate format constraints
378
378
if let Some(format_value) = schema.get("format") {
379
379
if let Some(format_str) = format_value.as_str() {
380
-
if let Some(format) = StringFormat::from_str(format_str) {
380
+
if let Ok(format) = format_str.parse::<StringFormat>() {
381
381
self.validate_string_format(data_str, format, ctx)?;
382
382
}
383
383
}
+3
-7
packages/lexicon-rs/src/validation/resolution.rs
+3
-7
packages/lexicon-rs/src/validation/resolution.rs
···
35
35
));
36
36
}
37
37
38
-
let (lexicon_id, def_name) = if reference.starts_with('#') {
38
+
let (lexicon_id, def_name) = if let Some(def_name) = reference.strip_prefix('#') {
39
39
// Local reference within current lexicon
40
-
let def_name = &reference[1..];
41
40
if def_name.is_empty() {
42
41
return Err(ValidationError::InvalidSchema(
43
42
"Local reference cannot be just '#'".to_string()
···
245
244
.map(|r| {
246
245
if r.starts_with('#') {
247
246
format!("{}{}", lexicon_id, r)
248
-
} else if !r.contains('#') {
249
-
r // Already a full reference to main
250
247
} else {
251
248
r // Already a full reference
252
249
}
···
262
259
let mut rec_stack = HashSet::new();
263
260
264
261
for def_id in dependency_graph.keys() {
265
-
if !visited.contains(def_id) {
266
-
if has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) {
262
+
if !visited.contains(def_id)
263
+
&& has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) {
267
264
return Err(ValidationError::InvalidSchema(format!(
268
265
"Circular dependency detected involving definition: {}", def_id
269
266
)));
270
267
}
271
-
}
272
268
}
273
269
274
270
Ok(())
packages/lexicon-rs/test_ref_validation
packages/lexicon-rs/test_ref_validation
This is a binary file and will not be displayed.
+52
packages/lexicon-rs/test_ref_validation.rs
+52
packages/lexicon-rs/test_ref_validation.rs
···
1
+
use serde_json::json;
2
+
use slices_lexicon::validate;
3
+
4
+
fn main() {
5
+
let broken_lexicon = json!({
6
+
"lexicon": 1,
7
+
"id": "network.slices.slice.getSyncSummary",
8
+
"defs": {
9
+
"main": {
10
+
"type": "query",
11
+
"description": "Get a summary",
12
+
"output": {
13
+
"encoding": "application/json",
14
+
"schema": {
15
+
"type": "object",
16
+
"required": ["collectionsSummary"],
17
+
"properties": {
18
+
"collectionsSummary": {
19
+
"type": "array",
20
+
"items": {
21
+
"type": "ref",
22
+
"ref": "#collectionSummar"
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
28
+
},
29
+
"collectionSummary": {
30
+
"type": "object",
31
+
"required": ["collection"],
32
+
"properties": {
33
+
"collection": {
34
+
"type": "string"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
});
40
+
41
+
let lexicons = vec![broken_lexicon];
42
+
43
+
match validate(lexicons) {
44
+
Ok(()) => println!("✅ Validation passed (should NOT happen with broken ref)"),
45
+
Err(errors) => {
46
+
println!("❌ Validation failed (expected):");
47
+
for (lexicon_id, error_list) in errors {
48
+
println!(" {}: {:?}", lexicon_id, error_list);
49
+
}
50
+
}
51
+
}
52
+
}
+76
-22
packages/lexicon/wasm/README.md
+76
-22
packages/lexicon/wasm/README.md
···
1
-
# slices-lexicon
1
+
# lexicon-rs
2
+
3
+
Rust implementation of AT Protocol lexicon validation.
4
+
5
+
## Overview
6
+
7
+
This validation engine can be used in any project that needs AT Protocol lexicon validation. It provides high-performance, spec-compliant validation of AT Protocol lexicon documents and data records. It can also be compiled to WebAssembly for use in JavaScript/TypeScript environments.
8
+
9
+
## Architecture
2
10
3
-
AT Protocol lexicon validation library for Rust, compiled to WebAssembly.
11
+
This package serves as the core validation engine and is typically consumed by higher-level packages:
12
+
13
+
- **`@slices/lexicon`** - TypeScript/Deno package with ergonomic APIs
14
+
- **`lexicon-intellisense`** - VS Code extension for lexicon development
15
+
- **Slices CLI** - Command-line tooling for lexicon management
4
16
5
17
## Features
6
18
7
-
- 🚀 Fast lexicon validation written in Rust
8
-
- 🌐 WebAssembly support for browser and Node.js environments
9
-
- 📦 Zero JavaScript dependencies
10
-
- 🔒 Type-safe validation with comprehensive error messages
11
-
- ✅ Full AT Protocol lexicon specification support
19
+
- High-performance lexicon validation implemented in Rust
20
+
- Comprehensive error reporting with detailed context
21
+
- Full AT Protocol lexicon specification compliance
22
+
- Support for all lexicon types: records, queries, procedures, subscriptions
23
+
- Optional WebAssembly compilation for JavaScript/TypeScript environments
24
+
- Zero JavaScript runtime dependencies when using WebAssembly
25
+
26
+
## Direct Usage
27
+
28
+
### Rust Library
12
29
13
-
## Installation
30
+
Add to your `Cargo.toml`:
14
31
15
32
```toml
16
33
[dependencies]
17
34
slices-lexicon = "0.1"
18
35
```
19
36
20
-
## Usage
21
-
22
-
### In Rust
37
+
Basic validation:
23
38
24
39
```rust
25
-
use slices_lexicon::{LexiconValidator, ValidationError};
40
+
use slices_lexicon::{validate, validate_record, is_valid_nsid};
26
41
use serde_json::json;
27
42
43
+
// Define lexicon documents
28
44
let lexicons = vec![
29
45
json!({
30
46
"id": "com.example.post",
47
+
"lexicon": 1,
31
48
"defs": {
32
49
"main": {
33
50
"type": "record",
51
+
"key": "tid",
34
52
"record": {
35
53
"type": "object",
54
+
"required": ["text"],
36
55
"properties": {
37
-
"text": { "type": "string" }
56
+
"text": { "type": "string", "maxLength": 300 }
38
57
}
39
58
}
40
59
}
···
42
61
})
43
62
];
44
63
45
-
let validator = LexiconValidator::new(lexicons)?;
64
+
// Validate lexicon documents
65
+
match validate(lexicons.clone()) {
66
+
Ok(()) => println!("All lexicons are valid"),
67
+
Err(errors) => {
68
+
for (lexicon_id, error_list) in errors {
69
+
println!("Errors in {}: {:?}", lexicon_id, error_list);
70
+
}
71
+
}
72
+
}
73
+
74
+
// Validate a data record against the schema
46
75
let record = json!({ "text": "Hello, world!" });
76
+
validate_record(lexicons, "com.example.post", record)?;
47
77
48
-
validator.validate_record("com.example.post", &record)?;
78
+
// Validate NSID format
79
+
let is_valid = is_valid_nsid("com.example.post");
80
+
println!("NSID valid: {}", is_valid);
49
81
```
50
82
51
-
### As WebAssembly
83
+
### WebAssembly
52
84
53
-
Build with wasm-pack:
85
+
Build the WASM module:
54
86
55
87
```bash
56
-
wasm-pack build --target web
88
+
wasm-pack build --target web --features wasm
57
89
```
58
90
59
-
Then use in JavaScript/TypeScript:
91
+
Use in JavaScript environments:
60
92
61
93
```javascript
62
-
import init, { WasmLexiconValidator } from './pkg/slices_lexicon.js';
94
+
import init, {
95
+
WasmLexiconValidator,
96
+
validate_lexicons_and_get_errors,
97
+
is_valid_nsid
98
+
} from './pkg/slices_lexicon.js';
63
99
64
100
await init();
65
101
66
-
const validator = new WasmLexiconValidator(JSON.stringify(lexicons));
67
-
validator.validate_record("com.example.post", JSON.stringify(record));
102
+
// Validate lexicons
103
+
const lexicons = [{
104
+
id: "com.example.post",
105
+
lexicon: 1,
106
+
defs: { /* ... */ }
107
+
}];
108
+
109
+
const errors = validate_lexicons_and_get_errors(JSON.stringify(lexicons));
110
+
console.log('Validation errors:', JSON.parse(errors));
111
+
112
+
// Validate NSID format
113
+
const isValid = is_valid_nsid("com.example.post");
68
114
```
115
+
116
+
## JavaScript/TypeScript Usage
117
+
118
+
If you're using JavaScript or TypeScript, use the higher-level packages instead of consuming this library directly:
119
+
120
+
- **TypeScript/JavaScript**: Use `@slices/lexicon` for ergonomic APIs with automatic resource management
121
+
- **VS Code Development**: Install the `lexicon-intellisense` extension
122
+
- **CLI Tools**: Use the Slices CLI for lexicon management tasks
69
123
70
124
## Supported Types
71
125
packages/lexicon/wasm/slices_lexicon_bg.wasm
packages/lexicon/wasm/slices_lexicon_bg.wasm
This is a binary file and will not be displayed.