1use std::collections::{BTreeMap, BTreeSet};
23use std::sync::Arc;
24
25use vertep_core::compile::{CompiledChart, ROOT, StateIdx};
26use vertep_core::datamodel::Value;
27use vertep_core::ir::{
28 Block, Chart, Exec, ExecKind, Extension, ExtensionValue, Send as SendElem, State, StateElement,
29 Transition, VT_NS,
30};
31use vertep_core::kernel::History;
32use vertep_core::session::Session;
33use vertep_rdf::ChartVersion;
34
35use crate::vocab;
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
40pub enum UnhandledPolicy {
41 Discard,
43 DeadLetter,
45 Error,
47}
48
49impl UnhandledPolicy {
50 pub fn as_str(self) -> &'static str {
52 match self {
53 UnhandledPolicy::Discard => "discard",
54 UnhandledPolicy::DeadLetter => "dead-letter",
55 UnhandledPolicy::Error => "error",
56 }
57 }
58
59 pub fn parse(s: &str) -> Option<Self> {
61 match s.trim() {
62 "discard" => Some(UnhandledPolicy::Discard),
63 "dead-letter" => Some(UnhandledPolicy::DeadLetter),
64 "error" => Some(UnhandledPolicy::Error),
65 _ => None,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum RestartMode {
73 Never,
75 OnFailure,
77 Always,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct RestartPolicy {
85 pub mode: RestartMode,
87 pub limit: u32,
89 pub window_ms: i64,
91 pub backoff_ms: i64,
93}
94
95impl Default for RestartPolicy {
96 fn default() -> Self {
97 RestartPolicy {
98 mode: RestartMode::Never,
99 limit: 3,
100 window_ms: 60_000,
101 backoff_ms: 100,
102 }
103 }
104}
105
106impl RestartPolicy {
107 pub fn backoff(&self, n: u32) -> i64 {
109 self.backoff_ms
110 .saturating_mul(1i64 << n.saturating_sub(1).min(30))
111 }
112}
113
114pub fn parse_duration(text: &str) -> Option<i64> {
117 let t = text.trim();
118 let units: [(&str, f64); 5] = [
119 ("ms", 1.0),
120 ("min", 60_000.0),
121 ("s", 1_000.0),
122 ("h", 3_600_000.0),
123 ("d", 86_400_000.0),
124 ];
125 for (suffix, factor) in units {
126 if let Some(n) = t.strip_suffix(suffix) {
127 let v: f64 = n.trim().parse().ok()?;
128 return (v >= 0.0).then(|| (v * factor).round() as i64);
129 }
130 }
131 t.parse::<i64>().ok().filter(|v| *v >= 0)
132}
133
134pub fn element_attributes(xml: &str) -> BTreeMap<String, String> {
136 let mut out = BTreeMap::new();
137 let Some(start) = xml.find('<') else {
138 return out;
139 };
140 let rest = &xml[start + 1..];
141 let end = rest.find('>').unwrap_or(rest.len());
142 let tag = rest[..end].trim_end_matches('/');
143 let mut chars = tag.char_indices().peekable();
144 while let Some((_, c)) = chars.peek() {
146 if c.is_whitespace() {
147 break;
148 }
149 chars.next();
150 }
151 let bytes = tag;
152 let mut i = chars.peek().map_or(bytes.len(), |(i, _)| *i);
153 while i < bytes.len() {
154 let rest = &bytes[i..];
155 let trimmed = rest.trim_start();
156 i += rest.len() - trimmed.len();
157 let Some(eq) = trimmed.find('=') else { break };
158 let name = trimmed[..eq].trim().to_owned();
159 let after = trimmed[eq + 1..].trim_start();
160 let Some(q) = after.chars().next().filter(|c| *c == '"' || *c == '\'') else {
161 break;
162 };
163 let body = &after[1..];
164 let Some(close) = body.find(q) else { break };
165 out.insert(
166 name.rsplit(':').next().unwrap_or(&name).to_owned(),
167 unescape(&body[..close]),
168 );
169 let consumed = (trimmed.len() - after.len()) + 1 + close + 1;
170 i += consumed;
171 }
172 out
173}
174
175fn unescape(s: &str) -> String {
176 s.replace("<", "<")
177 .replace(">", ">")
178 .replace(""", "\"")
179 .replace("'", "'")
180 .replace("&", "&")
181}
182
183fn vt_attr<'a>(exts: &'a [Extension], local: &str) -> Option<&'a str> {
184 exts.iter().find_map(|e| match &e.value {
185 ExtensionValue::Attribute(v) if e.ns == VT_NS && e.local == local => Some(v.as_str()),
186 _ => None,
187 })
188}
189
190fn vt_elements<'a>(
191 exts: &'a [Extension],
192 local: &'a str,
193) -> impl Iterator<Item = BTreeMap<String, String>> + 'a {
194 exts.iter().filter_map(move |e| match &e.value {
195 ExtensionValue::Element(xml) if e.ns == VT_NS && e.local == local => {
196 Some(element_attributes(xml))
197 }
198 _ => None,
199 })
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct LowerError(pub String);
205
206impl core::fmt::Display for LowerError {
207 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208 write!(f, "invalid vt: extension: {}", self.0)
209 }
210}
211
212pub fn request_type(timeout_ms: Option<i64>) -> String {
215 match timeout_ms {
216 Some(t) => format!("{};timeout={t}", vocab::REQUEST_SEND_TYPE),
217 None => vocab::REQUEST_SEND_TYPE.to_owned(),
218 }
219}
220
221pub fn split_type(type_: &str) -> (&str, BTreeMap<String, String>) {
223 let mut parts = type_.split(';');
224 let base = parts.next().unwrap_or_default();
225 let params = parts
226 .map(|p| {
227 let (k, v) = p.split_once('=').unwrap_or((p, ""));
228 (k.to_owned(), v.to_owned())
229 })
230 .collect();
231 (base, params)
232}
233
234pub fn lower(chart: &Chart) -> Result<(Chart, BTreeSet<String>), LowerError> {
243 let mut out = chart.clone();
244 let mut types = BTreeSet::new();
245 for s in &mut out.states {
246 lower_state(s, &mut types)?;
247 }
248 Ok((out, types))
249}
250
251fn lower_state(s: &mut State, types: &mut BTreeSet<String>) -> Result<(), LowerError> {
252 let requests: Vec<_> = vt_elements(&s.extensions, "request").collect();
253 for (k, r) in requests.iter().enumerate() {
254 let event = r
255 .get("event")
256 .ok_or_else(|| LowerError(format!("vt:request in state {} has no event", s.id)))?;
257 let timeout = match r.get("timeout") {
258 Some(t) => Some(parse_duration(t).ok_or_else(|| {
259 LowerError(format!("vt:request in state {}: bad timeout {t:?}", s.id))
260 })?),
261 None => None,
262 };
263 let ty = request_type(timeout);
264 types.insert(ty.clone());
265 let send = SendElem {
266 event: Some(event.clone()),
267 target: r.get("target").cloned(),
268 type_: Some(ty),
269 ..SendElem::default()
270 };
271 s.onentry
272 .push(vec![Exec::new(ExecKind::Send(Box::new(send)))]);
273 if k == 0 && s.element == StateElement::State && s.children.is_empty() {
274 s.children.push(State::new(
275 format!("{}.vt-waiting", s.id),
276 StateElement::State,
277 ));
278 }
279 if let Some(target) = r.get("reply") {
280 s.transitions.push(Transition {
281 events: vec![vocab::REPLY_EVENT.to_owned()],
282 targets: vec![target.clone()],
283 ..Transition::default()
284 });
285 }
286 if let Some(target) = r.get("ontimeout") {
287 s.transitions.push(Transition {
288 events: vec![vocab::TIMEOUT_EVENT.to_owned()],
289 targets: vec![target.clone()],
290 ..Transition::default()
291 });
292 }
293 }
294 for b in s.onentry.iter_mut().chain(s.onexit.iter_mut()) {
295 lower_block(b, types)?;
296 }
297 for t in &mut s.transitions {
298 lower_block(&mut t.body, types)?;
299 }
300 for c in &mut s.children {
301 lower_state(c, types)?;
302 }
303 Ok(())
304}
305
306fn lower_block(b: &mut Block, types: &mut BTreeSet<String>) -> Result<(), LowerError> {
307 for e in b.iter_mut() {
308 match &mut e.kind {
309 ExecKind::If { branches } => {
310 for br in branches {
311 lower_block(&mut br.body, types)?;
312 }
313 }
314 ExecKind::Foreach { body, .. } => lower_block(body, types)?,
315 ExecKind::Extension(x) if x.ns == VT_NS => {
316 let ExtensionValue::Element(xml) = &x.value else {
317 continue;
318 };
319 let a = element_attributes(xml);
320 let send = match x.local.as_str() {
321 "reminder" => {
322 let name = a
323 .get("name")
324 .ok_or_else(|| LowerError("vt:reminder has no name".into()))?;
325 let ty = if a.get("cancel").is_some_and(|c| c == "true") {
326 format!("{};name={name};cancel=true", vocab::REMINDER_SEND_TYPE)
327 } else {
328 let period = a
329 .get("period")
330 .and_then(|p| parse_duration(p))
331 .filter(|p| *p > 0)
332 .ok_or_else(|| {
333 LowerError(format!("vt:reminder {name}: bad period"))
334 })?;
335 format!("{};name={name};period={period}", vocab::REMINDER_SEND_TYPE)
336 };
337 SendElem {
338 event: Some(a.get("event").cloned().unwrap_or_else(|| name.clone())),
339 type_: Some(ty),
340 ..SendElem::default()
341 }
342 }
343 "migrate" => {
344 let to = a
345 .get("to")
346 .ok_or_else(|| LowerError("vt:migrate has no `to`".into()))?;
347 let m = a.get("migration").cloned().unwrap_or_default();
348 SendElem {
349 event: Some("vt:migrate".into()),
350 type_: Some(format!(
351 "{};to={to};migration={m}",
352 vocab::MIGRATE_SEND_TYPE
353 )),
354 ..SendElem::default()
355 }
356 }
357 _ => continue,
358 };
359 if let Some(t) = &send.type_ {
360 types.insert(t.clone());
361 }
362 e.kind = ExecKind::Send(Box::new(send));
363 }
364 _ => {}
365 }
366 }
367 Ok(())
368}
369
370pub struct ChartInfo {
373 pub version: ChartVersion,
375 pub chart_type: Option<String>,
377 pub ir: Chart,
379 pub compiled: CompiledChart,
381 pub vt_send_types: BTreeSet<String>,
383 pub defers: BTreeMap<StateIdx, Vec<String>>,
385 pub unhandled: Option<UnhandledPolicy>,
387}
388
389impl std::fmt::Debug for ChartInfo {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 f.debug_struct("ChartInfo")
392 .field("version", &self.version.as_str())
393 .field("chart_type", &self.chart_type)
394 .finish_non_exhaustive()
395 }
396}
397
398#[derive(Clone, Debug, PartialEq, Eq)]
400pub enum RegisterError {
401 BadType(String),
403 Lower(LowerError),
405 Invalid(Vec<String>),
407 InvokesVirtualActor {
409 state: String,
411 src: String,
413 },
414 Refused {
417 datamodel: String,
419 problems: Vec<String>,
421 },
422}
423
424impl core::fmt::Display for RegisterError {
425 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
426 match self {
427 RegisterError::BadType(t) => write!(f, "chart type {t:?} is not an absolute IRI"),
428 RegisterError::Lower(e) => write!(f, "{e}"),
429 RegisterError::Invalid(v) => write!(f, "chart is invalid: {}", v.join("; ")),
430 RegisterError::InvokesVirtualActor { state, src } => write!(
431 f,
432 "state {state} invokes the virtual actor {src}; virtual actors are not invokable"
433 ),
434 RegisterError::Refused {
435 datamodel,
436 problems,
437 } => write!(
438 f,
439 "the {datamodel} datamodel refuses the chart: {}",
440 problems.join("; ")
441 ),
442 }
443 }
444}
445
446impl std::error::Error for RegisterError {}
447
448impl ChartInfo {
449 pub fn new(chart: Chart, chart_type: Option<String>) -> Result<Self, RegisterError> {
455 let version = vertep_rdf::chart_version(&chart);
456 let (lowered, vt_send_types) = lower(&chart).map_err(RegisterError::Lower)?;
457 let compiled = CompiledChart::new(lowered)
458 .map_err(|v| RegisterError::Invalid(v.iter().map(|x| format!("{x:?}")).collect()))?;
459 let mut defers = BTreeMap::new();
460 collect_defers(&compiled.chart().states, &compiled, &mut defers);
461 let unhandled = vt_attr(&chart.extensions, "unhandled").and_then(UnhandledPolicy::parse);
462 Ok(ChartInfo {
463 version,
464 chart_type,
465 ir: chart,
466 compiled,
467 vt_send_types,
468 defers,
469 unhandled,
470 })
471 }
472
473 pub fn deferred(&self, session: &Session) -> Vec<String> {
476 let mut out = Vec::new();
477 for (idx, ds) in &self.defers {
478 if session.configuration.contains(*idx) {
479 out.extend(ds.iter().cloned());
480 }
481 }
482 out
483 }
484
485 pub fn restart_policy(&self, state: &str, index: usize) -> RestartPolicy {
487 let mut p = RestartPolicy::default();
488 let Some(inv) = find_state(&self.ir.states, state).and_then(|s| s.invokes.get(index))
489 else {
490 return p;
491 };
492 let x = &inv.extensions;
493 p.mode = match vt_attr(x, "restart") {
494 Some("on-failure") => RestartMode::OnFailure,
495 Some("always") => RestartMode::Always,
496 _ => RestartMode::Never,
497 };
498 if let Some(l) = vt_attr(x, "restart-limit").and_then(|v| v.trim().parse().ok()) {
499 p.limit = l;
500 }
501 if let Some(w) = vt_attr(x, "restart-window").and_then(parse_duration) {
502 p.window_ms = w;
503 }
504 if let Some(b) = vt_attr(x, "restart-backoff").and_then(parse_duration) {
505 p.backoff_ms = b;
506 }
507 p
508 }
509
510 pub fn active(&self, session: &Session) -> Vec<String> {
512 session.active(&self.compiled)
513 }
514}
515
516fn collect_defers(
517 states: &[State],
518 chart: &CompiledChart,
519 out: &mut BTreeMap<StateIdx, Vec<String>>,
520) {
521 for s in states {
522 if let Some(d) = vt_attr(&s.extensions, "defer")
523 && let Some(i) = chart.index_of(&s.id)
524 {
525 out.insert(i, d.split_whitespace().map(str::to_owned).collect());
526 }
527 collect_defers(&s.children, chart, out);
528 }
529}
530
531pub fn find_state<'a>(states: &'a [State], id: &str) -> Option<&'a State> {
533 for s in states {
534 if s.id == id {
535 return Some(s);
536 }
537 if let Some(f) = find_state(&s.children, id) {
538 return Some(f);
539 }
540 }
541 None
542}
543
544pub fn static_invoke_sources(states: &[State], out: &mut Vec<(String, String)>) {
546 for s in states {
547 for inv in &s.invokes {
548 if let Some(src) = &inv.src {
549 out.push((s.id.clone(), src.clone()));
550 }
551 }
552 static_invoke_sources(&s.children, out);
553 }
554}
555
556pub type Transform = Arc<dyn Fn(Value) -> Result<Value, String> + Send + Sync>;
558
559#[derive(Clone)]
565pub struct Migration {
566 pub iri: String,
568 pub from: ChartVersion,
570 pub to: ChartVersion,
572 pub configurations: Vec<(BTreeSet<String>, BTreeSet<String>)>,
574 pub states: BTreeMap<String, String>,
576 pub transform: Transform,
578}
579
580impl std::fmt::Debug for Migration {
581 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582 f.debug_struct("Migration")
583 .field("iri", &self.iri)
584 .field("from", &self.from.as_str())
585 .field("to", &self.to.as_str())
586 .finish_non_exhaustive()
587 }
588}
589
590impl Migration {
591 pub fn identity(iri: impl Into<String>, from: ChartVersion, to: ChartVersion) -> Self {
593 Migration {
594 iri: iri.into(),
595 from,
596 to,
597 configurations: Vec::new(),
598 states: BTreeMap::new(),
599 transform: Arc::new(Ok),
600 }
601 }
602
603 pub fn map_configuration<I: IntoIterator<Item = S>, S: Into<String>>(
605 mut self,
606 from: I,
607 to: I,
608 ) -> Self {
609 self.configurations.push((
610 from.into_iter().map(Into::into).collect(),
611 to.into_iter().map(Into::into).collect(),
612 ));
613 self
614 }
615
616 pub fn rename_state(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
618 self.states.insert(from.into(), to.into());
619 self
620 }
621
622 pub fn with_transform(
624 mut self,
625 f: impl Fn(Value) -> Result<Value, String> + Send + Sync + 'static,
626 ) -> Self {
627 self.transform = Arc::new(f);
628 self
629 }
630
631 pub fn map(&self, old: &BTreeSet<String>, to: &ChartInfo) -> Option<BTreeSet<String>> {
633 let core: BTreeSet<String> = old.iter().filter(|s| !is_generated(s)).cloned().collect();
634 if let Some((_, n)) = self.configurations.iter().find(|(o, _)| *o == core) {
635 return Some(n.clone());
636 }
637 let mut out = BTreeSet::new();
638 for s in &core {
639 let n = self.states.get(s).unwrap_or(s);
640 to.compiled.index_of(n)?;
641 out.insert(n.clone());
642 }
643 Some(out)
644 }
645}
646
647fn is_generated(id: &str) -> bool {
648 id.ends_with(".vt-waiting")
649}
650
651pub fn reachable_configurations(
659 chart: &Chart,
660 limit: usize,
661) -> Result<Vec<BTreeSet<String>>, usize> {
662 let mut all = Vec::new();
663 for s in &chart.states {
664 if matches!(s.element, StateElement::History(_)) {
665 continue;
666 }
667 all.extend(configs(s, limit)?);
668 if all.len() > limit {
669 return Err(all.len());
670 }
671 }
672 Ok(all)
673}
674
675fn configs(s: &State, limit: usize) -> Result<Vec<BTreeSet<String>>, usize> {
676 let kids: Vec<&State> = s
677 .children
678 .iter()
679 .filter(|c| !matches!(c.element, StateElement::History(_)))
680 .collect();
681 let mut out: Vec<BTreeSet<String>> = Vec::new();
682 if kids.is_empty() {
683 out.push(BTreeSet::from([s.id.clone()]));
684 } else if s.element == StateElement::Parallel {
685 out.push(BTreeSet::from([s.id.clone()]));
686 for k in kids {
687 let sub = configs(k, limit)?;
688 let mut next = Vec::with_capacity(out.len() * sub.len());
689 for a in &out {
690 for b in &sub {
691 let mut c = a.clone();
692 c.extend(b.iter().cloned());
693 next.push(c);
694 }
695 if next.len() > limit {
696 return Err(next.len());
697 }
698 }
699 out = next;
700 }
701 } else {
702 for k in kids {
703 for mut c in configs(k, limit)? {
704 c.insert(s.id.clone());
705 out.push(c);
706 }
707 if out.len() > limit {
708 return Err(out.len());
709 }
710 }
711 }
712 Ok(out)
713}
714
715pub fn unmapped_configurations(
718 m: &Migration,
719 from: &ChartInfo,
720 to: &ChartInfo,
721) -> Result<Vec<BTreeSet<String>>, usize> {
722 Ok(reachable_configurations(&from.ir, 100_000)?
723 .into_iter()
724 .filter(|c| m.map(c, to).is_none())
725 .collect())
726}
727
728pub fn migrated_session(old: &Session, ids: &BTreeSet<String>, to: &ChartInfo) -> Session {
731 let c = &to.compiled;
732 let mut config = c.empty_set();
733 for id in ids {
734 if let Some(i) = c.index_of(id) {
735 config.insert(i);
736 let mut p = c.state(i).parent;
737 while let Some(a) = p {
738 if a == ROOT {
739 break;
740 }
741 config.insert(a);
742 p = c.state(a).parent;
743 }
744 }
745 }
746 complete(c, ROOT, &mut config);
747 let mut data = config.clone();
748 data.insert(ROOT);
749 let mut options = old.options.clone();
750 for t in &to.vt_send_types {
751 if !options.send_types.contains(t) {
752 options.send_types.push(t.clone());
753 }
754 }
755 Session {
756 sessionid: old.sessionid.clone(),
757 status: old.status,
758 configuration: config,
759 history: History::new(c.len()),
760 internal_queue: Default::default(),
761 invocations: Vec::new(),
762 data_initialized: data,
763 send_counter: old.send_counter,
764 invoke_counter: old.invoke_counter,
765 macrosteps: old.macrosteps,
766 options,
767 }
768}
769
770fn complete(c: &CompiledChart, s: StateIdx, config: &mut vertep_core::bitset::StateSet) {
771 let st = c.state(s);
772 if st.children.is_empty() {
773 return;
774 }
775 let parallel = st.kind == vertep_core::ir::StateKind::Parallel;
776 let active: Vec<StateIdx> = st
777 .children
778 .iter()
779 .copied()
780 .filter(|k| config.contains(*k))
781 .collect();
782 if parallel {
783 for k in st.children.clone() {
784 config.insert(k);
785 complete(c, k, config);
786 }
787 } else if active.is_empty() {
788 if let Some(&k) = c.default_entry(s).first().or(st.children.first()) {
789 config.insert(k);
790 let mut p = c.state(k).parent;
791 while let Some(a) = p {
792 if a == s {
793 break;
794 }
795 config.insert(a);
796 p = c.state(a).parent;
797 }
798 complete(c, k, config);
799 }
800 } else {
801 for k in active {
802 complete(c, k, config);
803 }
804 }
805}
806
807#[derive(Default)]
811pub struct ChartRegistry {
812 versions: BTreeMap<String, Arc<ChartInfo>>,
813 types: BTreeMap<String, Vec<String>>,
814 migrations: BTreeMap<String, Migration>,
815 bindings: BTreeMap<String, (String, String)>,
817}
818
819impl std::fmt::Debug for ChartRegistry {
820 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
821 f.debug_struct("ChartRegistry")
822 .field("versions", &self.versions.keys().collect::<Vec<_>>())
823 .field("types", &self.types)
824 .finish_non_exhaustive()
825 }
826}
827
828impl ChartRegistry {
829 pub fn insert(&mut self, info: ChartInfo) -> Arc<ChartInfo> {
831 let key = info.version.as_str().to_owned();
832 let ty = info.chart_type.clone();
833 let info = self
834 .versions
835 .entry(key.clone())
836 .or_insert_with(|| Arc::new(info))
837 .clone();
838 if let Some(t) = ty {
839 let list = self.types.entry(t).or_default();
840 list.retain(|v| *v != key);
841 list.push(key);
842 }
843 info
844 }
845
846 pub fn get(&self, iri: &str) -> Option<Arc<ChartInfo>> {
848 self.versions.get(iri).cloned()
849 }
850
851 pub fn latest(&self, ty: &str) -> Option<Arc<ChartInfo>> {
853 self.types
854 .get(ty)
855 .and_then(|l| l.last())
856 .and_then(|v| self.get(v))
857 }
858
859 pub fn versions_of(&self, ty: &str) -> Vec<String> {
861 self.types.get(ty).cloned().unwrap_or_default()
862 }
863
864 pub fn types(&self) -> impl Iterator<Item = &String> {
866 self.types.keys()
867 }
868
869 pub fn type_of(&self, address: &str) -> Option<(String, String)> {
872 if let Some(b) = self.bindings.get(address) {
873 return Some(b.clone());
874 }
875 self.types
876 .keys()
877 .filter_map(|t| {
878 let rest = address.strip_prefix(t.as_str())?.strip_prefix('/')?;
879 Some((t, rest))
880 })
881 .max_by_key(|(t, _)| t.len())
882 .and_then(|(t, k)| Some((t.clone(), crate::address::percent_decode(k)?)))
883 }
884
885 pub fn bind(&mut self, address: &str, chart_type: &str, key: &str) {
888 self.bindings
889 .insert(address.to_owned(), (chart_type.to_owned(), key.to_owned()));
890 }
891
892 pub fn remove(&mut self, iri: &str) -> bool {
894 for l in self.types.values_mut() {
895 l.retain(|v| v != iri);
896 }
897 self.versions.remove(iri).is_some()
898 }
899
900 pub fn add_migration(&mut self, m: Migration) {
902 self.migrations.insert(m.iri.clone(), m);
903 }
904
905 pub fn migration(&self, iri: &str) -> Option<Migration> {
907 self.migrations.get(iri).cloned()
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914
915 #[test]
916 fn durations() {
917 assert_eq!(parse_duration("100ms"), Some(100));
918 assert_eq!(parse_duration("5s"), Some(5_000));
919 assert_eq!(parse_duration("10min"), Some(600_000));
920 assert_eq!(parse_duration("1h"), Some(3_600_000));
921 assert_eq!(parse_duration("250"), Some(250));
922 assert_eq!(parse_duration("x"), None);
923 }
924
925 #[test]
926 fn attributes_of_an_extension_element() {
927 let a = element_attributes(
928 r#"<vt:request xmlns:vt="https://w3id.org/vertep/vt#" target="urn:b" event='price.get' timeout="5s"/>"#,
929 );
930 assert_eq!(a.get("target").map(String::as_str), Some("urn:b"));
931 assert_eq!(a.get("event").map(String::as_str), Some("price.get"));
932 assert_eq!(a.get("timeout").map(String::as_str), Some("5s"));
933 assert_eq!(
934 split_type("x;a=1;b=2").1.get("b").map(String::as_str),
935 Some("2")
936 );
937 }
938
939 #[test]
940 fn backoff_doubles() {
941 let p = RestartPolicy {
942 backoff_ms: 100,
943 ..RestartPolicy::default()
944 };
945 assert_eq!((p.backoff(1), p.backoff(2), p.backoff(3)), (100, 200, 400));
946 }
947}