gioser

tawasuyu/brahman

Rust 334TOML 129Markdown 39JSON 174745 Definiciones

⎇ 1da4ee11d7525a84d65df447bfde39c4ed085ab9 crates/modules/shuma/shuma-intent/src/parse.rs Rust · 118 líneas · 3.7 KB Historia Quién tocó qué crudo

feat(shuma): núcleo del shell — parser de intenciones + grafo de contexto 1da4ee11d · sergio ·

1//! Parser de intenciones del prompt de shuma.
2//!
3//! Una "intención" es una línea del prompt: etapas separadas por `|`.
4//! Cada etapa es un comando a ejecutar, o un token de referencia a un
5//! resultado previo de la sesión (`%cN` un comando, `%pN` un buffer
6//! intermedio). Ej: `ssh nodo 'cat data.json' | %p1 | sort`.
7
8use serde::{Deserialize, Serialize};
9
10/// Referencia a un resultado de la sesión.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Ref {
13    /// `%cN` — un comando registrado de la sesión.
14    Command(u32),
15    /// `%pN` — un buffer intermedio producido por un comando.
16    Buffer(u32),
17}
18
19impl Ref {
20    /// Parsea un token aislado `%c3` / `%p12`. `None` si no es un token.
21    pub fn parse(token: &str) -> Option<Ref> {
22        let rest = token.trim().strip_prefix('%')?;
23        let mut chars = rest.chars();
24        let kind = chars.next()?;
25        let num: u32 = chars.as_str().parse().ok()?;
26        match kind {
27            'c' => Some(Ref::Command(num)),
28            'p' => Some(Ref::Buffer(num)),
29            _ => None,
30        }
31    }
32}
33
34/// Una etapa del pipeline de una intención.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub enum Stage {
37    /// Comando a ejecutar (texto crudo; puede ser `ssh host '...'`).
38    Exec(String),
39    /// Inyección de un resultado previo de la sesión.
40    Inject(Ref),
41}
42
43/// Una intención parseada: etapas conectadas por pipes.
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct Intention {
46    pub stages: Vec<Stage>,
47}
48
49impl Intention {
50    /// Parsea una línea del prompt. Las etapas se separan por `|`; una
51    /// etapa que es exactamente un token `%pN`/`%cN` es `Inject`, el
52    /// resto es `Exec`. Las etapas vacías se descartan.
53    pub fn parse(line: &str) -> Intention {
54        let stages = line
55            .split('|')
56            .map(str::trim)
57            .filter(|s| !s.is_empty())
58            .map(|s| match Ref::parse(s) {
59                Some(r) => Stage::Inject(r),
60                None => Stage::Exec(s.to_string()),
61            })
62            .collect();
63        Intention { stages }
64    }
65
66    /// `true` si la intención no tiene etapas (línea vacía).
67    pub fn is_empty(&self) -> bool {
68        self.stages.is_empty()
69    }
70
71    /// Todas las referencias que la intención consume.
72    pub fn refs(&self) -> Vec<Ref> {
73        self.stages
74            .iter()
75            .filter_map(|s| match s {
76                Stage::Inject(r) => Some(*r),
77                Stage::Exec(_) => None,
78            })
79            .collect()
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn parses_ref_tokens() {
89        assert_eq!(Ref::parse("%c3"), Some(Ref::Command(3)));
90        assert_eq!(Ref::parse("%p12"), Some(Ref::Buffer(12)));
91        assert_eq!(Ref::parse(" %p1 "), Some(Ref::Buffer(1)));
92        assert_eq!(Ref::parse("sort"), None);
93        assert_eq!(Ref::parse("%x9"), None);
94        assert_eq!(Ref::parse("%p"), None);
95    }
96
97    #[test]
98    fn parses_the_spec_example() {
99        // ssh nodo 'cat data.json' | %p1 | sort
100        let i = Intention::parse("ssh nodo 'cat data.json' | %p1 | sort");
101        assert_eq!(i.stages.len(), 3);
102        assert_eq!(i.stages[0], Stage::Exec("ssh nodo 'cat data.json'".into()));
103        assert_eq!(i.stages[1], Stage::Inject(Ref::Buffer(1)));
104        assert_eq!(i.stages[2], Stage::Exec("sort".into()));
105    }
106
107    #[test]
108    fn refs_extracts_only_injections() {
109        let i = Intention::parse("cat x | %p1 | %c2 | wc -l");
110        assert_eq!(i.refs(), vec![Ref::Buffer(1), Ref::Command(2)]);
111    }
112
113    #[test]
114    fn empty_line_is_empty_intention() {
115        assert!(Intention::parse("   ").is_empty());
116        assert!(Intention::parse("| |").is_empty());
117    }
118}