Skip to main content

polyspec_crudui_validator/expr/
token.rs

1//! CRUDUI expression tokens (expressions.md §1, JS `types.ts` TokenType parity).
2//!
3//! `TokenType` is the terminal tag; `Token` is `{type,value,literal}`. WHITESPACE
4//! never reaches the token array (the lexer drops it); EOF is always the final
5//! token. The serialized shape (`to_value`) byte-matches the shared fixture's
6//! `tokens` entries — no `position` field, since the 4-language token contract
7//! excludes positions (the Rust AST holds none either).
8//!
9//! Intentionally absent (GRAMMAR §1·§10): arithmetic `+ - * / %`, function calls,
10//! method calls, regex, assignment `=`, bitwise, root path `/`. Do not add them.
11
12use serde_json::{json, Value};
13
14/// Terminal token kind. The string spelling matches the shared fixture's
15/// `token.type` ("DOT"|"IDENTIFIER"|…).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TokenType {
18    /// Quoted string literal `'…'` / `"…"`.
19    String,
20    /// Integer or decimal number literal.
21    Number,
22    /// `true` / `false`.
23    Boolean,
24    /// `null`.
25    Null,
26    /// Path segment / unquoted identifier `[A-Za-z_][A-Za-z0-9_]*`.
27    Identifier,
28    /// Single leading `.`.
29    Dot,
30    /// Two-or-more dots `..` / `…` (parent ascent).
31    DotDot,
32    /// Wildcard `*`.
33    Asterisk,
34    /// `==`.
35    Eq,
36    /// `!=`.
37    Ne,
38    /// `>`.
39    Gt,
40    /// `>=`.
41    Ge,
42    /// `<`.
43    Lt,
44    /// `<=`.
45    Le,
46    /// `&&`.
47    And,
48    /// `||`.
49    Or,
50    /// `!`.
51    Not,
52    /// `in`.
53    In,
54    /// `not in`.
55    NotIn,
56    /// `(`.
57    LParen,
58    /// `)`.
59    RParen,
60    /// `[`.
61    LBracket,
62    /// `]`.
63    RBracket,
64    /// `,`.
65    Comma,
66    /// `?`.
67    Question,
68    /// `:`.
69    Colon,
70    /// End of input.
71    Eof,
72    /// Whitespace run (dropped before the token array is built).
73    Whitespace,
74    /// One unrecognized character.
75    Invalid,
76}
77
78impl TokenType {
79    /// The fixture string tag for this token type.
80    pub fn as_str(self) -> &'static str {
81        match self {
82            TokenType::String => "STRING",
83            TokenType::Number => "NUMBER",
84            TokenType::Boolean => "BOOLEAN",
85            TokenType::Null => "NULL",
86            TokenType::Identifier => "IDENTIFIER",
87            TokenType::Dot => "DOT",
88            TokenType::DotDot => "DOT_DOT",
89            TokenType::Asterisk => "ASTERISK",
90            TokenType::Eq => "EQ",
91            TokenType::Ne => "NE",
92            TokenType::Gt => "GT",
93            TokenType::Ge => "GE",
94            TokenType::Lt => "LT",
95            TokenType::Le => "LE",
96            TokenType::And => "AND",
97            TokenType::Or => "OR",
98            TokenType::Not => "NOT",
99            TokenType::In => "IN",
100            TokenType::NotIn => "NOT_IN",
101            TokenType::LParen => "LPAREN",
102            TokenType::RParen => "RPAREN",
103            TokenType::LBracket => "LBRACKET",
104            TokenType::RBracket => "RBRACKET",
105            TokenType::Comma => "COMMA",
106            TokenType::Question => "QUESTION",
107            TokenType::Colon => "COLON",
108            TokenType::Eof => "EOF",
109            TokenType::Whitespace => "WHITESPACE",
110            TokenType::Invalid => "INVALID",
111        }
112    }
113}
114
115/// A literal payload carried by a value token. `value` for STRING/NUMBER/
116/// BOOLEAN/NULL, the dot count for DOT_DOT, the identifier text for IDENTIFIER;
117/// `None` for everything else.
118#[derive(Debug, Clone, PartialEq)]
119pub enum Literal {
120    /// Decoded string content (STRING) or identifier text (IDENTIFIER).
121    Str(String),
122    /// Integer number literal (the fixture keeps integers as integers).
123    Int(i64),
124    /// Decimal number literal.
125    Float(f64),
126    /// Boolean keyword literal.
127    Bool(bool),
128    /// Dot count for a DOT_DOT token.
129    DotCount(i64),
130    /// No literal payload.
131    None,
132}
133
134impl Literal {
135    fn to_value(&self) -> Value {
136        match self {
137            Literal::Str(s) => Value::String(s.clone()),
138            Literal::Int(n) => json!(n),
139            Literal::Float(f) => json!(f),
140            Literal::Bool(b) => Value::Bool(*b),
141            Literal::DotCount(n) => json!(n),
142            Literal::None => Value::Null,
143        }
144    }
145}
146
147/// A single lexical token. `value` is the raw source text; `literal` is the
148/// parsed payload (or `None`). No position field — the 4-language token contract
149/// excludes positions, matching the shared fixture's token shape.
150#[derive(Debug, Clone, PartialEq)]
151pub struct Token {
152    /// The token kind.
153    pub token_type: TokenType,
154    /// Raw source text for this token.
155    pub value: String,
156    /// Parsed literal payload, if any.
157    pub literal: Literal,
158}
159
160impl Token {
161    /// Build a token from its type, raw text, and literal payload.
162    pub fn new(token_type: TokenType, value: impl Into<String>, literal: Literal) -> Self {
163        Token {
164            token_type,
165            value: value.into(),
166            literal,
167        }
168    }
169
170    /// Serialize to the shared-fixture token shape `{type,value,literal}`.
171    pub fn to_value(&self) -> Value {
172        json!({
173            "type": self.token_type.as_str(),
174            "value": self.value,
175            "literal": self.literal.to_value(),
176        })
177    }
178}