Something went wrong. Try again.
A human-friendly DSL for ATProto Lexicons
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013use crate::lexer::Token as LexToken;use crate::{ Lexicon, ParseError, ast::*, lexer::{SpannedToken, tokenize}, span::{Span, Spanned},};use alloc::vec::Vec;
struct Parser { tokens: Vec<SpannedToken>, pos: usize,}
impl Parser { fn new(tokens: Vec<SpannedToken>) -> Self { Parser { tokens, pos: 0 } }
fn current(&self) -> &SpannedToken { &self.tokens[self.pos] }
fn peek(&self, offset: usize) -> Option<&SpannedToken> { self.tokens.get(self.pos + offset) }
fn is_eof(&self) -> bool { matches!(self.current().token, LexToken::Eof) }
fn advance(&mut self) -> &SpannedToken { let current = &self.tokens[self.pos]; if !self.is_eof() { self.pos += 1; } current }
fn expect(&mut self, expected: LexToken) -> Result<Span, ParseError> { let current = self.current(); if current.token == expected { Ok(self.advance().span) } else { Err(ParseError::Syntax { message: alloc::format!("Expected {}, found {}", expected, current.token), span: current.span, }) } }
fn parse_ident(&mut self) -> Result<Ident, ParseError> { let current = self.current(); if let LexToken::Ident(name) = ¤t.token { let ident = Ident { name: name.clone(), span: current.span, }; self.advance(); Ok(ident) } else { Err(ParseError::Syntax { message: alloc::format!("Expected identifier, found {}", current.token), span: current.span, }) } }
fn parse_field_name(&mut self) -> Result<Ident, ParseError> { let current = self.current(); // Field names can be identifiers or keywords let name = match ¤t.token { LexToken::Ident(n) => n.clone(), LexToken::Record => "record".into(), LexToken::Token => "token".into(), LexToken::Inline => "inline".into(), LexToken::Def => "def".into(), LexToken::Type => "type".into(), LexToken::Query => "query".into(), LexToken::Procedure => "procedure".into(), LexToken::Subscription => "subscription".into(), LexToken::Error => "error".into(), LexToken::Use => "use".into(), LexToken::As => "as".into(), LexToken::String => "string".into(), LexToken::Integer => "integer".into(), LexToken::Boolean => "boolean".into(), LexToken::Null => "null".into(), LexToken::Unknown => "unknown".into(), LexToken::Constrained => "constrained".into(), LexToken::True => "true".into(), LexToken::False => "false".into(), _ => { return Err(ParseError::Syntax { message: alloc::format!("Expected field name, found {}", current.token), span: current.span, }); } }; let ident = Ident { name, span: current.span, }; self.advance(); Ok(ident) }
fn parse_ident_or_keyword(&mut self) -> Result<Ident, ParseError> { let current = self.current(); // Path segments can be identifiers or keywords let name = match ¤t.token { LexToken::Ident(n) => n.clone(), LexToken::Record => "record".into(), LexToken::Token => "token".into(), LexToken::Inline => "inline".into(), LexToken::Def => "def".into(), LexToken::Type => "type".into(), LexToken::Query => "query".into(), LexToken::Procedure => "procedure".into(), LexToken::Subscription => "subscription".into(), LexToken::Error => "error".into(), LexToken::Use => "use".into(), LexToken::As => "as".into(), LexToken::String => "string".into(), LexToken::Integer => "integer".into(), LexToken::Boolean => "boolean".into(), LexToken::Null => "null".into(), LexToken::Unknown => "unknown".into(), LexToken::Constrained => "constrained".into(), LexToken::Blob => "blob".into(), LexToken::Bytes => "bytes".into(), LexToken::Namespace => "namespace".into(), _ => { return Err(ParseError::Syntax { message: alloc::format!("Expected identifier, found {}", current.token), span: current.span, }); } }; let ident = Ident { name, span: current.span, }; self.advance(); Ok(ident) }
fn parse_path(&mut self) -> Result<Path, ParseError> { let mut segments = Vec::new(); let start = self.current().span.start;
segments.push(self.parse_ident_or_keyword()?);
while matches!(self.current().token, LexToken::Dot) { // Check if next token is LeftBrace (for use namespace.{ syntax) if let Some(next) = self.peek(1) { if matches!(next.token, LexToken::LeftBrace) { break; } } self.advance(); segments.push(self.parse_ident_or_keyword()?); }
let end = segments.last().unwrap().span.end; Ok(Path { segments, span: Span::new(start, end), }) }}
pub fn parse_lexicon(input: &str) -> Result<Lexicon, ParseError> { let tokens = tokenize(input)?; let mut parser = Parser::new(tokens); let start = parser.current().span.start;
let mut items = Vec::new();
while !parser.is_eof() { items.push(parser.parse_item()?); }
let end = if let Some(last) = items.last() { last.span().end } else { start };
Ok(Lexicon { items, span: Span::new(start, end), })}
impl Parser { fn parse_item(&mut self) -> Result<Item, ParseError> { let mut doc_comments = Vec::new(); while let LexToken::DocComment(comment) = &self.current().token { let span = self.current().span; doc_comments.push(DocComment { text: comment.clone(), span, }); self.advance(); }
let annotations = self.parse_annotations()?;
match &self.current().token { LexToken::Record => self.parse_record(doc_comments, annotations), LexToken::Inline => self.parse_inline_type(doc_comments, annotations), LexToken::Def => self.parse_def_type(doc_comments, annotations), LexToken::Token => self.parse_token(doc_comments, annotations), LexToken::Query => self.parse_query(doc_comments, annotations), LexToken::Procedure => self.parse_procedure(doc_comments, annotations), LexToken::Subscription => self.parse_subscription(doc_comments, annotations), LexToken::Use => self.parse_use(), LexToken::SelfKw => self.parse_self(doc_comments, annotations), _ => Err(ParseError::Syntax { message: alloc::format!("Expected item definition, found {}", self.current().token), span: self.current().span, }), } }
/// Parse a `self { }` declaration — the lexicon-as-item. Body is /// required but currently always empty; the `{}` shape is reserved /// for future contents (see the C6 design doc). fn parse_self( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::SelfKw)?; self.expect(LexToken::LeftBrace)?; // V1: body must be empty. Allow whitespace/comments (already // skipped by the tokeniser) but reject any real content. if !matches!(self.current().token, LexToken::RightBrace) { return Err(ParseError::Syntax { message: alloc::format!( "`self {{}}` body is reserved for future use; expected `}}`, found {}", self.current().token ), span: self.current().span, }); } let end = self.expect(LexToken::RightBrace)?;
Ok(Item::SelfItem(SelfItem { docs, annotations, span: Span::new(start.start, end.end), })) }
fn parse_annotations(&mut self) -> Result<Vec<Annotation>, ParseError> { let mut annotations = Vec::new();
while matches!(self.current().token, LexToken::At) { annotations.push(self.parse_annotation()?); }
Ok(annotations) }
fn parse_annotation(&mut self) -> Result<Annotation, ParseError> { let start = self.expect(LexToken::At)?; let first_ident = self.parse_ident()?;
// Check if this is a generator selector or bare annotation let (selectors, name) = if matches!(self.current().token, LexToken::Comma) || matches!(self.current().token, LexToken::Colon) { // Generator selector syntax: @rust:deprecated or @rust,typescript:deprecated let mut selectors = alloc::vec![first_ident];
// Parse comma-separated selectors while matches!(self.current().token, LexToken::Comma) { self.advance(); // consume comma selectors.push(self.parse_ident()?); }
// Now expect and consume the colon self.expect(LexToken::Colon)?;
// Parse the annotation name let name = self.parse_ident()?; (selectors, name) } else { // Bare annotation: @deprecated (alloc::vec![], first_ident) };
let mut args = Vec::new();
if matches!(self.current().token, LexToken::LeftParen) { self.advance();
while !matches!(self.current().token, LexToken::RightParen) { args.push(self.parse_annotation_arg()?);
if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } }
self.expect(LexToken::RightParen)?; }
let end = if args.is_empty() { name.span.end } else { self.tokens[self.pos - 1].span.end };
Ok(Annotation { selectors, name, args, span: Span::new(start.start, end), }) }
fn parse_annotation_arg(&mut self) -> Result<AnnotationArg, ParseError> { if let Some(next) = self.peek(1) { if matches!(next.token, LexToken::Colon) { let name = self.parse_ident()?; self.expect(LexToken::Colon)?; let value = self.parse_annotation_value()?; return Ok(AnnotationArg::Named { name, value }); } }
let value = self.parse_annotation_value()?; Ok(AnnotationArg::Positional(value)) }
fn parse_annotation_value(&mut self) -> Result<AnnotationValue, ParseError> { let current = self.current(); match ¤t.token { LexToken::StringLit(s) => { let value = AnnotationValue::String(s.clone()); self.advance(); Ok(value) } LexToken::IntLit(i) => { let value = AnnotationValue::Number(*i as f64); self.advance(); Ok(value) } LexToken::FloatLit(f) => { let value = AnnotationValue::Number(*f); self.advance(); Ok(value) } LexToken::True => { self.advance(); Ok(AnnotationValue::Boolean(true)) } LexToken::False => { self.advance(); Ok(AnnotationValue::Boolean(false)) } LexToken::Null => { self.advance(); Ok(AnnotationValue::Null) } LexToken::LeftBracket => self.parse_annotation_value_array(), LexToken::LeftBrace => self.parse_annotation_value_object(), // Identifier → type reference path. Consumed here so `@reference` // can carry a named type like `SomeType` or `ns.Foo`. LexToken::Ident(_) => { let path = self.parse_path()?; Ok(AnnotationValue::Reference(path)) } _ => Err(ParseError::Syntax { message: alloc::format!("Expected annotation value, found {}", current.token), span: current.span, }), } }
/// Parse `[value, value, ...]` as an annotation-value array. Trailing /// commas are accepted. Empty `[]` is valid. fn parse_annotation_value_array(&mut self) -> Result<AnnotationValue, ParseError> { self.expect(LexToken::LeftBracket)?; let mut items = Vec::new(); while !matches!(self.current().token, LexToken::RightBracket) { items.push(self.parse_annotation_value()?); if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } } self.expect(LexToken::RightBracket)?; Ok(AnnotationValue::Array(items)) }
/// Parse `{ "key": value, ... }` as an annotation-value object. Keys /// must be string literals (not identifiers) so arbitrary JSON-legal /// keys like `"x-vendor-flag"` work without additional grammar. fn parse_annotation_value_object(&mut self) -> Result<AnnotationValue, ParseError> { self.expect(LexToken::LeftBrace)?; let mut entries = Vec::new(); while !matches!(self.current().token, LexToken::RightBrace) { let current = self.current(); let key = match ¤t.token { LexToken::StringLit(s) => s.clone(), _ => { return Err(ParseError::Syntax { message: alloc::format!( "Expected string literal as object key, found {}", current.token ), span: current.span, }); } }; self.advance(); self.expect(LexToken::Colon)?; let value = self.parse_annotation_value()?; entries.push((key, value)); if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } } self.expect(LexToken::RightBrace)?; Ok(AnnotationValue::Object(entries)) }
fn parse_record( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Record)?; let name = self.parse_ident()?; self.expect(LexToken::LeftBrace)?;
let mut fields = Vec::new(); let mut field_docs = Vec::new();
while !matches!(self.current().token, LexToken::RightBrace) { if let LexToken::DocComment(comment) = &self.current().token { let span = self.current().span; field_docs.push(DocComment { text: comment.clone(), span, }); self.advance(); } else { fields.push(self.parse_field(field_docs.clone())?); field_docs.clear(); } }
let end = self.expect(LexToken::RightBrace)?;
Ok(Item::Record(Record { docs, annotations, name, fields, span: Span::new(start.start, end.end), })) }
fn parse_field(&mut self, docs: Vec<DocComment>) -> Result<Field, ParseError> { let annotations = self.parse_annotations()?; let name = self.parse_field_name()?;
// Check for ! to mark as required (default is optional) let optional = if matches!(self.current().token, LexToken::Exclamation) { self.advance(); false // ! means required } else { true // default is optional };
self.expect(LexToken::Colon)?; let ty = self.parse_type()?;
// Comma is required (unless we're at the end) if !matches!(self.current().token, LexToken::RightBrace) { self.expect(LexToken::Comma)?; } else if matches!(self.current().token, LexToken::Comma) { // Allow trailing comma self.advance(); }
let span = Span::new(name.span.start, ty.span().end);
Ok(Field { docs, annotations, name, ty, optional, span, }) }
fn parse_inline_type( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Inline)?; self.expect(LexToken::Type)?; let name = self.parse_ident()?; self.expect(LexToken::Equals)?; let ty = self.parse_type()?; let end = self.expect(LexToken::Semicolon)?;
Ok(Item::InlineType(InlineType { docs, annotations, name, ty, span: Span::new(start.start, end.end), })) }
fn parse_def_type( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Def)?; self.expect(LexToken::Type)?; let name = self.parse_ident()?; // Backticked keywords are already converted to Ident by lexer self.expect(LexToken::Equals)?; let ty = self.parse_type()?; let end = self.expect(LexToken::Semicolon)?;
Ok(Item::DefType(DefType { docs, annotations, name, ty, span: Span::new(start.start, end.end), })) }
fn parse_token( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Token)?; let name = self.parse_ident()?; let end = self.expect(LexToken::Semicolon)?;
Ok(Item::Token(Token { docs, annotations, name, span: Span::new(start.start, end.end), })) }
fn parse_query( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Query)?; let name = self.parse_ident()?; self.expect(LexToken::LeftParen)?;
let params = self.parse_params()?;
let right_paren_span = self.expect(LexToken::RightParen)?;
// Check if there's a return type (colon present) let returns = if matches!(self.current().token, LexToken::Colon) { self.advance(); // consume colon
let output = self.parse_base_type()?;
if matches!(self.current().token, LexToken::Pipe) { self.advance(); if matches!(self.current().token, LexToken::Error) { self.advance(); let errors = self.parse_errors()?; let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); ReturnType::TypeWithErrors { success: output, errors, span: error_span, } } else { let mut types = alloc::vec![output]; types.push(self.parse_base_type()?);
while matches!(self.current().token, LexToken::Pipe) { self.advance(); types.push(self.parse_base_type()?); }
let span = Span::new(types[0].span().start, types.last().unwrap().span().end); // Return type unions are open by default (no ! support in return types yet) ReturnType::Type(Type::Union { types, closed: false, span, }) } } else { ReturnType::Type(output) } } else { // No return type specified ReturnType::None { span: right_paren_span, } };
let end = self.expect(LexToken::Semicolon)?;
Ok(Item::Query(Query { docs, annotations, name, params, returns, span: Span::new(start.start, end.end), })) }
fn parse_procedure( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Procedure)?; let name = self.parse_ident()?; self.expect(LexToken::LeftParen)?;
let params = self.parse_params()?;
let right_paren_span = self.expect(LexToken::RightParen)?;
// Check if there's a return type (colon present) let returns = if matches!(self.current().token, LexToken::Colon) { self.advance(); // consume colon
let output = self.parse_base_type()?;
if matches!(self.current().token, LexToken::Pipe) { self.advance(); if matches!(self.current().token, LexToken::Error) { self.advance(); let errors = self.parse_errors()?; let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); ReturnType::TypeWithErrors { success: output, errors, span: error_span, } } else { let mut types = alloc::vec![output]; types.push(self.parse_base_type()?);
while matches!(self.current().token, LexToken::Pipe) { self.advance(); types.push(self.parse_base_type()?); }
let span = Span::new(types[0].span().start, types.last().unwrap().span().end); // Return type unions are open by default (no ! support in return types yet) ReturnType::Type(Type::Union { types, closed: false, span, }) } } else { ReturnType::Type(output) } } else { // No return type specified ReturnType::None { span: right_paren_span, } };
let end = self.expect(LexToken::Semicolon)?;
Ok(Item::Procedure(Procedure { docs, annotations, name, params, returns, span: Span::new(start.start, end.end), })) }
fn parse_subscription( &mut self, docs: Vec<DocComment>, annotations: Vec<Annotation>, ) -> Result<Item, ParseError> { let start = self.expect(LexToken::Subscription)?; let name = self.parse_ident()?; self.expect(LexToken::LeftParen)?;
let params = self.parse_params()?;
self.expect(LexToken::RightParen)?;
// Check if there's a messages type (colon present) let messages = if matches!(self.current().token, LexToken::Colon) { self.advance(); // consume colon Some(self.parse_type()?) } else { None };
let end = self.expect(LexToken::Semicolon)?;
Ok(Item::Subscription(Subscription { docs, annotations, name, params, messages, span: Span::new(start.start, end.end), })) }
fn parse_use(&mut self) -> Result<Item, ParseError> { let start = self.expect(LexToken::Use)?; let path = self.parse_path()?;
let imports = if matches!(self.current().token, LexToken::Dot) { // use namespace.{ items } self.advance(); // consume . self.expect(LexToken::LeftBrace)?; // consume { let mut items = Vec::new();
while !matches!(self.current().token, LexToken::RightBrace) { // Check for 'main' keyword let current = self.current(); let name_ident = if let LexToken::Ident(name) = ¤t.token { if name == "main" { // Create a special identifier for main let ident = Ident { name: "main".into(), span: current.span, }; self.advance(); ident } else { self.parse_ident()? } } else { self.parse_ident()? };
// Check for alias (as keyword) let alias = if matches!(self.current().token, LexToken::As) { self.advance(); Some(self.parse_ident()?) } else { None };
items.push(UseItem { name: name_ident, alias, });
// Comma is optional before closing brace if matches!(self.current().token, LexToken::Comma) { self.advance(); } else if !matches!(self.current().token, LexToken::RightBrace) { return Err(ParseError::Syntax { message: alloc::format!( "Expected comma or closing brace, found {}", self.current().token ), span: self.current().span, }); } }
self.expect(LexToken::RightBrace)?; UseImports::Items(items) } else if matches!(self.current().token, LexToken::As) { // use namespace as alias self.advance(); let alias = self.parse_ident()?; UseImports::Items(alloc::vec![UseItem { name: path.segments.last().unwrap().clone(), alias: Some(alias), }]) } else { // use namespace (imports all) UseImports::All };
let end = self.expect(LexToken::Semicolon)?;
Ok(Item::Use(Use { path, imports, span: Span::new(start.start, end.end), })) }
fn parse_params(&mut self) -> Result<Vec<Field>, ParseError> { let mut params = Vec::new(); let mut doc_comments = Vec::new();
while !matches!(self.current().token, LexToken::RightParen) { if let LexToken::DocComment(comment) = &self.current().token { let span = self.current().span; doc_comments.push(DocComment { text: comment.clone(), span, }); self.advance(); } else { let annotations = self.parse_annotations()?; let name = self.parse_ident()?;
// Check for ! to mark as required (default is optional) let optional = if matches!(self.current().token, LexToken::Exclamation) { self.advance(); false // ! means required } else { true // default is optional };
self.expect(LexToken::Colon)?; let ty = self.parse_type()?;
let span = Span::new(name.span.start, ty.span().end);
params.push(Field { docs: doc_comments.clone(), annotations, name, ty, optional, span, }); doc_comments.clear();
if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } } }
Ok(params) }
fn parse_errors(&mut self) -> Result<Vec<ErrorDef>, ParseError> { self.expect(LexToken::LeftBrace)?;
let mut errors = Vec::new(); let mut doc_comments = Vec::new();
while !matches!(self.current().token, LexToken::RightBrace) { if let LexToken::DocComment(comment) = &self.current().token { let span = self.current().span; doc_comments.push(DocComment { text: comment.clone(), span, }); self.advance(); } else { let name = self.parse_ident()?; let span = name.span;
// Comma is required (unless we're at the end) if !matches!(self.current().token, LexToken::RightBrace) { self.expect(LexToken::Comma)?; } else if matches!(self.current().token, LexToken::Comma) { // Allow trailing comma self.advance(); }
errors.push(ErrorDef { docs: doc_comments.clone(), name, span, }); doc_comments.clear(); } }
self.expect(LexToken::RightBrace)?;
Ok(errors) }
fn parse_type(&mut self) -> Result<Type, ParseError> { let base = self.parse_base_type()?;
if matches!(self.current().token, LexToken::Pipe) { let mut types = alloc::vec![base];
let mut has_exclamation = false; while matches!(self.current().token, LexToken::Pipe) { self.advance(); if matches!(self.current().token, LexToken::Error) { break; } // Check if this is an exclamation mark (closed union marker) if matches!(self.current().token, LexToken::Exclamation) { has_exclamation = true; self.advance(); } else { types.push(self.parse_base_type()?); } }
let span = Span::new(types[0].span().start, types.last().unwrap().span().end); // Unions are open by default, closed if ! is present let closed = has_exclamation; return Ok(Type::Union { types, closed, span, }); }
Ok(base) }
fn parse_base_type(&mut self) -> Result<Type, ParseError> { let current = self.current(); let start = current.span.start;
let mut ty = match ¤t.token { LexToken::String => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::String, span, } } LexToken::Integer => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::Integer, span, } } LexToken::Boolean => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::Boolean, span, } } LexToken::Blob => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::Blob, span, } } LexToken::Bytes => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::Bytes, span, } } LexToken::Null => { let span = self.advance().span; Type::Primitive { kind: PrimitiveType::Null, span, } } LexToken::Unknown => { let span = self.advance().span; Type::Unknown { span } } LexToken::Ident(_) => { let path = self.parse_path()?; let span = path.span; Type::Reference { path, span } } LexToken::LeftBrace => { self.advance(); let mut fields = Vec::new(); let mut doc_comments = Vec::new();
while !matches!(self.current().token, LexToken::RightBrace) { if let LexToken::DocComment(comment) = &self.current().token { let span = self.current().span; doc_comments.push(DocComment { text: comment.clone(), span, }); self.advance(); } else { fields.push(self.parse_field(doc_comments.clone())?); doc_comments.clear(); } }
let end = self.expect(LexToken::RightBrace)?; Type::Object { fields, span: Span::new(start, end.end), } } LexToken::LeftParen => { self.advance(); let inner = self.parse_type()?; let end = self.expect(LexToken::RightParen)?; Type::Parenthesized { inner: alloc::boxed::Box::new(inner), span: Span::new(start, end.end), } } _ => { return Err(ParseError::Syntax { message: alloc::format!("Expected type, found {}", current.token), span: current.span, }); } };
// Handle array suffix: Type[] if matches!(self.current().token, LexToken::LeftBracket) { self.advance(); let end = self.expect(LexToken::RightBracket)?; ty = Type::Array { inner: alloc::boxed::Box::new(ty), span: Span::new(start, end.end), }; }
if matches!(self.current().token, LexToken::Constrained) { self.advance(); self.expect(LexToken::LeftBrace)?;
let mut constraints = Vec::new();
while !matches!(self.current().token, LexToken::RightBrace) { constraints.push(self.parse_constraint()?);
// Comma is required (unless we're at the end) if !matches!(self.current().token, LexToken::RightBrace) { self.expect(LexToken::Comma)?; } else if matches!(self.current().token, LexToken::Comma) { // Allow trailing comma self.advance(); } }
let end = self.expect(LexToken::RightBrace)?;
ty = Type::Constrained { base: alloc::boxed::Box::new(ty), constraints, span: Span::new(start, end.end), }; }
Ok(ty) }
fn parse_constraint(&mut self) -> Result<Constraint, ParseError> { let name = self.parse_ident()?; self.expect(LexToken::Colon)?;
let start = name.span.start; let current = self.current(); let current_span = current.span;
let constraint = match name.name.as_str() { "minLength" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::MinLength { value: value as usize, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for minLength"), span: current_span, }); } } "maxLength" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::MaxLength { value: value as usize, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for maxLength"), span: current_span, }); } } "minimum" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::Minimum { value, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for minimum"), span: current_span, }); } } "maximum" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::Maximum { value, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for maximum"), span: current_span, }); } } "enum" => { if matches!(current.token, LexToken::LeftBracket) { self.advance(); let mut values = Vec::new();
while !matches!(self.current().token, LexToken::RightBracket) { let current = self.current(); // Accept either string literals or identifier paths let value_ref = match ¤t.token { LexToken::StringLit(s) => { let v = ValueRef::Literal(s.clone()); self.advance(); v } LexToken::Ident(_) => { let path = self.parse_path()?; ValueRef::Reference(path) } _ => { return Err(ParseError::Syntax { message: alloc::format!( "Expected string literal or identifier in enum" ), span: current.span, }); } }; values.push(value_ref);
if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } }
let end = self.expect(LexToken::RightBracket)?; Constraint::Enum { values, span: Span::new(start, end.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected array for enum"), span: current_span, }); } } "format" => { if let LexToken::StringLit(s) = ¤t.token { let value = s.clone(); self.advance(); Constraint::Format { value, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected string for format"), span: current_span, }); } } "minGraphemes" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::MinGraphemes { value: value as usize, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for minGraphemes"), span: current_span, }); } } "maxGraphemes" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::MaxGraphemes { value: value as usize, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for maxGraphemes"), span: current_span, }); } } "maxSize" => { if let LexToken::IntLit(i) = current.token { let value = i; self.advance(); Constraint::MaxSize { value: value as usize, span: Span::new(start, current_span.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected integer for maxSize"), span: current_span, }); } } "accept" => { if matches!(current.token, LexToken::LeftBracket) { self.advance(); let mut mimes = Vec::new();
while !matches!(self.current().token, LexToken::RightBracket) { let current = self.current(); match ¤t.token { LexToken::StringLit(s) => { mimes.push(s.clone()); self.advance(); } _ => { return Err(ParseError::Syntax { message: alloc::format!("Expected string literal in accept"), span: current.span, }); } }
if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } }
let end = self.expect(LexToken::RightBracket)?; Constraint::Accept { mimes, span: Span::new(start, end.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected array for accept"), span: current_span, }); } } "knownValues" => { if matches!(current.token, LexToken::LeftBracket) { self.advance(); let mut values = Vec::new();
while !matches!(self.current().token, LexToken::RightBracket) { let current = self.current(); // Accept either string literals or identifier paths let value_ref = match ¤t.token { LexToken::StringLit(s) => { let v = ValueRef::Literal(s.clone()); self.advance(); v } LexToken::Ident(_) => { let path = self.parse_path()?; ValueRef::Reference(path) } _ => { return Err(ParseError::Syntax { message: alloc::format!( "Expected string literal or identifier in knownValues" ), span: current.span, }); } }; values.push(value_ref);
if matches!(self.current().token, LexToken::Comma) { self.advance(); } else { break; } }
let end = self.expect(LexToken::RightBracket)?; Constraint::KnownValues { values, span: Span::new(start, end.end), } } else { return Err(ParseError::Syntax { message: alloc::format!("Expected array for knownValues"), span: current_span, }); } } "default" => { use crate::ast::ConstraintValue; let end_span = current_span.end; let value = match ¤t.token { LexToken::StringLit(s) => { let v = ConstraintValue::String(s.clone()); self.advance(); v } LexToken::IntLit(i) => { let v = ConstraintValue::Integer(*i); self.advance(); v } LexToken::True => { let v = ConstraintValue::Boolean(true); self.advance(); v } LexToken::False => { let v = ConstraintValue::Boolean(false); self.advance(); v } LexToken::Ident(_) => { let path = self.parse_path()?; ConstraintValue::Reference(path) } _ => { return Err(ParseError::Syntax { message: alloc::format!( "Expected string, integer, boolean, or identifier for default" ), span: current_span, }); } }; Constraint::Default { value, span: Span::new(start, end_span), } } "const" => { use crate::ast::ConstraintValue; let end_span = current_span.end; let value = match ¤t.token { LexToken::StringLit(s) => { let v = ConstraintValue::String(s.clone()); self.advance(); v } LexToken::IntLit(i) => { let v = ConstraintValue::Integer(*i); self.advance(); v } LexToken::True => { let v = ConstraintValue::Boolean(true); self.advance(); v } LexToken::False => { let v = ConstraintValue::Boolean(false); self.advance(); v } LexToken::Ident(_) => { let path = self.parse_path()?; ConstraintValue::Reference(path) } _ => { return Err(ParseError::Syntax { message: alloc::format!( "Expected string, integer, boolean, or identifier for const" ), span: current_span, }); } }; Constraint::Const { value, span: Span::new(start, end_span), } } _ => { return Err(ParseError::Syntax { message: alloc::format!("Unknown constraint: {}", name.name), span: name.span, }); } };
Ok(constraint) }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_parse_record() { let input = r#"record user { name: string, age: integer, }"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Record(r) => { assert_eq!(r.name.name, "user"); assert_eq!(r.fields.len(), 2); } _ => panic!("Expected record"), } }
#[test] fn test_parse_inline_type() { let input = "inline type userId = string;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::InlineType(a) => { assert_eq!(a.name.name, "userId"); } _ => panic!("Expected inline type"), } }
#[test] fn test_parse_token() { let input = "token like;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Token(t) => { assert_eq!(t.name.name, "like"); } _ => panic!("Expected token"), } }
#[test] fn test_parse_query() { let input = "query getUser(id: string,): user;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.name.name, "getUser"); assert_eq!(q.params.len(), 1); } _ => panic!("Expected query"), } }
#[test] fn test_parse_query_with_errors() { let input = r#"query getUser(id: string,): user | error { /// User not found NotFound, };"#; let result = parse_lexicon(input); if let Err(e) = &result { eprintln!("Parse error: {:?}", e); } assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.name.name, "getUser"); match &q.returns { ReturnType::TypeWithErrors { errors, .. } => { assert_eq!(errors.len(), 1); assert_eq!(errors[0].name.name, "NotFound"); } _ => panic!("Expected TypeWithErrors"), } } _ => panic!("Expected query"), } }
#[test] fn test_parse_subscription() { let input = "subscription subscribeRepos(cursor: integer,): commit | identity;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Subscription(s) => { assert_eq!(s.name.name, "subscribeRepos"); assert_eq!(s.params.len(), 1); assert!(s.params[0].optional); } _ => panic!("Expected subscription"), } }
#[test] fn test_parse_constrained_type() { let input = r#"inline type shortString = string constrained { maxLength: 100, };"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::InlineType(a) => match &a.ty { Type::Constrained { constraints, .. } => { assert_eq!(constraints.len(), 1); } _ => panic!("Expected constrained type"), }, _ => panic!("Expected inline type"), } }
#[test] fn test_parse_union_type() { let input = "inline type result = success | failure;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::InlineType(a) => match &a.ty { Type::Union { types, .. } => { assert_eq!(types.len(), 2); } _ => panic!("Expected union type"), }, _ => panic!("Expected inline type"), } }
#[test] fn test_parse_array_type() { let input = "inline type userList = user[];"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::InlineType(a) => match &a.ty { Type::Array { .. } => {} _ => panic!("Expected array type"), }, _ => panic!("Expected inline type"), } }
#[test] fn test_parse_annotation() { let input = "@deprecated\nrecord old {}"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Record(r) => { assert_eq!(r.annotations.len(), 1); assert_eq!(r.annotations[0].name.name, "deprecated"); } _ => panic!("Expected record"), } }
#[test] fn test_parse_annotation_with_args() { let input = "@validate(min: 0, max: 100)\nrecord data {}"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Record(r) => { assert_eq!(r.annotations.len(), 1); assert_eq!(r.annotations[0].args.len(), 2); } _ => panic!("Expected record"), } }
#[test] fn test_parse_annotation_bare() { let input = "@deprecated\nquery foo();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 1); assert_eq!(q.annotations[0].selectors.len(), 0); assert_eq!(q.annotations[0].name.name, "deprecated"); } _ => panic!("Expected query"), } }
#[test] fn test_parse_annotation_single_selector() { let input = "@rust:deprecated\nquery bar();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 1); assert_eq!(q.annotations[0].selectors.len(), 1); assert_eq!(q.annotations[0].selectors[0].name, "rust"); assert_eq!(q.annotations[0].name.name, "deprecated"); } _ => panic!("Expected query"), } }
#[test] fn test_parse_annotation_multiple_selectors() { let input = "@rust,typescript:deprecated\nquery baz();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 1); assert_eq!(q.annotations[0].selectors.len(), 2); assert_eq!(q.annotations[0].selectors[0].name, "rust"); assert_eq!(q.annotations[0].selectors[1].name, "typescript"); assert_eq!(q.annotations[0].name.name, "deprecated"); } _ => panic!("Expected query"), } }
#[test] fn test_parse_annotation_multiple_separate() { let input = "@rust:deprecated\n@typescript:deprecated\nquery qux();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 2); assert_eq!(q.annotations[0].selectors[0].name, "rust"); assert_eq!(q.annotations[0].name.name, "deprecated"); assert_eq!(q.annotations[1].selectors[0].name, "typescript"); assert_eq!(q.annotations[1].name.name, "deprecated"); } _ => panic!("Expected query"), } }
#[test] fn test_parse_annotation_bare_with_args() { let input = "@foo(1)\n@bar(\"blah\", true)\n@baz(thing: 1, thang: 2)\nquery test();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 3); // @foo(1) assert_eq!(q.annotations[0].selectors.len(), 0); assert_eq!(q.annotations[0].name.name, "foo"); assert_eq!(q.annotations[0].args.len(), 1); // @bar("blah", true) assert_eq!(q.annotations[1].selectors.len(), 0); assert_eq!(q.annotations[1].name.name, "bar"); assert_eq!(q.annotations[1].args.len(), 2); // @baz(thing: 1, thang: 2) assert_eq!(q.annotations[2].selectors.len(), 0); assert_eq!(q.annotations[2].name.name, "baz"); assert_eq!(q.annotations[2].args.len(), 2); } _ => panic!("Expected query"), } }
#[test] fn test_parse_annotation_selectors_with_args() { let input = "@rust:derive(\"Debug, Clone\")\n@typescript:export(name: \"CustomName\")\n@rust,typescript:since(1, 2, 0)\nquery test();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.annotations.len(), 3); // @rust:derive("Debug, Clone") assert_eq!(q.annotations[0].selectors.len(), 1); assert_eq!(q.annotations[0].selectors[0].name, "rust"); assert_eq!(q.annotations[0].name.name, "derive"); assert_eq!(q.annotations[0].args.len(), 1); // @typescript:export(name: "CustomName") assert_eq!(q.annotations[1].selectors.len(), 1); assert_eq!(q.annotations[1].selectors[0].name, "typescript"); assert_eq!(q.annotations[1].name.name, "export"); assert_eq!(q.annotations[1].args.len(), 1); // @rust,typescript:since(1, 2, 0) assert_eq!(q.annotations[2].selectors.len(), 2); assert_eq!(q.annotations[2].selectors[0].name, "rust"); assert_eq!(q.annotations[2].selectors[1].name, "typescript"); assert_eq!(q.annotations[2].name.name, "since"); assert_eq!(q.annotations[2].args.len(), 3); } _ => panic!("Expected query"), } }
#[test] fn test_parse_optional_field() { let input = r#"record user { name: string, }"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); match &lexicon.items[0] { Item::Record(r) => { assert_eq!(r.fields.len(), 1); assert!(r.fields[0].optional); } _ => panic!("Expected record"), } }
#[test] fn test_parse_required_field() { let input = r#"record user { name!: string, }"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); match &lexicon.items[0] { Item::Record(r) => { assert_eq!(r.fields.len(), 1); assert!(!r.fields[0].optional); } _ => panic!("Expected record"), } }
#[test] fn test_parse_enum_constraint() { let input = r#"inline type status = string constrained { enum: ["active", "inactive"], };"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); match &lexicon.items[0] { Item::InlineType(a) => match &a.ty { Type::Constrained { constraints, .. } => match &constraints[0] { Constraint::Enum { values, .. } => { assert_eq!(values.len(), 2); } _ => panic!("Expected enum constraint"), }, _ => panic!("Expected constrained type"), }, _ => panic!("Expected inline type"), } }
#[test] fn test_parse_query_without_return_type() { let input = "query foo();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.name.name, "foo"); assert_eq!(q.params.len(), 0); assert!(matches!(q.returns, ReturnType::None { .. })); } _ => panic!("Expected query"), } }
#[test] fn test_parse_query_with_params_no_return() { let input = "query bar(id: string, count: integer);"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Query(q) => { assert_eq!(q.name.name, "bar"); assert_eq!(q.params.len(), 2); assert!(matches!(q.returns, ReturnType::None { .. })); } _ => panic!("Expected query"), } }
#[test] fn test_parse_procedure_without_return_type() { let input = "procedure baz();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Procedure(p) => { assert_eq!(p.name.name, "baz"); assert_eq!(p.params.len(), 0); assert!(matches!(p.returns, ReturnType::None { .. })); } _ => panic!("Expected procedure"), } }
#[test] fn test_parse_subscription_without_messages() { let input = "subscription updates();"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Subscription(s) => { assert_eq!(s.name.name, "updates"); assert_eq!(s.params.len(), 0); assert!(s.messages.is_none()); } _ => panic!("Expected subscription"), } }
#[test] fn test_parse_use_with_main() { let input = "use com.example.thread.{ main };"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Use(u) => { assert_eq!(u.path.to_string(), "com.example.thread"); match &u.imports { UseImports::Items(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0].name.name, "main"); assert!(items[0].alias.is_none()); } _ => panic!("Expected UseImports::Items"), } } _ => panic!("Expected use statement"), } }
#[test] fn test_parse_use_with_main_and_alias() { let input = "use com.example.thread.{ main as ThreadRecord };"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Use(u) => { assert_eq!(u.path.to_string(), "com.example.thread"); match &u.imports { UseImports::Items(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0].name.name, "main"); assert_eq!(items[0].alias.as_ref().unwrap().name, "ThreadRecord"); } _ => panic!("Expected UseImports::Items"), } } _ => panic!("Expected use statement"), } }
#[test] fn test_parse_use_with_multiple_items() { let input = "use com.example.{ main, foo, bar as Baz };"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::Use(u) => { assert_eq!(u.path.to_string(), "com.example"); match &u.imports { UseImports::Items(items) => { assert_eq!(items.len(), 3); assert_eq!(items[0].name.name, "main"); assert!(items[0].alias.is_none()); assert_eq!(items[1].name.name, "foo"); assert!(items[1].alias.is_none()); assert_eq!(items[2].name.name, "bar"); assert_eq!(items[2].alias.as_ref().unwrap().name, "Baz"); } _ => panic!("Expected UseImports::Items"), } } _ => panic!("Expected use statement"), } }
#[test] fn test_parse_keywords_in_paths() { let input = r#"def type test = { field1: app.bsky.embed.record, field2: com.example.query, field3: com.example.string.type, };"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { Item::DefType(d) => { assert_eq!(d.name.name, "test"); match &d.ty { Type::Object { fields, .. } => { assert_eq!(fields.len(), 3); // Verify the paths contain keywords match &fields[0].ty { Type::Reference { path, .. } => { assert_eq!(path.to_string(), "app.bsky.embed.record"); } _ => panic!("Expected reference type"), } match &fields[1].ty { Type::Reference { path, .. } => { assert_eq!(path.to_string(), "com.example.query"); } _ => panic!("Expected reference type"), } match &fields[2].ty { Type::Reference { path, .. } => { assert_eq!(path.to_string(), "com.example.string.type"); } _ => panic!("Expected reference type"), } } _ => panic!("Expected object type"), } } _ => panic!("Expected def type"), } }}