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