telltale_runtime/topology/
parser.rs1use super::{Location, RoleFamilyConstraint, Topology, TopologyConstraint, TopologyMode};
6use crate::identifiers::{
7 Datacenter, Endpoint as TopologyEndpoint, IdentifierError, Namespace, Region, RoleName,
8};
9use crate::ChannelCapacity;
10use pest::Parser;
11use pest_derive::Parser;
12use thiserror::Error;
13
14#[derive(Parser)]
15#[grammar = "compiler/topology.pest"]
16struct TopologyParser;
17
18#[derive(Debug, Clone, Error)]
20pub enum TopologyParseError {
21 #[error("Parse error: {0}")]
22 ParseError(String),
23
24 #[error("Unknown mode: {0}")]
25 UnknownMode(String),
26
27 #[error("Invalid location: {0}")]
28 InvalidLocation(String),
29
30 #[error("Invalid constraint: {0}")]
31 InvalidConstraint(String),
32
33 #[error("Invalid capacity: {0}")]
34 InvalidCapacity(String),
35
36 #[error("Invalid identifier: {0}")]
37 InvalidIdentifier(IdentifierError),
38}
39
40impl From<pest::error::Error<Rule>> for TopologyParseError {
41 fn from(e: pest::error::Error<Rule>) -> Self {
42 TopologyParseError::ParseError(e.to_string())
43 }
44}
45
46impl From<IdentifierError> for TopologyParseError {
47 fn from(err: IdentifierError) -> Self {
48 TopologyParseError::InvalidIdentifier(err)
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct ParsedTopology {
55 pub name: String,
57 pub for_choreography: String,
59 pub topology: Topology,
61}
62
63pub fn parse_topology(input: &str) -> Result<ParsedTopology, TopologyParseError> {
65 let pairs = TopologyParser::parse(Rule::topology, input)?;
66 let mut name = String::new();
67 let mut for_choreography = String::new();
68 let mut topology = Topology::new();
69
70 for pair in pairs {
71 if pair.as_rule() == Rule::topology {
72 let mut inner = pair.into_inner();
73
74 if let Some(name_pair) = inner.next() {
76 name = name_pair.as_str().to_string();
77 }
78
79 if let Some(for_pair) = inner.next() {
81 for_choreography = for_pair.as_str().to_string();
82 }
83
84 if let Some(body_pair) = inner.next() {
86 topology = parse_topology_body(body_pair)?;
87 }
88 }
89 }
90
91 Ok(ParsedTopology {
92 name,
93 for_choreography,
94 topology,
95 })
96}
97
98fn parse_topology_body(pair: pest::iterators::Pair<Rule>) -> Result<Topology, TopologyParseError> {
99 let mut topology = Topology::new();
100
101 for inner in pair.into_inner() {
102 match inner.as_rule() {
103 Rule::topology_mode => {
104 topology.mode = Some(parse_topology_mode(inner)?);
105 }
106 Rule::topology_mappings => {
107 for mapping in inner.into_inner() {
108 let (role, location) = parse_topology_mapping(mapping)?;
109 topology.locations.insert(role, location);
110 }
111 }
112 Rule::topology_constraints => {
113 for constraint in inner.into_inner() {
114 if constraint.as_rule() == Rule::constraint_decl {
115 topology.constraints.push(parse_constraint(constraint)?);
116 }
117 }
118 }
119 Rule::channel_capacities_block => {
120 for decl in inner.into_inner() {
121 if decl.as_rule() == Rule::channel_capacity_decl {
122 let (sender, receiver, capacity) = parse_channel_capacity_decl(decl)?;
123 topology
124 .channel_capacities
125 .insert((sender, receiver), capacity);
126 }
127 }
128 }
129 Rule::role_constraints_block => {
130 for decl in inner.into_inner() {
131 if decl.as_rule() == Rule::role_constraint_decl {
132 let (family, constraint) = parse_role_constraint_decl(decl)?;
133 topology.role_constraints.insert(family, constraint);
134 }
135 }
136 }
137 _ => {}
138 }
139 }
140
141 Ok(topology)
142}
143
144fn parse_topology_mode(
145 pair: pest::iterators::Pair<Rule>,
146) -> Result<TopologyMode, TopologyParseError> {
147 for inner in pair.into_inner() {
148 if inner.as_rule() == Rule::topology_mode_value {
149 return parse_mode_value(inner);
150 }
151 }
152 Err(TopologyParseError::UnknownMode("empty mode".to_string()))
153}
154
155fn parse_mode_value(pair: pest::iterators::Pair<Rule>) -> Result<TopologyMode, TopologyParseError> {
156 let inner = pair.into_inner().next();
157 match inner {
158 Some(p) => match p.as_rule() {
159 Rule::kubernetes_mode => {
160 let namespace = p.into_inner().next().map(|i| i.as_str()).ok_or_else(|| {
161 TopologyParseError::InvalidConstraint(
162 "kubernetes mode requires a namespace".to_string(),
163 )
164 })?;
165 Ok(TopologyMode::Kubernetes(Namespace::new(namespace)?))
166 }
167 Rule::consul_mode => {
168 let datacenter = p.into_inner().next().map(|i| i.as_str()).ok_or_else(|| {
169 TopologyParseError::InvalidConstraint(
170 "consul mode requires a datacenter".to_string(),
171 )
172 })?;
173 Ok(TopologyMode::Consul(Datacenter::new(datacenter)?))
174 }
175 _ => {
176 let s = p.as_str();
177 match s {
178 "local" => Ok(TopologyMode::Local),
179 "per_role" => Ok(TopologyMode::PerRole),
180 _ => Err(TopologyParseError::UnknownMode(s.to_string())),
181 }
182 }
183 },
184 None => Ok(TopologyMode::Local),
185 }
186}
187
188fn parse_topology_mapping(
189 pair: pest::iterators::Pair<Rule>,
190) -> Result<(RoleName, Location), TopologyParseError> {
191 let mut inner = pair.into_inner();
192 let role = inner
193 .next()
194 .map(|p| RoleName::new(p.as_str()))
195 .transpose()?
196 .ok_or_else(|| TopologyParseError::InvalidConstraint("missing role".to_string()))?;
197 let location = inner
198 .next()
199 .map(|p| parse_location(p))
200 .transpose()?
201 .unwrap_or(Location::Local);
202
203 Ok((role, location))
204}
205
206fn parse_location(pair: pest::iterators::Pair<Rule>) -> Result<Location, TopologyParseError> {
207 let inner = pair.into_inner().next();
208 match inner {
209 Some(p) => match p.as_rule() {
210 Rule::local_location => Ok(Location::Local),
211 Rule::colocated_location => {
212 let peer = p
213 .into_inner()
214 .next()
215 .map(|i| RoleName::new(i.as_str()))
216 .transpose()?
217 .ok_or_else(|| {
218 TopologyParseError::InvalidLocation("colocated requires a role".to_string())
219 })?;
220 Ok(Location::Colocated(peer))
221 }
222 Rule::endpoint => Ok(Location::Remote(TopologyEndpoint::new(p.as_str())?)),
223 _ => {
224 let s = p.as_str();
225 if s == "local" {
226 Ok(Location::Local)
227 } else {
228 Ok(Location::Remote(TopologyEndpoint::new(s)?))
229 }
230 }
231 },
232 None => Ok(Location::Local),
233 }
234}
235
236fn parse_constraint(
237 pair: pest::iterators::Pair<Rule>,
238) -> Result<TopologyConstraint, TopologyParseError> {
239 let inner = pair
240 .into_inner()
241 .next()
242 .ok_or_else(|| TopologyParseError::InvalidConstraint("empty constraint".to_string()))?;
243
244 match inner.as_rule() {
245 Rule::colocated_constraint => {
246 let mut idents = inner.into_inner();
247 let r1 = idents
248 .next()
249 .map(|p| RoleName::new(p.as_str()))
250 .transpose()?
251 .ok_or_else(|| {
252 TopologyParseError::InvalidConstraint(
253 "colocated requires two roles".to_string(),
254 )
255 })?;
256 let r2 = idents
257 .next()
258 .map(|p| RoleName::new(p.as_str()))
259 .transpose()?
260 .ok_or_else(|| {
261 TopologyParseError::InvalidConstraint(
262 "colocated requires two roles".to_string(),
263 )
264 })?;
265 Ok(TopologyConstraint::Colocated(r1, r2))
266 }
267 Rule::separated_constraint => {
268 let roles: Vec<RoleName> = inner
269 .into_inner()
270 .flat_map(|p| p.into_inner())
271 .map(|p| RoleName::new(p.as_str()))
272 .collect::<Result<Vec<_>, _>>()?;
273 if roles.len() >= 2 {
275 Ok(TopologyConstraint::Separated(
276 roles[0].clone(),
277 roles[1].clone(),
278 ))
279 } else {
280 Err(TopologyParseError::InvalidConstraint(
281 "separated requires at least 2 roles".to_string(),
282 ))
283 }
284 }
285 Rule::pinned_constraint => {
286 let mut inner_iter = inner.into_inner();
287 let role = inner_iter
288 .next()
289 .map(|p| RoleName::new(p.as_str()))
290 .transpose()?
291 .ok_or_else(|| {
292 TopologyParseError::InvalidConstraint("pinned requires a role".to_string())
293 })?;
294 let location = inner_iter
295 .next()
296 .map(|p| parse_location(p))
297 .transpose()?
298 .unwrap_or(Location::Local);
299 Ok(TopologyConstraint::Pinned(role, location))
300 }
301 Rule::region_constraint => {
302 let mut idents = inner.into_inner();
303 let role = idents
304 .next()
305 .map(|p| RoleName::new(p.as_str()))
306 .transpose()?
307 .ok_or_else(|| {
308 TopologyParseError::InvalidConstraint("region requires a role".to_string())
309 })?;
310 let region = idents
311 .next()
312 .map(|p| Region::new(p.as_str()))
313 .transpose()?
314 .ok_or_else(|| {
315 TopologyParseError::InvalidConstraint("region requires a value".to_string())
316 })?;
317 Ok(TopologyConstraint::Region(role, region))
318 }
319 _ => Err(TopologyParseError::InvalidConstraint(format!(
320 "unknown constraint type: {:?}",
321 inner.as_rule()
322 ))),
323 }
324}
325
326fn parse_channel_capacity_decl(
327 pair: pest::iterators::Pair<Rule>,
328) -> Result<(RoleName, RoleName, ChannelCapacity), TopologyParseError> {
329 let mut inner = pair.into_inner();
330 let sender = inner
331 .next()
332 .map(|p| RoleName::new(p.as_str()))
333 .transpose()?
334 .ok_or_else(|| TopologyParseError::InvalidConstraint("missing sender".to_string()))?;
335 let receiver = inner
336 .next()
337 .map(|p| RoleName::new(p.as_str()))
338 .transpose()?
339 .ok_or_else(|| TopologyParseError::InvalidConstraint("missing receiver".to_string()))?;
340 let capacity = inner
341 .next()
342 .ok_or_else(|| TopologyParseError::InvalidConstraint("missing capacity".to_string()))?
343 .as_str()
344 .parse::<u32>()
345 .map_err(|e| TopologyParseError::InvalidCapacity(e.to_string()))?;
346 let capacity = ChannelCapacity::try_new(capacity)
347 .map_err(|e| TopologyParseError::InvalidCapacity(e.to_string()))?;
348
349 Ok((sender, receiver, capacity))
350}
351
352fn parse_role_constraint_decl(
353 pair: pest::iterators::Pair<Rule>,
354) -> Result<(String, RoleFamilyConstraint), TopologyParseError> {
355 let mut inner = pair.into_inner();
356
357 let family = inner
359 .next()
360 .map(|p| p.as_str().to_string())
361 .ok_or_else(|| {
362 TopologyParseError::InvalidConstraint("role constraint missing family name".to_string())
363 })?;
364
365 let spec = inner.next().ok_or_else(|| {
367 TopologyParseError::InvalidConstraint("role constraint missing specification".to_string())
368 })?;
369
370 let constraint = parse_role_constraint_spec(spec)?;
371 Ok((family, constraint))
372}
373
374fn parse_role_constraint_spec(
375 pair: pest::iterators::Pair<Rule>,
376) -> Result<RoleFamilyConstraint, TopologyParseError> {
377 let mut min: Option<u32> = None;
378 let mut max: Option<u32> = None;
379
380 for inner in pair.into_inner() {
381 match inner.as_rule() {
382 Rule::min_constraint => {
383 let value = inner
384 .into_inner()
385 .next()
386 .and_then(|p| p.as_str().parse::<u32>().ok())
387 .ok_or_else(|| {
388 TopologyParseError::InvalidConstraint(
389 "min constraint requires integer value".to_string(),
390 )
391 })?;
392 min = Some(value);
393 }
394 Rule::max_constraint => {
395 let value = inner
396 .into_inner()
397 .next()
398 .and_then(|p| p.as_str().parse::<u32>().ok())
399 .ok_or_else(|| {
400 TopologyParseError::InvalidConstraint(
401 "max constraint requires integer value".to_string(),
402 )
403 })?;
404 max = Some(value);
405 }
406 _ => {}
407 }
408 }
409
410 Ok(RoleFamilyConstraint {
411 min: min.unwrap_or(0),
412 max,
413 })
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn test_parse_local_mode_topology() {
422 let input = r#"
423 topology TestLocal for PingPong {
424 mode: local
425 }
426 "#;
427
428 let result = parse_topology(input).unwrap();
429 assert_eq!(result.name, "TestLocal");
430 assert_eq!(result.for_choreography, "PingPong");
431 assert_eq!(result.topology.mode, Some(TopologyMode::Local));
432 }
433
434 #[test]
435 fn test_parse_topology_with_mappings() {
436 let input = r#"
437 topology Dev for PingPong {
438 Alice: localhost:8080
439 Bob: localhost:8081
440 }
441 "#;
442
443 let result = parse_topology(input).unwrap();
444 assert_eq!(result.name, "Dev");
445 assert_eq!(
446 result
447 .topology
448 .get_location(&RoleName::from_static("Alice"))
449 .unwrap(),
450 Location::Remote(TopologyEndpoint::new("localhost:8080").unwrap())
451 );
452 assert_eq!(
453 result
454 .topology
455 .get_location(&RoleName::from_static("Bob"))
456 .unwrap(),
457 Location::Remote(TopologyEndpoint::new("localhost:8081").unwrap())
458 );
459 }
460
461 #[test]
462 fn test_parse_topology_with_constraints() {
463 let input = r#"
464 topology Prod for TwoPhaseCommit {
465 Coordinator: coordinator.internal:9000
466 ParticipantA: participant-a.internal:9000
467 ParticipantB: participant-b.internal:9000
468
469 constraints {
470 separated: Coordinator, ParticipantA
471 region: Coordinator -> us_east_1
472 }
473 }
474 "#;
475
476 let result = parse_topology(input).unwrap();
477 assert_eq!(result.name, "Prod");
478 assert_eq!(result.topology.constraints.len(), 2);
479 }
480
481 #[test]
482 fn test_parse_channel_capacities() {
483 let input = r#"
484 topology Capacity for Protocol {
485 Alice: local
486 Bob: local
487
488 channel_capacities {
489 Alice -> Bob: 4
490 }
491 }
492 "#;
493
494 let result = parse_topology(input).unwrap();
495 let key = (RoleName::from_static("Alice"), RoleName::from_static("Bob"));
496 let capacity = result.topology.channel_capacities.get(&key).copied();
497 assert_eq!(
498 capacity,
499 Some(ChannelCapacity::try_new(4).expect("test capacity in range"))
500 );
501 }
502
503 #[test]
504 fn test_parse_kubernetes_mode() {
505 let input = r#"
506 topology K8s for MyProtocol {
507 mode: kubernetes(myapp)
508 }
509 "#;
510
511 let result = parse_topology(input).unwrap();
512 assert_eq!(
513 result.topology.mode,
514 Some(TopologyMode::Kubernetes(Namespace::new("myapp").unwrap()))
515 );
516 }
517
518 #[test]
519 fn test_parse_colocated_location() {
520 let input = r#"
521 topology Mixed for Protocol {
522 Alice: local
523 Bob: colocated(Alice)
524 Carol: remote.host:8080
525 }
526 "#;
527
528 let result = parse_topology(input).unwrap();
529 assert_eq!(
530 result
531 .topology
532 .get_location(&RoleName::from_static("Alice"))
533 .unwrap(),
534 Location::Local
535 );
536 assert_eq!(
537 result
538 .topology
539 .get_location(&RoleName::from_static("Bob"))
540 .unwrap(),
541 Location::Colocated(RoleName::from_static("Alice"))
542 );
543 }
544
545 #[test]
546 fn test_parse_role_constraints_min_only() {
547 let input = r#"
548 topology ThresholdSig for Protocol {
549 Coordinator: localhost:8000
550
551 role_constraints {
552 Witness: min = 3
553 }
554 }
555 "#;
556
557 let result = parse_topology(input).unwrap();
558 let constraint = result.topology.role_constraints.get("Witness").unwrap();
559 assert_eq!(constraint.min, 3);
560 assert_eq!(constraint.max, None);
561 }
562
563 #[test]
564 fn test_parse_role_constraints_min_and_max() {
565 let input = r#"
566 topology ThresholdSig for Protocol {
567 role_constraints {
568 Witness: min = 3, max = 10
569 }
570 }
571 "#;
572
573 let result = parse_topology(input).unwrap();
574 let constraint = result.topology.role_constraints.get("Witness").unwrap();
575 assert_eq!(constraint.min, 3);
576 assert_eq!(constraint.max, Some(10));
577 }
578
579 #[test]
580 fn test_parse_role_constraints_max_first() {
581 let input = r#"
582 topology ThresholdSig for Protocol {
583 role_constraints {
584 Worker: max = 5, min = 1
585 }
586 }
587 "#;
588
589 let result = parse_topology(input).unwrap();
590 let constraint = result.topology.role_constraints.get("Worker").unwrap();
591 assert_eq!(constraint.min, 1);
592 assert_eq!(constraint.max, Some(5));
593 }
594
595 #[test]
596 fn test_parse_role_constraints_multiple_families() {
597 let input = r#"
598 topology ThresholdSig for Protocol {
599 role_constraints {
600 Witness: min = 3
601 Worker: min = 1, max = 10
602 Validator: max = 5
603 }
604 }
605 "#;
606
607 let result = parse_topology(input).unwrap();
608 assert_eq!(result.topology.role_constraints.len(), 3);
609
610 let witness = result.topology.role_constraints.get("Witness").unwrap();
611 assert_eq!(witness.min, 3);
612 assert_eq!(witness.max, None);
613
614 let worker = result.topology.role_constraints.get("Worker").unwrap();
615 assert_eq!(worker.min, 1);
616 assert_eq!(worker.max, Some(10));
617
618 let validator = result.topology.role_constraints.get("Validator").unwrap();
619 assert_eq!(validator.min, 0); assert_eq!(validator.max, Some(5));
621 }
622
623 #[test]
624 fn test_parse_role_constraints_with_mappings_and_constraints() {
625 let input = r#"
626 topology Prod for TwoPhaseCommit {
627 Coordinator: coordinator.internal:9000
628
629 role_constraints {
630 Participant: min = 2, max = 100
631 }
632
633 constraints {
634 region: Coordinator -> us_east_1
635 }
636 }
637 "#;
638
639 let result = parse_topology(input).unwrap();
640 assert_eq!(result.topology.role_constraints.len(), 1);
641 assert_eq!(result.topology.constraints.len(), 1);
642
643 let participant = result.topology.role_constraints.get("Participant").unwrap();
644 assert_eq!(participant.min, 2);
645 assert_eq!(participant.max, Some(100));
646 }
647}