1#[derive(Debug, Clone, Copy)]
5pub enum Field {
6 Expr(&'static str),
7 ExprOpt(&'static str),
8 ExprList(&'static str),
9 Num(&'static str),
10 NumOpt(&'static str),
11 BoolOpt(&'static str),
12 Str(&'static str),
13 StrOpt(&'static str),
14 StrListOpt(&'static str),
15}
16
17pub fn field_name(f: &Field) -> &'static str {
18 match f {
19 Field::Expr(n)
20 | Field::ExprOpt(n)
21 | Field::ExprList(n)
22 | Field::Num(n)
23 | Field::NumOpt(n)
24 | Field::BoolOpt(n)
25 | Field::Str(n)
26 | Field::StrOpt(n)
27 | Field::StrListOpt(n) => n,
28 }
29}
30
31pub fn field_kind(f: &Field) -> &'static str {
34 match f {
35 Field::Expr(_) | Field::ExprOpt(_) => "expr",
36 Field::ExprList(_) => "list",
37 Field::Num(_) | Field::NumOpt(_) => "number",
38 Field::BoolOpt(_) => "bool",
39 Field::Str(_) | Field::StrOpt(_) => "string",
40 Field::StrListOpt(_) => "list-string",
41 }
42}
43
44pub fn field_required(f: &Field) -> bool {
47 matches!(
48 f,
49 Field::Expr(_) | Field::Num(_) | Field::Str(_) | Field::ExprList(_)
50 )
51}
52
53pub struct OpSig {
54 pub tag: &'static str,
55 pub fields: &'static [Field],
56}
57
58struct Row {
66 names: &'static [&'static str],
67 sig: OpSig,
68 desc: &'static str,
69}
70
71use Field::*;
72
73static ROWS: &[Row] = &[
74 Row {
75 names: &["sma", "average"],
76 sig: OpSig {
77 tag: "Average",
78 fields: &[Expr("of"), Num("n")],
79 },
80 desc: "Simple moving average of `of` over `n` days.",
81 },
82 Row {
83 names: &["ema"],
84 sig: OpSig {
85 tag: "Ema",
86 fields: &[Expr("of"), Num("n")],
87 },
88 desc: "Exponential moving average of `of` over `n` days.",
89 },
90 Row {
91 names: &["std"],
92 sig: OpSig {
93 tag: "Std",
94 fields: &[Expr("of"), Num("n")],
95 },
96 desc: "Rolling standard deviation of `of` over `n` days.",
97 },
98 Row {
99 names: &["rsi"],
100 sig: OpSig {
101 tag: "Rsi",
102 fields: &[Expr("of"), Num("n")],
103 },
104 desc: "Relative Strength Index of `of` over `n` days.",
105 },
106 Row {
107 names: &["pct_change"],
108 sig: OpSig {
109 tag: "PctChange",
110 fields: &[Expr("of"), Num("n")],
111 },
112 desc: "Percentage change of `of` over `n` days.",
113 },
114 Row {
115 names: &["rise"],
116 sig: OpSig {
117 tag: "Rise",
118 fields: &[Expr("of"), Num("n")],
119 },
120 desc: "1 where `of` rose for `n` consecutive days, else 0.",
121 },
122 Row {
123 names: &["fall"],
124 sig: OpSig {
125 tag: "Fall",
126 fields: &[Expr("of"), Num("n")],
127 },
128 desc: "1 where `of` fell for `n` consecutive days, else 0.",
129 },
130 Row {
131 names: &["shift"],
132 sig: OpSig {
133 tag: "Shift",
134 fields: &[Expr("of"), Num("n")],
135 },
136 desc: "`of` lagged forward by `n` days.",
137 },
138 Row {
139 names: &["rolling_max"],
140 sig: OpSig {
141 tag: "RollingMax",
142 fields: &[Expr("of"), Num("n")],
143 },
144 desc: "Rolling maximum of `of` over `n` days.",
145 },
146 Row {
147 names: &["rolling_min"],
148 sig: OpSig {
149 tag: "RollingMin",
150 fields: &[Expr("of"), Num("n")],
151 },
152 desc: "Rolling minimum of `of` over `n` days.",
153 },
154 Row {
155 names: &["bollinger_mid"],
156 sig: OpSig {
157 tag: "BollingerMid",
158 fields: &[Expr("of"), Num("n")],
159 },
160 desc: "Bollinger mid band: the `n`-day simple moving average of `of`.",
161 },
162 Row {
163 names: &["bollinger_upper"],
164 sig: OpSig {
165 tag: "BollingerUpper",
166 fields: &[Expr("of"), Num("n"), NumOpt("k")],
167 },
168 desc: "Bollinger upper band: sma(of, n) + k * std(of, n) (k defaults to 2).",
169 },
170 Row {
171 names: &["bollinger_lower"],
172 sig: OpSig {
173 tag: "BollingerLower",
174 fields: &[Expr("of"), Num("n"), NumOpt("k")],
175 },
176 desc: "Bollinger lower band: sma(of, n) - k * std(of, n) (k defaults to 2).",
177 },
178 Row {
179 names: &["macd"],
180 sig: OpSig {
181 tag: "Macd",
182 fields: &[Expr("of"), NumOpt("fast"), NumOpt("slow")],
183 },
184 desc: "MACD line: ema(of, fast) - ema(of, slow) (fast/slow default 12/26).",
185 },
186 Row {
187 names: &["macd_signal"],
188 sig: OpSig {
189 tag: "MacdSignal",
190 fields: &[Expr("of"), NumOpt("fast"), NumOpt("slow"), NumOpt("signal")],
191 },
192 desc: "MACD signal line: `signal`-day EMA of the MACD line (defaults 12/26/9).",
193 },
194 Row {
195 names: &["macd_hist"],
196 sig: OpSig {
197 tag: "MacdHist",
198 fields: &[Expr("of"), NumOpt("fast"), NumOpt("slow"), NumOpt("signal")],
199 },
200 desc: "MACD histogram: MACD line minus its signal line (defaults 12/26/9).",
201 },
202 Row {
203 names: &["donchian_high"],
204 sig: OpSig {
205 tag: "DonchianHigh",
206 fields: &[Expr("of"), Num("n")],
207 },
208 desc: "Donchian channel upper band: rolling `n`-day high of `of`.",
209 },
210 Row {
211 names: &["donchian_low"],
212 sig: OpSig {
213 tag: "DonchianLow",
214 fields: &[Expr("of"), Num("n")],
215 },
216 desc: "Donchian channel lower band: rolling `n`-day low of `of`.",
217 },
218 Row {
219 names: &["donchian_mid"],
220 sig: OpSig {
221 tag: "DonchianMid",
222 fields: &[Expr("of"), Num("n")],
223 },
224 desc: "Donchian channel mid-line: (rolling_max + rolling_min) / 2 over `n` days.",
225 },
226 Row {
227 names: &["atr"],
228 sig: OpSig {
229 tag: "Atr",
230 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
231 },
232 desc: "Average True Range over `n` days from high/low/close.",
233 },
234 Row {
235 names: &["natr"],
236 sig: OpSig {
237 tag: "Natr",
238 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
239 },
240 desc: "Normalized ATR (percent) over `n` days.",
241 },
242 Row {
243 names: &["willr"],
244 sig: OpSig {
245 tag: "WillR",
246 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
247 },
248 desc: "Williams %R over `n` days.",
249 },
250 Row {
251 names: &["cci"],
252 sig: OpSig {
253 tag: "Cci",
254 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
255 },
256 desc: "Commodity Channel Index over `n` days.",
257 },
258 Row {
259 names: &["stoch_k"],
260 sig: OpSig {
261 tag: "StochK",
262 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
263 },
264 desc: "Stochastic %K over `n` days.",
265 },
266 Row {
267 names: &["stoch_d"],
268 sig: OpSig {
269 tag: "StochD",
270 fields: &[
271 Expr("high"),
272 Expr("low"),
273 Expr("close"),
274 Num("n"),
275 NumOpt("d"),
276 ],
277 },
278 desc: "Stochastic %D: `d`-day average of %K over `n` days (d defaults to 3).",
279 },
280 Row {
281 names: &["aroon_up"],
282 sig: OpSig {
283 tag: "AroonUp",
284 fields: &[Expr("high"), Num("n")],
285 },
286 desc: "Aroon Up over `n` days from high.",
287 },
288 Row {
289 names: &["aroon_down"],
290 sig: OpSig {
291 tag: "AroonDown",
292 fields: &[Expr("low"), Num("n")],
293 },
294 desc: "Aroon Down over `n` days from low.",
295 },
296 Row {
297 names: &["adx"],
298 sig: OpSig {
299 tag: "Adx",
300 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
301 },
302 desc: "Average Directional Index over `n` days.",
303 },
304 Row {
305 names: &["plus_di"],
306 sig: OpSig {
307 tag: "PlusDi",
308 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
309 },
310 desc: "Plus Directional Indicator (+DI) over `n` days.",
311 },
312 Row {
313 names: &["minus_di"],
314 sig: OpSig {
315 tag: "MinusDi",
316 fields: &[Expr("high"), Expr("low"), Expr("close"), Num("n")],
317 },
318 desc: "Minus Directional Indicator (-DI) over `n` days.",
319 },
320 Row {
321 names: &["obv"],
322 sig: OpSig {
323 tag: "Obv",
324 fields: &[Expr("close"), Expr("volume")],
325 },
326 desc: "On-Balance Volume from close and volume.",
327 },
328 Row {
329 names: &["mfi"],
330 sig: OpSig {
331 tag: "Mfi",
332 fields: &[
333 Expr("high"),
334 Expr("low"),
335 Expr("close"),
336 Expr("volume"),
337 Num("n"),
338 ],
339 },
340 desc: "Money Flow Index over `n` days.",
341 },
342 Row {
343 names: &["vwap"],
344 sig: OpSig {
345 tag: "Vwap",
346 fields: &[
347 Expr("high"),
348 Expr("low"),
349 Expr("close"),
350 Expr("volume"),
351 Num("n"),
352 ],
353 },
354 desc: "Volume-Weighted Average Price over `n` days from high/low/close/volume.",
355 },
356 Row {
357 names: &["is_largest"],
358 sig: OpSig {
359 tag: "IsLargest",
360 fields: &[Expr("of"), Num("n")],
361 },
362 desc: "1 for the `n` highest values per row (cross-section), else 0.",
363 },
364 Row {
365 names: &["is_smallest"],
366 sig: OpSig {
367 tag: "IsSmallest",
368 fields: &[Expr("of"), Num("n")],
369 },
370 desc: "1 for the `n` lowest values per row (cross-section), else 0.",
371 },
372 Row {
373 names: &["sustain"],
374 sig: OpSig {
375 tag: "Sustain",
376 fields: &[Expr("of"), Num("nwindow"), NumOpt("nsatisfy")],
377 },
378 desc: "1 where `of` held true at least `nsatisfy` times within the last `nwindow` rows.",
379 },
380 Row {
381 names: &["is_entry"],
382 sig: OpSig {
383 tag: "IsEntry",
384 fields: &[Expr("of")],
385 },
386 desc: "1 on the row where `of` turns false->true (rising edge).",
387 },
388 Row {
389 names: &["is_exit"],
390 sig: OpSig {
391 tag: "IsExit",
392 fields: &[Expr("of")],
393 },
394 desc: "1 on the row where `of` turns true->false (falling edge).",
395 },
396 Row {
397 names: &["exit_when"],
398 sig: OpSig {
399 tag: "ExitWhen",
400 fields: &[Expr("entry"), Expr("exit")],
401 },
402 desc: "Hold true from an entry edge of `entry` until an exit edge (or `exit` is true).",
403 },
404 Row {
405 names: &["quantile_row"],
406 sig: OpSig {
407 tag: "QuantileRow",
408 fields: &[Expr("of"), Num("c")],
409 },
410 desc: "Per-row quantile of `of` across symbols at level `c` (e.g. 0.5 = median); one-column result.",
411 },
412 Row {
413 names: &["winsorize"],
414 sig: OpSig {
415 tag: "Winsorize",
416 fields: &[Expr("of"), Num("lower"), Num("upper")],
417 },
418 desc: "Per-row winsorize: clip values to empirical quantiles `lower`/`upper` (in 0..1).",
419 },
420 Row {
421 names: &["zscore"],
422 sig: OpSig {
423 tag: "Zscore",
424 fields: &[Expr("of")],
425 },
426 desc: "Per-row z-score (population std); NaN preserved; constant rows become 0.",
427 },
428 Row {
429 names: &["bucket"],
430 sig: OpSig {
431 tag: "Bucket",
432 fields: &[Expr("of"), Num("n")],
433 },
434 desc: "Per-row quantile buckets labeled 1..=n (ties share average rank).",
435 },
436 Row {
437 names: &["demean"],
438 sig: OpSig {
439 tag: "Demean",
440 fields: &[Expr("of")],
441 },
442 desc: "Per-row demean: subtract the cross-sectional mean of non-NaN cells.",
443 },
444 Row {
445 names: &["ceil"],
446 sig: OpSig {
447 tag: "Ceil",
448 fields: &[Expr("of")],
449 },
450 desc: "Ceiling of `of`.",
451 },
452 Row {
453 names: &["rank"],
454 sig: OpSig {
455 tag: "Rank",
456 fields: &[Expr("of"), BoolOpt("pct"), BoolOpt("ascending")],
457 },
458 desc: "Cross-sectional rank of `of` per row; `pct` for 0..1 percentile (default true), `ascending` sets direction (default true).",
459 },
460 Row {
461 names: &["mask"],
462 sig: OpSig {
463 tag: "Mask",
464 fields: &[Expr("of"), Expr("by")],
465 },
466 desc: "`of` kept only where `by` is true; elsewhere dropped.",
467 },
468 Row {
469 names: &["normalize_row"],
470 sig: OpSig {
471 tag: "NormalizeRow",
472 fields: &[Expr("of")],
473 },
474 desc: "Scale each row so gross weight (sum of |w|) is 1 — turns a raw signal into explicit portfolio weights. NaN preserved; zero rows unchanged.",
475 },
476 Row {
477 names: &["hold_until"],
478 sig: OpSig {
479 tag: "HoldUntil",
480 fields: &[
481 Expr("entry"),
482 Expr("exit"),
483 NumOpt("nstocks_limit"),
484 ExprOpt("rank"),
485 NumOpt("stop_loss"),
486 NumOpt("take_profit"),
487 NumOpt("trail_stop"),
488 NumOpt("trail_stop_activation"),
489 ],
490 },
491 desc: "Stateful rotation: enter on `entry`, exit on `exit`, hold up to `nstocks_limit` (prioritised by `rank`), with optional stop_loss/take_profit/trail_stop/trail_stop_activation.",
492 },
493 Row {
494 names: &["rebalance"],
495 sig: OpSig {
496 tag: "Rebalance",
497 fields: &[Expr("of"), StrOpt("freq"), ExprOpt("on")],
498 },
499 desc: "Hold `of`, refreshing on calendar `freq` (W/ME/QE) or on dates where `on` is true.",
500 },
501 Row {
502 names: &["neutralize"],
503 sig: OpSig {
504 tag: "Neutralize",
505 fields: &[Expr("of"), ExprList("by"), BoolOpt("add_const")],
506 },
507 desc: "Cross-sectionally regress `of` against the `by` factors, optionally adding a constant (default true).",
508 },
509 Row {
510 names: &["neutralize_industry"],
511 sig: OpSig {
512 tag: "NeutralizeIndustry",
513 fields: &[Expr("of"), BoolOpt("add_const")],
514 },
515 desc: "Neutralize `of` within each industry/sector (add_const defaults to true).",
516 },
517 Row {
518 names: &["industry_rank"],
519 sig: OpSig {
520 tag: "IndustryRank",
521 fields: &[Expr("of"), StrListOpt("categories")],
522 },
523 desc: "Rank `of` within each industry, optionally limited to `categories`.",
524 },
525 Row {
526 names: &["groupby_category"],
527 sig: OpSig {
528 tag: "GroupbyCategory",
529 fields: &[Expr("of"), Str("agg")],
530 },
531 desc: "Aggregate `of` within each industry using `agg` (e.g. mean).",
532 },
533 Row {
534 names: &["in_sector"],
535 sig: OpSig {
536 tag: "InSector",
537 fields: &[Expr("of"), Str("name")],
538 },
539 desc: "Boolean mask (1/0) where the symbol's industry equals `name` (exact, case-sensitive); shape follows `of`.",
540 },
541];
542
543pub fn field_default(tag: &str, field_name: &str) -> Option<serde_json::Value> {
555 use serde_json::json;
556 match (tag, field_name) {
557 ("StochD", "d") => Some(json!(3)),
558 ("BollingerUpper", "k") | ("BollingerLower", "k") => Some(json!(2.0)),
559 ("Macd", "fast") | ("MacdSignal", "fast") | ("MacdHist", "fast") => Some(json!(12)),
560 ("Macd", "slow") | ("MacdSignal", "slow") | ("MacdHist", "slow") => Some(json!(26)),
561 ("MacdSignal", "signal") | ("MacdHist", "signal") => Some(json!(9)),
562 ("Rank", "pct") => Some(json!(true)),
563 ("Rank", "ascending") => Some(json!(true)),
564 ("Neutralize", "add_const") => Some(json!(true)),
565 ("NeutralizeIndustry", "add_const") => Some(json!(true)),
566 _ => None,
567 }
568}
569
570pub struct FieldInfo {
572 pub name: &'static str,
573 pub kind: &'static str,
575 pub required: bool,
576 pub default: Option<serde_json::Value>,
577}
578
579pub struct OpInfo {
582 pub name: &'static str,
583 pub aliases: &'static [&'static str],
584 pub tag: &'static str,
585 pub description: &'static str,
586 pub fields: Vec<FieldInfo>,
587}
588
589pub fn function_ops() -> Vec<OpInfo> {
591 ROWS.iter()
592 .map(|r| OpInfo {
593 name: r.names[0],
594 aliases: &r.names[1..],
595 tag: r.sig.tag,
596 description: r.desc,
597 fields: r
598 .sig
599 .fields
600 .iter()
601 .map(|f| {
602 let name = field_name(f);
603 FieldInfo {
604 name,
605 kind: field_kind(f),
606 required: field_required(f),
607 default: field_default(r.sig.tag, name),
608 }
609 })
610 .collect(),
611 })
612 .collect()
613}
614
615pub fn binary_operators() -> &'static [(&'static str, &'static str)] {
617 BINOPS
618}
619
620pub fn op_by_name(name: &str) -> Option<&'static OpSig> {
621 ROWS.iter()
622 .find(|r| r.names.contains(&name))
623 .map(|r| &r.sig)
624}
625
626pub fn sig_by_tag(tag: &str) -> Option<&'static OpSig> {
627 ROWS.iter().find(|r| r.sig.tag == tag).map(|r| &r.sig)
628}
629
630pub fn dsl_name_for_tag(tag: &str) -> &'static str {
632 ROWS.iter()
633 .find(|r| r.sig.tag == tag)
634 .map(|r| r.names[0])
635 .unwrap_or_else(|| {
636 ALL_OP_TAGS
640 .iter()
641 .copied()
642 .find(|t| *t == tag)
643 .unwrap_or("")
644 })
645}
646
647static BINOPS: &[(&str, &str)] = &[
648 (">", "Gt"),
649 ("<", "Lt"),
650 (">=", "Ge"),
651 ("<=", "Le"),
652 ("and", "And"),
653 ("or", "Or"),
654 ("+", "Add"),
655 ("-", "Sub"),
656 ("*", "Mul"),
657 ("/", "Div"),
658];
659
660pub fn binop_tag(op: &str) -> Option<&'static str> {
661 BINOPS.iter().find(|(s, _)| *s == op).map(|(_, t)| *t)
662}
663
664pub fn binop_symbol_for_tag(tag: &str) -> Option<&'static str> {
665 BINOPS.iter().find(|(_, t)| *t == tag).map(|(s, _)| *s)
666}
667
668pub fn prefix_tag(op: &str) -> Option<&'static str> {
669 if op == "-" {
670 Some("Neg")
671 } else {
672 None
673 }
674}
675
676pub static ALL_OP_TAGS: &[&str] = &[
678 "Data",
679 "Const",
680 "Average",
681 "Ema",
682 "Std",
683 "Rsi",
684 "PctChange",
685 "Rise",
686 "Shift",
687 "RollingMax",
688 "RollingMin",
689 "BollingerMid",
690 "BollingerUpper",
691 "BollingerLower",
692 "Macd",
693 "MacdSignal",
694 "MacdHist",
695 "DonchianHigh",
696 "DonchianLow",
697 "DonchianMid",
698 "Atr",
699 "Natr",
700 "WillR",
701 "Cci",
702 "StochK",
703 "StochD",
704 "AroonUp",
705 "AroonDown",
706 "Adx",
707 "PlusDi",
708 "MinusDi",
709 "Obv",
710 "Mfi",
711 "Vwap",
712 "Fall",
713 "IsLargest",
714 "IsSmallest",
715 "Sustain",
716 "IsEntry",
717 "IsExit",
718 "ExitWhen",
719 "QuantileRow",
720 "Winsorize",
721 "Zscore",
722 "Bucket",
723 "Demean",
724 "Gt",
725 "Lt",
726 "Ge",
727 "Le",
728 "And",
729 "Or",
730 "Not",
731 "Add",
732 "Sub",
733 "Mul",
734 "Div",
735 "Neg",
736 "Ceil",
737 "Rank",
738 "Mask",
739 "NormalizeRow",
740 "HoldUntil",
741 "Rebalance",
742 "Neutralize",
743 "NeutralizeIndustry",
744 "IndustryRank",
745 "GroupbyCategory",
746 "InSector",
747];
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn looks_up_alias_and_snake_case() {
755 assert_eq!(op_by_name("sma").unwrap().tag, "Average");
756 assert_eq!(op_by_name("average").unwrap().tag, "Average");
757 assert_eq!(op_by_name("rolling_max").unwrap().tag, "RollingMax");
758 assert!(op_by_name("not_an_op").is_none());
759 }
760
761 #[test]
762 fn maps_operators() {
763 assert_eq!(binop_tag(">"), Some("Gt"));
764 assert_eq!(binop_tag("and"), Some("And"));
765 assert_eq!(binop_tag("+"), Some("Add"));
766 assert_eq!(prefix_tag("-"), Some("Neg"));
767 }
768
769 #[test]
770 fn round_trip_names_for_printer() {
771 assert_eq!(dsl_name_for_tag("Average"), "sma");
772 assert_eq!(binop_symbol_for_tag("Gt"), Some(">"));
773 assert_eq!(binop_symbol_for_tag("Average"), None);
774 }
775
776 #[test]
777 fn function_ops_expose_every_row_with_schema_metadata() {
778 let ops = function_ops();
779 assert_eq!(ops.len(), ROWS.len());
781
782 let sma = ops.iter().find(|o| o.tag == "Average").unwrap();
784 assert_eq!(sma.name, "sma");
785 assert!(sma.aliases.contains(&"average"));
786 assert!(!sma.description.is_empty());
787
788 let mut kinds = std::collections::BTreeSet::new();
791 for op in &ops {
792 for f in &op.fields {
793 assert!(!f.name.is_empty());
794 assert!(matches!(
795 f.kind,
796 "expr" | "number" | "string" | "bool" | "list" | "list-string"
797 ));
798 kinds.insert(f.kind);
799 }
800 }
801 assert!(kinds.contains("expr"));
803 assert!(kinds.contains("number"));
804 }
805
806 #[test]
807 fn field_defaults_match_serde() {
808 assert_eq!(field_default("StochD", "d"), Some(serde_json::json!(3)));
810 assert_eq!(field_default("Rank", "pct"), Some(serde_json::json!(true)));
811 assert_eq!(
812 field_default("Neutralize", "add_const"),
813 Some(serde_json::json!(true))
814 );
815 assert_eq!(field_default("Average", "n"), None);
817 assert_eq!(field_default("Nope", "nope"), None);
818
819 let stoch_d = function_ops()
821 .into_iter()
822 .find(|o| o.tag == "StochD")
823 .unwrap();
824 let d_field = stoch_d.fields.iter().find(|f| f.name == "d").unwrap();
825 assert_eq!(d_field.default, Some(serde_json::json!(3)));
826 }
827
828 #[test]
829 fn field_kind_and_required_cover_every_variant() {
830 use Field::*;
831 for (f, kind, required) in [
832 (Expr("x"), "expr", true),
833 (ExprOpt("x"), "expr", false),
834 (ExprList("x"), "list", true),
835 (Num("x"), "number", true),
836 (NumOpt("x"), "number", false),
837 (BoolOpt("x"), "bool", false),
838 (Str("x"), "string", true),
839 (StrOpt("x"), "string", false),
840 (StrListOpt("x"), "list-string", false),
841 ] {
842 assert_eq!(field_name(&f), "x");
843 assert_eq!(field_kind(&f), kind);
844 assert_eq!(field_required(&f), required);
845 }
846 }
847
848 #[test]
849 fn tag_and_operator_lookups() {
850 assert_eq!(sig_by_tag("Average").unwrap().tag, "Average");
852 assert!(sig_by_tag("NotATag").is_none());
853
854 let ops = binary_operators();
856 assert!(ops.contains(&(">", "Gt")));
857 assert!(ops.contains(&("/", "Div")));
858 assert_eq!(ops.len(), 10);
859
860 assert_eq!(dsl_name_for_tag("TotallyUnknownTag"), "");
862 assert_eq!(prefix_tag("+"), None);
863 }
864
865 #[test]
866 fn every_spec_op_has_a_signature_or_operator() {
867 for tag in ALL_OP_TAGS {
870 let known = ROWS.iter().any(|r| r.sig.tag == *tag)
871 || binop_symbol_for_tag(tag).is_some()
872 || *tag == "Neg"
873 || *tag == "Not"
874 || *tag == "Const"
875 || *tag == "Data";
876 assert!(known, "op `{tag}` has no DSL handler");
877 }
878 }
879}