osp_cli/dsl/parse/
pipeline.rs1use crate::dsl::model::{ParsedPipeline, ParsedStage, ParsedStageKind};
2use crate::dsl::verbs::is_registered_explicit_verb;
3
4use super::lexer::{LexerError, StageSegment, split_pipeline, tokenize_stage};
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct Pipeline {
8 pub command: String,
9 pub stages: Vec<String>,
10}
11
12pub fn parse_pipeline(line: &str) -> Result<Pipeline, LexerError> {
14 let segments = split_pipeline(line)?;
15
16 let command = segments
17 .first()
18 .map(|segment| segment.raw.clone())
19 .unwrap_or_default();
20
21 let stages = if segments.len() > 1 {
22 segments[1..]
23 .iter()
24 .map(|segment| segment.raw.clone())
25 .collect()
26 } else {
27 Vec::new()
28 };
29
30 Ok(Pipeline { command, stages })
31}
32
33pub fn parse_stage(raw_stage: &str) -> Result<ParsedStage, LexerError> {
40 let segment = stage_segment_from_raw(raw_stage);
41
42 if segment.raw.is_empty() {
43 return Ok(empty_quick_stage(raw_stage));
44 }
45
46 let tokens = tokenize_stage(&segment)?;
47
48 let Some(first) = tokens.first() else {
49 return Ok(empty_quick_stage(raw_stage));
50 };
51
52 let verb = first.text.to_ascii_uppercase();
53 let spec = stage_spec_after_first_token(&segment, first.span.end);
54
55 Ok(ParsedStage::new(
56 classify_stage_kind(&verb),
57 verb,
58 spec,
59 segment.raw,
60 ))
61}
62
63pub fn parse_stage_list(stages: &[String]) -> Result<ParsedPipeline, LexerError> {
65 Ok(ParsedPipeline {
66 raw: stages.join(" | "),
67 stages: stages
68 .iter()
69 .map(|stage| parse_stage(stage))
70 .collect::<Result<Vec<_>, _>>()?,
71 })
72}
73
74fn stage_segment_from_raw(raw_stage: &str) -> StageSegment {
75 let trimmed = raw_stage.trim();
76 StageSegment {
77 raw: trimmed.to_string(),
78 span: super::lexer::Span {
79 start: 0,
80 end: trimmed.len(),
81 },
82 }
83}
84
85fn empty_quick_stage(raw_stage: &str) -> ParsedStage {
86 ParsedStage::new(ParsedStageKind::Quick, "", "", raw_stage)
87}
88
89fn stage_spec_after_first_token(segment: &StageSegment, token_end: usize) -> String {
90 if token_end > segment.raw.len() {
91 return String::new();
92 }
93 segment.raw[token_end..].trim().to_string()
94}
95
96fn classify_stage_kind(verb: &str) -> ParsedStageKind {
97 if is_registered_explicit_verb(verb) {
98 return ParsedStageKind::Explicit;
99 }
100
101 if verb.len() == 1 && verb.chars().all(|ch| ch.is_ascii_alphabetic()) {
102 return ParsedStageKind::UnknownExplicit;
103 }
104
105 ParsedStageKind::Quick
106}
107
108#[cfg(test)]
109mod tests {
110 use crate::dsl::model::ParsedStageKind;
111
112 use super::{LexerError, parse_pipeline, parse_stage, parse_stage_list};
113
114 #[test]
115 fn parse_pipeline_extracts_command_and_stages() {
116 let parsed =
117 parse_pipeline("ldap user oistes | P uid,cn | F uid=oistes").expect("valid pipeline");
118 assert_eq!(parsed.command, "ldap user oistes");
119 assert_eq!(parsed.stages, vec!["P uid,cn", "F uid=oistes"]);
120 }
121
122 #[test]
123 fn parse_pipeline_ignores_empty_segments_like_python() {
124 let parsed =
125 parse_pipeline("ldap user oistes || P uid | | F uid=oistes").expect("valid pipeline");
126 assert_eq!(parsed.command, "ldap user oistes");
127 assert_eq!(parsed.stages, vec!["P uid", "F uid=oistes"]);
128 }
129
130 #[test]
131 fn parse_pipeline_rejects_invalid_quotes() {
132 let err =
133 parse_pipeline("ldap user 'oops | P uid").expect_err("invalid quotes should fail");
134 assert!(matches!(err, LexerError::UnterminatedSingleQuote { .. }));
135 }
136
137 #[test]
138 fn parse_pipeline_rejects_trailing_escape() {
139 let err = parse_pipeline("ldap user foo\\").expect_err("trailing escape should fail");
140 assert!(matches!(err, LexerError::TrailingEscape { .. }));
141 }
142
143 #[test]
144 fn parse_stage_extracts_verb_and_spec() {
145 let parsed = parse_stage("F uid=oistes").expect("stage should parse");
146 assert_eq!(parsed.kind, ParsedStageKind::Explicit);
147 assert_eq!(parsed.verb, "F");
148 assert_eq!(parsed.spec, "uid=oistes");
149 }
150
151 #[test]
152 fn parse_stage_with_only_term_becomes_quick_candidate() {
153 let parsed = parse_stage("uid").expect("stage should parse");
154 assert_eq!(parsed.kind, ParsedStageKind::Quick);
155 assert_eq!(parsed.verb, "UID");
156 assert_eq!(parsed.spec, "");
157 }
158
159 #[test]
160 fn parse_stage_marks_unknown_single_letter_verb_as_explicit() {
161 let parsed = parse_stage("R oist").expect("stage should parse");
162 assert_eq!(parsed.kind, ParsedStageKind::UnknownExplicit);
163 assert_eq!(parsed.verb, "R");
164 assert_eq!(parsed.spec, "oist");
165 }
166
167 #[test]
168 fn parse_stage_list_rejects_invalid_quoted_stage() {
169 let err = parse_stage_list(&[r#"F note="oops"#.to_string()])
170 .expect_err("invalid quotes should fail");
171 assert!(matches!(err, LexerError::UnterminatedDoubleQuote { .. }));
172 }
173
174 #[test]
175 fn parse_stage_list_rejects_trailing_escape() {
176 let err = parse_stage_list(&["F path=C:\\Temp\\".to_string()])
177 .expect_err("trailing escape should fail");
178 assert!(matches!(err, LexerError::TrailingEscape { .. }));
179 }
180}