1use std::collections::{HashMap, HashSet};
4use std::sync::{Mutex, PoisonError, RwLock};
5
6use indexmap::IndexMap;
7use serde_json::{Map, Value as Json};
8
9use crate::config::{Config, py_repr};
10
11pub type Settings = Map<String, Json>;
14
15const RULE_KEYS: [&str; 9] = [
16 "writable",
17 "min",
18 "max",
19 "units",
20 "description",
21 "deadband",
22 "always",
23 "bits",
24 "together",
25];
26const INHERITED: [&str; 2] = ["rate_ms", "together"];
28const NOT_FOR_BITS: [&str; 6] = ["min", "max", "deadband", "units", "bits", "together"];
29
30fn is_decorator_key(key: &str) -> bool {
31 RULE_KEYS.contains(&key) || key == "rate_ms"
32}
33
34pub fn truthy(value: Option<&Json>) -> bool {
36 match value {
37 None | Some(Json::Null) => false,
38 Some(Json::Bool(b)) => *b,
39 Some(Json::Number(n)) => n.as_f64().is_some_and(|f| f != 0.0),
40 Some(Json::String(s)) => !s.is_empty(),
41 Some(Json::Array(a)) => !a.is_empty(),
42 Some(Json::Object(o)) => !o.is_empty(),
43 }
44}
45
46pub fn split_bit(tag: &str) -> Option<(&str, &str)> {
48 let (word, bit) = tag.rsplit_once('.')?;
49 (!word.is_empty() && !bit.is_empty() && bit.bytes().all(|b| b.is_ascii_digit()))
50 .then_some((word, bit))
51}
52
53fn map_indexes(path: &str, f: impl Fn(&str) -> Option<String>) -> String {
55 let mut out = String::with_capacity(path.len());
56 let mut rest = path;
57 while let Some(open) = rest.find('[') {
58 let Some(close) = rest[open..].find(']').map(|c| open + c) else {
59 break;
60 };
61 out.push_str(&rest[..open]);
62 match f(&rest[open + 1..close]) {
63 Some(text) => out.push_str(&text),
64 None => out.push_str(&rest[open..=close]),
65 }
66 rest = &rest[close + 1..];
67 }
68 out.push_str(rest);
69 out
70}
71
72pub fn canonical(path: &str) -> String {
74 map_indexes(path, |inside| {
75 let numbers: Option<Vec<String>> = inside
76 .split(',')
77 .map(|i| i.trim().parse::<i64>().ok().map(|n| n.to_string()))
78 .collect();
79 numbers.map(|n| format!("[{}]", n.join(",")))
80 })
81}
82
83fn strip_indexes(path: &str) -> String {
85 map_indexes(path, |_| Some(String::new()))
86}
87
88pub fn normalize(path: &str) -> String {
90 canonical(path).to_lowercase()
91}
92
93pub fn glob_match(pattern: &str, name: &str) -> bool {
95 let p: Vec<char> = pattern.to_lowercase().chars().collect();
96 let n: Vec<char> = name.to_lowercase().chars().collect();
97 let (mut pi, mut ni) = (0, 0);
98 let mut star: Option<(usize, usize)> = None;
99 while ni < n.len() {
100 if pi < p.len() && (p[pi] == '?' || (p[pi] != '*' && p[pi] == n[ni])) {
101 pi += 1;
102 ni += 1;
103 } else if pi < p.len() && p[pi] == '*' {
104 star = Some((pi, ni));
105 pi += 1;
106 } else if let Some((sp, sn)) = star {
107 pi = sp + 1;
108 ni = sn + 1;
109 star = Some((sp, sn + 1));
110 } else {
111 return false;
112 }
113 }
114 p[pi..].iter().all(|&c| c == '*')
115}
116
117pub fn patterns(table: &Map<String, Json>) -> Vec<&str> {
119 match table.get("match") {
120 Some(Json::String(s)) => vec![s.as_str()],
121 Some(Json::Array(items)) => items.iter().filter_map(Json::as_str).collect(),
122 _ => Vec::new(),
123 }
124}
125
126fn matches(table: &Map<String, Json>, name: &str) -> bool {
127 patterns(table).iter().any(|p| glob_match(p, name))
128}
129
130fn parts(rest: &str) -> Vec<&str> {
132 let mut out = Vec::new();
133 let mut i = 0;
134 let bytes = rest.as_bytes();
135 while i < bytes.len() {
136 match bytes[i] {
137 b'[' => match rest[i..].find(']') {
138 Some(close) => {
139 out.push(&rest[i..=i + close]);
140 i += close + 1;
141 }
142 None => i += 1,
143 },
144 b'.' | b']' => i += 1,
145 _ => {
146 let end = rest[i..]
147 .find(['.', '[', ']'])
148 .map_or(rest.len(), |e| i + e);
149 out.push(&rest[i..end]);
150 i = end;
151 }
152 }
153 }
154 out
155}
156
157fn program_split(tag: &str) -> (String, &str) {
159 if tag.len() >= 8 && tag.is_char_boundary(8) && tag[..8].eq_ignore_ascii_case("program:") {
160 match tag.find('.') {
161 None => (format!("{tag}."), ""),
162 Some(dot) => (tag[..=dot].to_owned(), &tag[dot + 1..]),
163 }
164 } else {
165 (String::new(), tag)
166 }
167}
168
169static WARNED: Mutex<Option<HashSet<(String, String)>>> = Mutex::new(None);
170
171fn fields(table: &Map<String, Json>, where_: &str) -> Result<Settings, String> {
173 for key in table.keys() {
174 if !is_decorator_key(key) {
175 let mut warned = WARNED.lock().unwrap_or_else(PoisonError::into_inner);
176 if warned
177 .get_or_insert_with(HashSet::new)
178 .insert((where_.to_owned(), key.clone()))
179 {
180 log::warn!("{where_}: unknown key {} ignored", py_repr(key));
181 }
182 }
183 }
184 if let Some(rate) = table.get("rate_ms") {
185 if !rate.as_u64().is_some_and(|r| r > 0) {
186 return Err(format!("{where_}: rate_ms must be an integer > 0"));
187 }
188 }
189 Ok(table
190 .iter()
191 .filter(|(k, _)| is_decorator_key(k))
192 .map(|(k, v)| (k.clone(), v.clone()))
193 .collect())
194}
195
196fn entry(value: &Json, decorators: &Map<String, Json>, where_: &str) -> Result<Settings, String> {
199 let mut own = Settings::new();
200 let mut value = value;
201 let empty = Json::Array(Vec::new());
202 if let Json::Object(table) = value {
203 let rest: Map<String, Json> = table
204 .iter()
205 .filter(|(k, _)| k.as_str() != "use")
206 .map(|(k, v)| (k.clone(), v.clone()))
207 .collect();
208 own = fields(&rest, where_)?;
209 value = table.get("use").unwrap_or(&empty);
210 }
211 let bad = || format!("{where_}: expected a decorator name, a list of names or a table");
212 let names: Vec<&str> = match value {
213 Json::String(s) => vec![s.as_str()],
214 Json::Array(items) => items
215 .iter()
216 .map(|n| n.as_str().ok_or_else(bad))
217 .collect::<Result<_, _>>()?,
218 _ => return Err(bad()),
219 };
220 let mut result = Settings::new();
221 for name in names {
222 let Some(Json::Object(decorator)) = decorators.get(name) else {
223 return Err(format!("{where_}: unknown decorator {}", py_repr(name)));
224 };
225 for (k, v) in decorator {
226 result.insert(k.clone(), v.clone());
227 }
228 if decorator.contains_key("rate_ms") {
229 result.insert("group".into(), Json::String(name.to_owned()));
230 }
231 }
232 let own_rate = own.contains_key("rate_ms");
233 result.extend(own);
234 if own_rate {
235 result.insert("group".into(), Json::String("tag".into()));
236 }
237 Ok(result)
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct Group {
243 pub name: String,
245 pub rate_ms: u64,
247}
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250enum Kind {
251 Tag,
252 Member,
253 Element,
254 Bit,
255}
256
257type TypeEntry = (String, String, Option<Vec<u64>>);
259
260#[derive(Clone, Debug, Default)]
263pub struct Resolver {
264 udt: HashMap<String, HashMap<String, Settings>>,
265 tags: IndexMap<String, Settings>,
266 names: HashMap<String, String>,
267 rules: Vec<Map<String, Json>>,
268 groups: Vec<Map<String, Json>>,
269 tag_types: IndexMap<String, TypeEntry>,
270 types: HashMap<String, IndexMap<String, TypeEntry>>,
271}
272
273fn dims_of(value: Option<&Json>) -> Option<Vec<u64>> {
274 value?.as_array()?.iter().map(Json::as_u64).collect()
275}
276
277impl Resolver {
278 pub fn new(
280 decorators: &Map<String, Json>,
281 udt: &Map<String, Json>,
282 tags: &Map<String, Json>,
283 rules: Vec<Map<String, Json>>,
284 groups: Vec<Map<String, Json>>,
285 where_: &str,
286 ) -> Result<Self, String> {
287 let mut checked = Map::new();
288 for (name, d) in decorators {
289 let table = d
290 .as_object()
291 .ok_or_else(|| format!("decorator.{name} must be a table"))?;
292 checked.insert(
293 name.clone(),
294 Json::Object(fields(table, &format!("decorator.{name}"))?),
295 );
296 }
297 let mut udt_map = HashMap::new();
298 for (u, members) in udt {
299 let members = members
300 .as_object()
301 .ok_or_else(|| format!("{where_}.udt.{u} must be a table"))?;
302 let mut resolved = HashMap::new();
303 for (m, e) in members {
304 resolved.insert(
305 m.to_lowercase(),
306 entry(e, &checked, &format!("{where_}.udt.{u}.{m}"))?,
307 );
308 }
309 udt_map.insert(u.to_lowercase(), resolved);
310 }
311 let mut tag_map = IndexMap::new();
312 let mut names = HashMap::new();
313 for (k, e) in tags {
314 tag_map.insert(
315 normalize(k),
316 entry(e, &checked, &format!("{where_}.tags.{k}"))?,
317 );
318 names.insert(normalize(k), canonical(k));
319 }
320 Ok(Resolver {
321 udt: udt_map,
322 tags: tag_map,
323 names,
324 rules,
325 groups,
326 ..Resolver::default()
327 })
328 }
329
330 pub fn from_body(body: &Json) -> Result<Self, String> {
332 let empty = Map::new();
333 let obj = |key: &str| body.get(key).and_then(Json::as_object).unwrap_or(&empty);
334 let list = |key: &str| -> Vec<Map<String, Json>> {
335 body.get(key)
336 .and_then(Json::as_array)
337 .map(|a| a.iter().filter_map(|t| t.as_object().cloned()).collect())
338 .unwrap_or_default()
339 };
340 let mut resolver = Resolver::new(
341 obj("decorators"),
342 obj("udt"),
343 obj("tag_settings"),
344 list("rules"),
345 list("groups"),
346 body.get("plc").and_then(Json::as_str).unwrap_or("plc"),
347 )?;
348 let tags = body
349 .get("tags")
350 .and_then(Json::as_array)
351 .cloned()
352 .unwrap_or_default();
353 resolver.set_types(&tags, obj("types"));
354 Ok(resolver)
355 }
356
357 pub fn set_types(&mut self, tag_list: &[Json], types: &Map<String, Json>) {
359 self.tag_types = tag_list
360 .iter()
361 .filter_map(|t| {
362 let name = t.get("name")?.as_str()?;
363 let type_ = t
364 .get("type")
365 .and_then(Json::as_str)
366 .unwrap_or("")
367 .to_lowercase();
368 Some((
369 name.to_lowercase(),
370 (name.to_owned(), type_, dims_of(t.get("dims"))),
371 ))
372 })
373 .collect();
374 self.types = types
375 .iter()
376 .map(|(u, t)| {
377 let members = t
378 .get("members")
379 .and_then(Json::as_object)
380 .map(|members| {
381 members
382 .iter()
383 .map(|(m, i)| {
384 let type_ = i
385 .get("type")
386 .and_then(Json::as_str)
387 .unwrap_or("")
388 .to_lowercase();
389 (m.to_lowercase(), (m.clone(), type_, dims_of(i.get("dims"))))
390 })
391 .collect()
392 })
393 .unwrap_or_default();
394 (u.to_lowercase(), members)
395 })
396 .collect();
397 }
398
399 fn member_type(&self, udt: Option<&str>, member: &str) -> Option<&TypeEntry> {
400 self.types.get(udt?)?.get(&member.to_lowercase())
401 }
402
403 fn nodes(&self, tag: &str) -> Vec<(Kind, Settings)> {
405 let (prefix, rest) = program_split(tag);
406 let parts = parts(rest);
407 if parts.is_empty() {
408 return vec![(
409 Kind::Tag,
410 self.tags.get(&normalize(tag)).cloned().unwrap_or_default(),
411 )];
412 }
413 let mut path = format!("{prefix}{}", parts[0]);
414 let mut type_: Option<String> = self
415 .tag_types
416 .get(&path.to_lowercase())
417 .map(|t| t.1.clone());
418 let mut result = vec![(
419 Kind::Tag,
420 self.tags
421 .get(&normalize(&path))
422 .cloned()
423 .unwrap_or_default(),
424 )];
425 for (i, part) in parts.iter().enumerate().skip(1) {
426 let mut udt = None;
427 let kind = if part.starts_with('[') {
428 path.push_str(part);
429 Kind::Element
430 } else if part.bytes().all(|b| b.is_ascii_digit()) && i + 1 == parts.len() {
431 path = format!("{path}.{part}");
432 type_ = None;
433 Kind::Bit
434 } else {
435 path = format!("{path}.{part}");
436 udt = type_.take();
437 type_ = self.member_type(udt.as_deref(), part).map(|t| t.1.clone());
438 Kind::Member
439 };
440 let key = normalize(&path);
441 let mut found = udt
442 .as_deref()
443 .and_then(|u| self.udt.get(u))
444 .and_then(|members| members.get(&part.to_lowercase()))
445 .cloned()
446 .unwrap_or_default();
447 let stripped = strip_indexes(&key);
448 if stripped != key {
449 if let Some(s) = self.tags.get(&stripped) {
450 found.extend(s.clone());
451 }
452 }
453 if let Some(s) = self.tags.get(&key) {
454 found.extend(s.clone());
455 }
456 result.push((kind, found));
457 }
458 result
459 }
460
461 pub fn effective(&self, tag: &str) -> Settings {
466 let nodes = self.nodes(tag);
467 let mut result = Settings::new();
468 for (_, found) in &nodes {
469 for key in INHERITED {
470 if let Some(v) = found.get(key) {
471 result.insert(key.into(), v.clone());
472 if key == "rate_ms" {
473 result.insert(
474 "group".into(),
475 found.get("group").cloned().unwrap_or(Json::Null),
476 );
477 }
478 }
479 }
480 }
481 let mut i = nodes.len() - 1;
482 let mut chain = vec![&nodes[i]];
483 while nodes[i].0 == Kind::Element {
484 i -= 1;
485 chain.insert(0, &nodes[i]);
486 }
487 for (_, found) in chain {
488 for (k, v) in found {
489 if !INHERITED.contains(&k.as_str()) && k != "group" {
490 result.insert(k.clone(), v.clone());
491 }
492 }
493 }
494 for rule in &self.rules {
495 if matches(rule, tag) {
496 for key in RULE_KEYS {
497 if let Some(v) = rule.get(key) {
498 if !result.contains_key(key) {
499 result.insert(key.into(), v.clone());
500 }
501 }
502 }
503 }
504 }
505 if let Some((word, _)) = split_bit(tag) {
506 result.retain(|k, _| !NOT_FOR_BITS.contains(&k.as_str()));
507 if !result.contains_key("writable") {
508 if let Some(w) = self.effective(word).get("writable") {
509 result.insert("writable".into(), w.clone());
510 }
511 }
512 }
513 result
514 }
515
516 pub fn group(&self, tag: &str, settings: &Settings) -> Option<Group> {
519 if let Some(rate) = settings.get("rate_ms").and_then(Json::as_u64) {
520 let name = settings
521 .get("group")
522 .and_then(Json::as_str)
523 .unwrap_or("tag");
524 return Some(Group {
525 name: name.to_owned(),
526 rate_ms: rate,
527 });
528 }
529 self.groups.iter().find(|g| matches(g, tag)).and_then(|g| {
530 Some(Group {
531 name: match g.get("name")? {
532 Json::String(s) => s.clone(),
533 other => other.to_string(),
534 },
535 rate_ms: g.get("rate_ms")?.as_u64()?,
536 })
537 })
538 }
539
540 pub fn always(&self) -> Vec<String> {
543 let mut result: Vec<String> = Vec::new();
544 for rule in &self.rules {
545 if truthy(rule.get("always")) {
546 for name in patterns(rule) {
547 if name.contains(['*', '?']) {
548 log::warn!(
549 "always ignored for the pattern {}, it takes tag names only",
550 py_repr(name)
551 );
552 } else {
553 result.push(name.to_owned());
554 }
555 }
556 }
557 }
558 for (k, found) in &self.tags {
559 if truthy(found.get("always")) {
560 result.extend(self.expand(&self.names[k]));
561 }
562 }
563 let mut targets = HashSet::new();
564 for (u, members) in &self.udt {
565 for (m, found) in members {
566 if truthy(found.get("always")) {
567 targets.insert((u.clone(), m.clone()));
568 }
569 }
570 }
571 result.extend(self.instances(&targets));
572 let mut seen = HashSet::new();
573 result
574 .into_iter()
575 .filter(|name| seen.insert(name.clone()))
576 .filter(|name| truthy(self.effective(name).get("always")))
577 .collect()
578 }
579
580 fn expand(&self, key: &str) -> Vec<String> {
583 let (prefix, rest) = program_split(key);
584 let parts = parts(rest);
585 let found = parts
586 .first()
587 .and_then(|p| self.tag_types.get(&format!("{prefix}{p}").to_lowercase()));
588 let Some(found) = found.filter(|_| !key.contains('[')) else {
589 return vec![key.to_owned()];
590 };
591 let mut paths = vec![found.0.clone()];
592 let (mut type_, mut dims) = (Some(found.1.clone()), found.2.clone());
593 for part in &parts[1..] {
594 if let Some(d) = dims.as_deref().filter(|d| !d.is_empty()) {
595 paths = paths
596 .iter()
597 .flat_map(|p| {
598 product(d)
599 .into_iter()
600 .map(move |i| format!("{p}[{}]", join(&i)))
601 })
602 .collect();
603 }
604 let (name, t, d) = match self.member_type(type_.as_deref(), part) {
605 Some((name, t, d)) => (name.clone(), Some(t.clone()), d.clone()),
606 None => ((*part).to_owned(), None, None),
607 };
608 paths = paths.into_iter().map(|p| format!("{p}.{name}")).collect();
609 type_ = t;
610 dims = d;
611 }
612 paths
613 }
614
615 fn instances(&self, targets: &HashSet<(String, String)>) -> Vec<String> {
617 if targets.is_empty() {
618 return Vec::new();
619 }
620 let mut reach: HashMap<String, bool> = HashMap::new();
621 let mut found = Vec::new();
622 for (name, type_, dims) in self.tag_types.values() {
623 if self.reaches(type_, targets, &mut reach) {
624 self.walk(
625 name,
626 type_,
627 dims.as_deref(),
628 targets,
629 &mut reach,
630 &mut found,
631 );
632 }
633 }
634 found
635 }
636
637 fn reaches(
638 &self,
639 type_: &str,
640 targets: &HashSet<(String, String)>,
641 reach: &mut HashMap<String, bool>,
642 ) -> bool {
643 if let Some(r) = reach.get(type_) {
644 return *r;
645 }
646 reach.insert(type_.to_owned(), false); let result = self.types.get(type_).is_some_and(|members| {
648 members.iter().any(|(m, (_, t, _))| {
649 targets.contains(&(type_.to_owned(), m.clone())) || self.reaches(t, targets, reach)
650 })
651 });
652 reach.insert(type_.to_owned(), result);
653 result
654 }
655
656 fn walk(
657 &self,
658 path: &str,
659 type_: &str,
660 dims: Option<&[u64]>,
661 targets: &HashSet<(String, String)>,
662 reach: &mut HashMap<String, bool>,
663 found: &mut Vec<String>,
664 ) {
665 if let Some(d) = dims.filter(|d| !d.is_empty()) {
666 for index in product(d) {
667 self.walk(
668 &format!("{path}[{}]", join(&index)),
669 type_,
670 None,
671 targets,
672 reach,
673 found,
674 );
675 }
676 return;
677 }
678 let Some(members) = self.types.get(type_) else {
679 return;
680 };
681 for (m, (name, t, member_dims)) in members {
682 if targets.contains(&(type_.to_owned(), m.clone())) {
683 found.push(format!("{path}.{name}"));
684 }
685 if self.reaches(t, targets, reach) {
686 self.walk(
687 &format!("{path}.{name}"),
688 t,
689 member_dims.as_deref(),
690 targets,
691 reach,
692 found,
693 );
694 }
695 }
696 }
697}
698
699fn product(dims: &[u64]) -> Vec<Vec<u64>> {
701 let mut result = vec![Vec::new()];
702 for &n in dims {
703 result = result
704 .into_iter()
705 .flat_map(|prefix| {
706 (0..n).map(move |i| {
707 let mut next = prefix.clone();
708 next.push(i);
709 next
710 })
711 })
712 .collect();
713 }
714 result
715}
716
717fn join(index: &[u64]) -> String {
718 index
719 .iter()
720 .map(u64::to_string)
721 .collect::<Vec<_>>()
722 .join(",")
723}
724
725#[derive(Debug)]
727pub struct Rules {
728 resolvers: HashMap<String, RwLock<Resolver>>,
729 groups: HashMap<String, Vec<Map<String, Json>>>,
730 memo: Mutex<HashMap<(String, String), Settings>>,
731}
732
733const MEMO_MAX: usize = 10_000;
734
735impl Rules {
736 pub fn new(config: &Config) -> Result<Self, String> {
738 let mut resolvers = HashMap::new();
739 let mut groups = HashMap::new();
740 for (name, plc) in &config.plcs {
741 let resolver = Resolver::new(
742 &config.decorators,
743 &plc.udt,
744 &plc.tags,
745 plc.rules.clone(),
746 plc.groups.clone(),
747 &format!("plc.{name}"),
748 )?;
749 resolvers.insert(name.clone(), RwLock::new(resolver));
750 groups.insert(name.clone(), plc.groups.clone());
751 }
752 Ok(Rules {
753 resolvers,
754 groups,
755 memo: Mutex::new(HashMap::new()),
756 })
757 }
758
759 fn with<T>(&self, plc: &str, f: impl FnOnce(&Resolver) -> T) -> T {
760 match self.resolvers.get(plc) {
761 Some(r) => f(&r.read().unwrap_or_else(PoisonError::into_inner)),
762 None => f(&Resolver::default()),
763 }
764 }
765
766 pub fn set_types(&self, plc: &str, tag_list: &[Json], types: &Map<String, Json>) {
768 if let Some(r) = self.resolvers.get(plc) {
769 r.write()
770 .unwrap_or_else(PoisonError::into_inner)
771 .set_types(tag_list, types);
772 }
773 self.memo
774 .lock()
775 .unwrap_or_else(PoisonError::into_inner)
776 .clear();
777 }
778
779 pub fn effective(&self, plc: &str, tag: &str) -> Settings {
781 let key = (plc.to_owned(), tag.to_lowercase());
782 if let Some(s) = self
783 .memo
784 .lock()
785 .unwrap_or_else(PoisonError::into_inner)
786 .get(&key)
787 {
788 return s.clone();
789 }
790 let settings = self.with(plc, |r| r.effective(tag));
791 let mut memo = self.memo.lock().unwrap_or_else(PoisonError::into_inner);
792 if memo.len() >= MEMO_MAX {
793 memo.clear();
794 }
795 memo.insert(key, settings.clone());
796 settings
797 }
798
799 pub fn resolve(&self, plc: &str, tag: &str) -> String {
801 if !tag.contains('.') || split_bit(tag).is_some() {
802 return tag.to_owned();
803 }
804 let Some((word, name)) = tag.rsplit_once('.') else {
805 return tag.to_owned();
806 };
807 if let Some(Json::Object(bits)) = self.effective(plc, word).get("bits") {
808 for (bit_name, n) in bits {
809 if bit_name.to_lowercase() == name.to_lowercase() {
810 let n = match n {
811 Json::String(s) => s.clone(),
812 other => other.to_string(),
813 };
814 return format!("{word}.{n}");
815 }
816 }
817 }
818 tag.to_owned()
819 }
820
821 pub fn group(&self, plc: &str, tag: &str) -> Option<Group> {
823 let settings = self.effective(plc, tag);
824 self.with(plc, |r| r.group(tag, &settings))
825 }
826
827 pub fn groups(&self, plc: &str) -> &[Map<String, Json>] {
829 self.groups.get(plc).map_or(&[], Vec::as_slice)
830 }
831
832 pub fn always(&self, plc: &str) -> Vec<String> {
834 self.with(plc, Resolver::always)
835 }
836}
837
838#[cfg(test)]
839#[allow(clippy::unwrap_used)]
840mod tests {
841 use super::*;
842 use serde_json::json;
843
844 fn obj(v: Json) -> Map<String, Json> {
845 v.as_object().cloned().unwrap_or_default()
846 }
847
848 #[test]
849 fn glob_patterns() {
850 assert!(glob_match("Tank[*].Level", "tank[3].LEVEL"));
851 assert!(glob_match("PLC1/Bench.*", "PLC1/Bench.SpeedSP"));
852 assert!(glob_match("A?C", "abc"));
853 assert!(!glob_match("A?C", "abbc"));
854 assert!(glob_match("*", ""));
855 assert!(!glob_match("Arr[[]3]", "Arr[3]"));
856 assert!(glob_match("a*b*c", "aXXbYYc"));
857 assert!(!glob_match("a*b*c", "aXXbYY"));
858 }
859
860 #[test]
861 fn canonical_indexes() {
862 assert_eq!(canonical("A[02].B[ 3, 4]"), "A[2].B[3,4]");
863 assert_eq!(canonical("A[x]"), "A[x]");
864 assert_eq!(normalize("Tank[01].Level"), "tank[1].level");
865 assert_eq!(strip_indexes("a[1].b[2,3]"), "a.b");
866 }
867
868 #[test]
869 fn parts_and_bits() {
870 assert_eq!(parts("A[1].B.3"), vec!["A", "[1]", "B", "3"]);
871 assert_eq!(split_bit("X.3"), Some(("X", "3")));
872 assert_eq!(split_bit("X.Y"), None);
873 assert_eq!(split_bit(".3"), None);
874 assert_eq!(
875 program_split("Program:Main.Tag.X"),
876 ("Program:Main.".into(), "Tag.X")
877 );
878 assert_eq!(program_split("Program:Main"), ("Program:Main.".into(), ""));
879 }
880
881 fn resolver() -> Resolver {
882 let decorators = obj(json!({
883 "temperature": {"units": "°C", "rate_ms": 1000},
884 "speed": {"units": "rpm", "min": 0, "max": 3000},
885 "setpoint": {"writable": true},
886 "fast": {"rate_ms": 250},
887 }));
888 let udt = obj(json!({
889 "MotorData": {"Temp": "temperature", "SpeedSP": {"use": ["speed", "setpoint"], "description": "SP"}},
890 "PumpData": {"Motor": "fast"},
891 }));
892 let tags = obj(json!({
893 "Pump1": "fast",
894 "TankLevel": "temperature",
895 "Pumps[2].Motor.SpeedSP": {"max": 1500},
896 "Program:Main.StatusWord": {"bits": {"EStop": 0, "DoorOpen": 1}},
897 }));
898 let rules = vec![obj(
899 json!({"match": "Line.*", "units": "m", "writable": true}),
900 )];
901 let mut r = Resolver::new(&decorators, &udt, &tags, rules, vec![], "plc.P").unwrap();
902 let tag_list = vec![
903 json!({"name": "Pump1", "type": "PumpData"}),
904 json!({"name": "Pumps", "type": "PumpData", "dims": [3]}),
905 json!({"name": "TankLevel", "type": "REAL", "dims": [4]}),
906 ];
907 let types = obj(json!({
908 "PumpData": {"members": {"Motor": {"type": "MotorData"}}},
909 "MotorData": {"members": {"Temp": {"type": "REAL"}, "SpeedSP": {"type": "REAL"}}},
910 }));
911 r.set_types(&tag_list, &types);
912 r
913 }
914
915 #[test]
916 fn decorators_resolve_like_the_python_server() {
917 let r = resolver();
918 let sp = r.effective("Pumps[1].Motor.SpeedSP");
919 assert_eq!(sp.get("max"), Some(&json!(3000)));
920 assert_eq!(sp.get("writable"), Some(&json!(true)));
921 assert_eq!(sp.get("rate_ms"), Some(&json!(250)));
922 assert_eq!(sp.get("group"), Some(&json!("fast")));
923 assert_eq!(
924 r.effective("Pumps[2].Motor.SpeedSP").get("max"),
925 Some(&json!(1500))
926 );
927 let temp = r.effective("Pump1.Motor.Temp");
928 assert_eq!(temp.get("rate_ms"), Some(&json!(1000)));
929 assert_eq!(temp.get("group"), Some(&json!("temperature")));
930 assert_eq!(r.effective("TankLevel[3]").get("units"), Some(&json!("°C")));
931 let bit = r.effective("Pumps[1].Motor.SpeedSP.3");
933 assert_eq!(bit.get("writable"), Some(&json!(true)));
934 assert!(!bit.contains_key("max"));
935 assert_eq!(r.effective("Line.Speed").get("units"), Some(&json!("m")));
937 }
938
939 #[test]
940 fn groups_from_decorators_then_tables() {
941 let mut r = resolver();
942 r.groups = vec![obj(
943 json!({"name": "slow", "rate_ms": 2000, "match": "Other*"}),
944 )];
945 let s = r.effective("OtherTag");
946 assert_eq!(
947 r.group("OtherTag", &s),
948 Some(Group {
949 name: "slow".into(),
950 rate_ms: 2000
951 })
952 );
953 let s = r.effective("Pump1.Motor.Temp");
954 assert_eq!(
955 r.group("Pump1.Motor.Temp", &s),
956 Some(Group {
957 name: "temperature".into(),
958 rate_ms: 1000
959 })
960 );
961 }
962
963 #[test]
964 fn always_expands_udt_members_and_keys() {
965 let decorators = obj(json!({"alarm": {"always": true}}));
966 let udt = obj(json!({"MotorData": {"Fault": "alarm"}}));
967 let tags = obj(json!({"Pumps.Motor.Temp": "alarm"}));
968 let rules = vec![obj(json!({"match": ["Status", "Bad*"], "always": true}))];
969 let mut r = Resolver::new(&decorators, &udt, &tags, rules, vec![], "plc.P").unwrap();
970 r.set_types(
971 &[
972 json!({"name": "Pumps", "type": "PumpData", "dims": [2]}),
973 json!({"name": "M", "type": "MotorData"}),
974 ],
975 &obj(json!({
976 "PumpData": {"members": {"Motor": {"type": "MotorData"}}},
977 "MotorData": {"members": {"Temp": {"type": "REAL"}, "Fault": {"type": "BOOL"}}},
978 })),
979 );
980 assert_eq!(
981 r.always(),
982 vec![
983 "Status",
984 "Pumps[0].Motor.Temp",
985 "Pumps[1].Motor.Temp",
986 "Pumps[0].Motor.Fault",
987 "Pumps[1].Motor.Fault",
988 "M.Fault"
989 ]
990 );
991 }
992
993 #[test]
994 fn bit_names_resolve() {
995 let config = crate::config::parse_config(
996 &toml::from_str::<Json>(
997 "[plc.P]\npath=\"x\"\n[[plc.P.rule]]\nmatch=\"W\"\nbits={EStop=0, Door=1}\n",
998 )
999 .unwrap(),
1000 std::path::Path::new("."),
1001 )
1002 .unwrap();
1003 let rules = Rules::new(&config).unwrap();
1004 assert_eq!(rules.resolve("P", "W.estop"), "W.0");
1005 assert_eq!(rules.resolve("P", "W.Door"), "W.1");
1006 assert_eq!(rules.resolve("P", "W.Other"), "W.Other");
1007 assert_eq!(rules.resolve("P", "W.3"), "W.3");
1008 }
1009 #[test]
1012 fn match_patterns() {
1013 for (pattern, name, expected) in [
1014 ("Bench.SpeedSP", "bench.speedsp", true), ("Tank[*].Level", "Tank[3].Level", true), ("Tank[*].Level", "Tank3.Level", false),
1017 ("Arr[[]3]", "Arr[3]", false), ("Bench.*", "Bench.Axis.Position", true), ("PLC1/Speed*", "PLC1/SpeedSP", true),
1020 ("Speed?", "Speed1", true),
1021 ("Speed?", "Speed12", false),
1022 ("Speed", "SpeedSP", false), ("a.b", "aXb", false), ("Program:Bench.*", "Program:Bench.Mode", true),
1025 ] {
1026 assert_eq!(glob_match(pattern, name), expected, "{pattern} {name}");
1027 }
1028 }
1029
1030 fn rules(tables: Json, groups: Json) -> Rules {
1031 let data = json!({"plc": {"P": {"path": "x", "rule": tables, "group": groups}}});
1032 Rules::new(&crate::config::parse_config(&data, std::path::Path::new(".")).unwrap()).unwrap()
1033 }
1034
1035 #[test]
1036 fn effective_first_rule_per_field_wins() {
1037 let r = rules(
1038 json!([{"match": "Bench.SpeedSP", "writable": true, "max": 1800},
1039 {"match": ["Bench.*", "Other"], "units": "rpm", "max": 5, "writable": false, "nonsense": 1}]),
1040 json!([]),
1041 );
1042 let eff = |tag: &str| Json::Object(r.effective("P", tag));
1043 assert_eq!(
1044 eff("bench.speedsp"),
1045 json!({"writable": true, "max": 1800, "units": "rpm"})
1046 );
1047 let other = json!({"units": "rpm", "max": 5, "writable": false});
1048 assert_eq!(eff("Bench.Speed"), other);
1049 assert_eq!(eff("Other"), other);
1050 assert_eq!(eff("Nothing"), json!({}));
1051 }
1052
1053 #[test]
1054 fn effective_of_a_bit_takes_only_writable_from_its_word() {
1055 let r = rules(
1056 json!([{"match": "W", "writable": true, "min": 0, "bits": {"EStop": 0}, "units": "x", "description": "d"},
1057 {"match": "V.*", "max": 3, "description": "bit"}, {"match": "V", "writable": true}]),
1058 json!([]),
1059 );
1060 let eff = |tag: &str| Json::Object(r.effective("P", tag));
1061 assert_eq!(eff("W.3"), json!({"writable": true}));
1062 assert_eq!(eff("V.0"), json!({"description": "bit", "writable": true}));
1064 assert_eq!(eff("Z.0"), json!({}));
1065 }
1066
1067 #[test]
1068 fn effective_memo_is_bounded() {
1069 let r = rules(json!([{"match": "*", "units": "u"}]), json!([]));
1070 for i in 0..MEMO_MAX + 10 {
1071 assert_eq!(
1072 Json::Object(r.effective("P", &format!("T{i}"))),
1073 json!({"units": "u"})
1074 );
1075 }
1076 assert!(r.memo.lock().unwrap().len() <= MEMO_MAX);
1077 }
1078
1079 #[test]
1080 fn resolve_bit_names() {
1081 let r = rules(
1082 json!([{"match": "Bench.StatusWord", "bits": {"EStop": 0, "DoorOpen": 1}},
1083 {"match": "Tank[*].Status", "bits": {"Alarm": 7}}]),
1084 json!([]),
1085 );
1086 assert_eq!(
1087 r.resolve("P", "Bench.StatusWord.estop"),
1088 "Bench.StatusWord.0"
1089 );
1090 assert_eq!(
1091 r.resolve("P", "Bench.StatusWord.DoorOpen"),
1092 "Bench.StatusWord.1"
1093 );
1094 assert_eq!(r.resolve("P", "Tank[2].Status.Alarm"), "Tank[2].Status.7");
1095 assert_eq!(r.resolve("P", "Bench.StatusWord.5"), "Bench.StatusWord.5");
1096 assert_eq!(
1098 r.resolve("P", "Bench.StatusWord.Nope"),
1099 "Bench.StatusWord.Nope"
1100 );
1101 assert_eq!(r.resolve("P", "Speed"), "Speed");
1102 }
1103
1104 #[test]
1105 fn groups() {
1106 let r = rules(
1107 json!([]),
1108 json!([{"name": "fast", "rate_ms": 50, "match": ["Bench.Axis.*"]},
1109 {"name": "also", "rate_ms": 10, "match": "Bench.Axis.Position"},
1110 {"name": "slow", "rate_ms": 2000, "match": "Tank[*].Temp"}]),
1111 );
1112 let group = |name: &str, rate_ms| {
1113 Some(Group {
1114 name: name.into(),
1115 rate_ms,
1116 })
1117 };
1118 assert_eq!(r.group("P", "Bench.Axis.Position"), group("fast", 50));
1120 assert_eq!(r.group("P", "tank[1].temp"), group("slow", 2000));
1121 assert_eq!(r.group("P", "Bench.Speed"), None);
1122 let names: Vec<&Json> = r.groups("P").iter().map(|g| &g["name"]).collect();
1123 assert_eq!(names, [&json!("fast"), &json!("also"), &json!("slow")]);
1124 }
1125
1126 #[test]
1127 fn always_takes_literal_names() {
1128 let r = rules(
1129 json!([{"match": "Off", "always": false},
1130 {"match": ["Bench.StatusWord", "Off", "Tank[*].Level"], "always": true},
1131 {"match": "Alarm", "always": true}]),
1132 json!([]),
1133 );
1134 assert_eq!(r.always("P"), ["Bench.StatusWord", "Alarm"]);
1136 }
1137
1138 fn decorators() -> Json {
1141 json!({
1142 "temp": {"units": "C", "rate_ms": 1000},
1143 "fast": {"rate_ms": 250},
1144 "slow": {"rate_ms": 2000, "together": true},
1145 "sp": {"writable": true, "min": 0, "max": 100, "units": "rpm"},
1146 "cmd": {"writable": true},
1147 "alarm": {"always": true},
1148 })
1149 }
1150
1151 fn tag_list() -> Vec<Json> {
1152 vec![
1153 json!({"name": "Line", "type": "LineData"}),
1154 json!({"name": "Axes", "type": "Axis", "dims": [2]}),
1155 json!({"name": "Zone", "type": "REAL", "dims": [8]}),
1156 json!({"name": "Program:Main.Axis", "type": "Axis"}),
1157 json!({"name": "Word", "type": "DINT"}),
1158 ]
1159 }
1160
1161 fn types() -> Json {
1162 json!({
1163 "LineData": {"members": {"Pump": {"type": "Pump"}, "Axes": {"type": "Axis", "dims": [2]}}},
1164 "Axis": {"members": {"Drive": {"type": "Drive"}, "Pos": {"type": "REAL"}, "Jog": {"type": "BOOL"}}},
1165 "Drive": {"members": {"Temp": {"type": "REAL"}, "Amps": {"type": "REAL"},
1166 "Faults": {"type": "DINT", "dims": [4]}}},
1167 "Pump": {"members": {"SpeedSP": {"type": "REAL"}, "Speed": {"type": "REAL"}, "Alarm": {"type": "BOOL"}}},
1168 })
1169 }
1170
1171 fn resolver_of(udt: Json, tags: Json, rules: Json, groups: Json, with_types: bool) -> Resolver {
1173 let list = |v: Json| -> Vec<Map<String, Json>> {
1174 v.as_array()
1175 .unwrap()
1176 .iter()
1177 .map(|t| obj(t.clone()))
1178 .collect()
1179 };
1180 let mut r = Resolver::new(
1181 &obj(decorators()),
1182 &obj(udt),
1183 &obj(tags),
1184 list(rules),
1185 list(groups),
1186 "plc.P",
1187 )
1188 .unwrap();
1189 if with_types {
1190 r.set_types(&tag_list(), &obj(types()));
1191 }
1192 r
1193 }
1194
1195 fn with(udt: Json, tags: Json) -> Resolver {
1196 resolver_of(udt, tags, json!([]), json!([]), true)
1197 }
1198
1199 fn eff(r: &Resolver, tag: &str) -> Json {
1200 Json::Object(r.effective(tag))
1201 }
1202
1203 fn group_of(r: &Resolver, tag: &str) -> Option<Group> {
1204 r.group(tag, &r.effective(tag))
1205 }
1206
1207 #[test]
1208 fn entry_forms_list_order_and_inline_fields() {
1209 let r = with(
1210 json!({}),
1211 json!({"A": "temp", "B": ["temp", "fast"], "C": ["fast", "temp"], "D": {"use": "sp", "max": 5},
1212 "E": {"use": ["sp", "temp"], "units": "%", "rate_ms": 10}, "F": {"min": 1}}),
1213 );
1214 assert_eq!(
1215 eff(&r, "A"),
1216 json!({"units": "C", "rate_ms": 1000, "group": "temp"})
1217 );
1218 assert_eq!(eff(&r, "B")["rate_ms"], 250);
1220 assert_eq!(
1221 group_of(&r, "B"),
1222 Some(Group {
1223 name: "fast".into(),
1224 rate_ms: 250
1225 })
1226 );
1227 assert_eq!(eff(&r, "C")["rate_ms"], 1000);
1228 assert_eq!(
1230 eff(&r, "D"),
1231 json!({"writable": true, "min": 0, "max": 5, "units": "rpm"})
1232 );
1233 assert_eq!(eff(&r, "e")["units"], "%");
1234 assert_eq!(
1235 group_of(&r, "E"),
1236 Some(Group {
1237 name: "tag".into(),
1238 rate_ms: 10
1239 })
1240 );
1241 assert_eq!(eff(&r, "F"), json!({"min": 1}));
1242 }
1243
1244 #[test]
1245 fn precedence_at_a_node() {
1246 let r = with(
1247 json!({"Axis": {"Pos": {"units": "udt", "min": 1, "max": 9}}}),
1248 json!({"Axes.Pos": {"units": "stripped", "min": 2}, "Axes[1].Pos": {"units": "specific"}}),
1249 );
1250 assert_eq!(
1251 eff(&r, "Axes[1].Pos"),
1252 json!({"units": "specific", "min": 2, "max": 9})
1253 );
1254 assert_eq!(
1255 eff(&r, "Axes[0].Pos"),
1256 json!({"units": "stripped", "min": 2, "max": 9})
1257 );
1258 assert_eq!(
1260 eff(&r, "Program:Main.Axis.Pos"),
1261 json!({"units": "udt", "min": 1, "max": 9})
1262 );
1263 assert_eq!(eff(&r, "AXES[01].pos")["units"], "specific"); }
1265
1266 #[test]
1267 fn inherited_fields_closest_wins() {
1268 let r = with(
1269 json!({"Axis": {"Drive": "fast"}, "Drive": {"Temp": "temp"}, "LineData": {"Pump": "slow"}}),
1270 json!({"Line": {"rate_ms": 50, "together": false}}),
1271 );
1272 assert_eq!(
1273 eff(&r, "Axes[1].Drive.Amps"),
1274 json!({"rate_ms": 250, "group": "fast"})
1275 );
1276 assert_eq!(
1277 eff(&r, "Axes[1].Drive.Faults[2]"),
1278 json!({"rate_ms": 250, "group": "fast"})
1279 );
1280 assert_eq!(
1281 eff(&r, "Axes[0].Drive.Temp"),
1282 json!({"rate_ms": 1000, "group": "temp", "units": "C"})
1283 );
1284 assert_eq!(
1285 eff(&r, "Line.Pump.Speed"),
1286 json!({"rate_ms": 2000, "group": "slow", "together": true})
1287 );
1288 assert_eq!(
1289 eff(&r, "Line.Axes[0].Pos"),
1290 json!({"rate_ms": 50, "group": "tag", "together": false})
1291 );
1292 assert_eq!(
1293 eff(&r, "Line.Axes[0].Drive.Amps"),
1294 json!({"rate_ms": 250, "group": "fast", "together": false})
1295 );
1296 }
1297
1298 #[test]
1299 fn value_fields_do_not_inherit_into_members() {
1300 let r = with(
1301 json!({}),
1302 json!({"Line": ["sp", "alarm"], "Line.Pump": {"description": "pump", "bits": {"X": 1}}}),
1303 );
1304 assert_eq!(eff(&r, "Line")["writable"], true);
1305 assert_eq!(
1306 eff(&r, "Line.Pump"),
1307 json!({"description": "pump", "bits": {"X": 1}})
1308 );
1309 assert_eq!(eff(&r, "Line.Pump.SpeedSP"), json!({}));
1310 }
1311
1312 #[test]
1313 fn array_elements_take_their_arrays_value_fields() {
1314 let r = with(
1315 json!({}),
1316 json!({"Zone": "temp", "Zone[2]": {"units": "K"}, "Axes": "sp"}),
1317 );
1318 assert_eq!(
1319 eff(&r, "Zone[3]"),
1320 json!({"units": "C", "rate_ms": 1000, "group": "temp"})
1321 );
1322 assert_eq!(eff(&r, "Zone[2]")["units"], "K");
1323 assert_eq!(eff(&r, "Axes[1]")["writable"], true);
1324 assert!(eff(&r, "Axes[1].Pos").get("writable").is_none());
1325 }
1326
1327 #[test]
1328 fn nested_udts_and_program_scope() {
1329 let r = with(
1330 json!({"Drive": {"Temp": "temp"}, "Pump": {"SpeedSP": "sp"}}),
1331 json!({"Program:Main.Axis.Jog": "cmd"}),
1332 );
1333 for path in [
1334 "Axes[0].Drive.Temp",
1335 "Line.Axes[1].Drive.Temp",
1336 "Program:Main.Axis.Drive.Temp",
1337 ] {
1338 assert_eq!(eff(&r, path)["units"], "C");
1339 }
1340 assert_eq!(eff(&r, "Line.Pump.SpeedSP")["writable"], true);
1341 assert_eq!(eff(&r, "program:main.axis.jog"), json!({"writable": true}));
1342 assert_eq!(eff(&r, "Program:Main.Axis.Pos"), json!({}));
1343 }
1344
1345 #[test]
1346 fn bits() {
1347 let r = with(
1348 json!({}),
1349 json!({"Word": {"bits": {"EStop": 0}, "writable": true, "units": "x", "rate_ms": 30},
1350 "Word.3": {"writable": false}}),
1351 );
1352 let data = json!({"decorator": decorators(), "plc": {"P": {"path": "x", "tags": {
1353 "Word": {"bits": {"EStop": 0}}, "Program:Main.Axis.Pos": {"bits": {"Home": 4}}}}}});
1354 let rules =
1355 Rules::new(&crate::config::parse_config(&data, std::path::Path::new(".")).unwrap())
1356 .unwrap();
1357 assert_eq!(
1358 eff(&r, "Word.0"),
1359 json!({"writable": true, "rate_ms": 30, "group": "tag"})
1360 );
1361 assert_eq!(
1362 eff(&r, "Word.3"),
1363 json!({"writable": false, "rate_ms": 30, "group": "tag"})
1364 );
1365 assert_eq!(rules.resolve("P", "Word.estop"), "Word.0");
1366 assert_eq!(
1367 rules.resolve("P", "Program:Main.Axis.Pos.Home"),
1368 "Program:Main.Axis.Pos.4"
1369 );
1370 }
1371
1372 #[test]
1373 fn rules_are_the_fallback_and_groups_need_no_decorator_rate() {
1374 let r = resolver_of(
1375 json!({"Axis": {"Pos": {"units": "m"}, "Drive": "fast"}}),
1376 json!({}),
1377 json!([{"match": "Axes[*].*", "units": "rule", "max": 7, "description": "r"}]),
1378 json!([{"name": "g", "rate_ms": 40, "match": "Axes[*].*"}]),
1379 true,
1380 );
1381 assert_eq!(
1382 eff(&r, "Axes[0].Pos"),
1383 json!({"units": "m", "max": 7, "description": "r"})
1384 );
1385 assert_eq!(
1386 group_of(&r, "Axes[0].Pos"),
1387 Some(Group {
1388 name: "g".into(),
1389 rate_ms: 40
1390 })
1391 );
1392 assert_eq!(
1393 group_of(&r, "Axes[0].Drive"),
1394 Some(Group {
1395 name: "fast".into(),
1396 rate_ms: 250
1397 })
1398 );
1399 }
1400
1401 #[test]
1402 fn types_unknown_before_connect_and_memo_cleared() {
1403 let data = json!({"decorator": decorators(), "plc": {"P": {"path": "x", "udt": {"Axis": {"Pos": "sp"}},
1404 "tags": {"Zone": "temp"}}}});
1405 let rules =
1406 Rules::new(&crate::config::parse_config(&data, std::path::Path::new(".")).unwrap())
1407 .unwrap();
1408 assert!(rules.effective("P", "Axes[0].Pos").is_empty()); assert_eq!(rules.effective("P", "Zone[1]")["units"], "C"); rules.set_types("P", &tag_list(), &obj(types()));
1411 assert_eq!(rules.effective("P", "Axes[0].Pos")["writable"], true);
1412 let tags: Vec<Json> = tag_list()
1414 .into_iter()
1415 .map(|mut t| {
1416 if t["name"] == "Axes" {
1417 t["type"] = json!("Axis2");
1418 }
1419 t
1420 })
1421 .collect();
1422 let mut types = obj(types());
1423 types.insert("Axis2".into(), types["Axis"].clone());
1424 rules.set_types("P", &tags, &types);
1425 assert!(rules.effective("P", "Axes[0].Pos").is_empty());
1426 }
1427
1428 #[test]
1429 fn always_literal_keys_and_udt_instances() {
1430 let r = resolver_of(
1431 json!({"Pump": {"Alarm": "alarm"}, "Axis": {"Jog": "alarm"}}),
1432 json!({"Word": "alarm", "Line.Axes[1].Jog": {"always": false}}),
1433 json!([{"match": "Zone[2]", "always": true}]),
1434 json!([]),
1435 true,
1436 );
1437 assert_eq!(
1439 r.always(),
1440 [
1441 "Zone[2]",
1442 "Word",
1443 "Line.Pump.Alarm",
1444 "Line.Axes[0].Jog",
1445 "Axes[0].Jog",
1446 "Axes[1].Jog",
1447 "Program:Main.Axis.Jog"
1448 ]
1449 );
1450 let untyped = resolver_of(
1451 json!({"Pump": {"Alarm": "alarm"}}),
1452 json!({}),
1453 json!([]),
1454 json!([]),
1455 false,
1456 );
1457 assert!(untyped.always().is_empty());
1458 }
1459
1460 #[test]
1461 fn always_index_less_key_takes_every_element() {
1462 let tags = json!({"Axes.Jog": "alarm", "Line.Axes.Drive.Faults": "alarm", "Zone": "alarm",
1463 "Axes[1].Pos": "alarm"});
1464 assert_eq!(
1465 with(json!({}), tags).always(),
1466 [
1467 "Axes[0].Jog",
1468 "Axes[1].Jog",
1469 "Line.Axes[0].Drive.Faults",
1470 "Line.Axes[1].Drive.Faults",
1471 "Zone",
1472 "Axes[1].Pos"
1473 ]
1474 );
1475 let untyped = resolver_of(
1477 json!({}),
1478 json!({"Axes.Jog": "alarm"}),
1479 json!([]),
1480 json!([]),
1481 false,
1482 );
1483 assert_eq!(untyped.always(), ["Axes.Jog"]);
1484 }
1485
1486 #[test]
1487 fn config_errors() {
1488 for (plc, error) in [
1489 (
1490 json!({"udt": {"Axis": {"Pos": "nope"}}}),
1491 "plc.P.udt.Axis.Pos: unknown decorator 'nope'",
1492 ),
1493 (
1494 json!({"tags": {"Zone": {"use": ["temp", "nope"]}}}),
1495 "plc.P.tags.Zone: unknown decorator 'nope'",
1496 ),
1497 (
1498 json!({"tags": {"Zone": 3}}),
1499 "plc.P.tags.Zone: expected a decorator name",
1500 ),
1501 (
1502 json!({"tags": {"Zone": {"rate_ms": 0}}}),
1503 "plc.P.tags.Zone: rate_ms",
1504 ),
1505 ] {
1506 let mut p = obj(plc);
1507 p.insert("path".into(), json!("x"));
1508 let data = json!({"decorator": decorators(), "plc": {"P": p}});
1509 let err = crate::config::parse_config(&data, std::path::Path::new(".")).unwrap_err();
1510 assert!(err.contains(error), "{err}");
1511 }
1512 }
1513
1514 #[test]
1515 fn unknown_decorator_names_the_file() {
1516 let dir = crate::testing::TempDir::new();
1517 let path = dir.path().join("t.toml");
1518 std::fs::write(
1519 &path,
1520 "[decorator.d]\nunit = \"x\"\nmin = 1\n[plc.P]\npath = \"x\"\n[plc.P.tags]\nA = \"d\"\nB = \"e\"\n",
1521 )
1522 .unwrap();
1523 let err = crate::config::load_config(&path).unwrap_err();
1524 assert!(
1525 err.contains("t.toml: plc.P.tags.B: unknown decorator 'e'"),
1526 "{err}"
1527 );
1528 std::fs::write(
1530 &path,
1531 "[decorator.d]\nunit = \"x\"\nmin = 1\n[plc.P]\npath = \"x\"\n[plc.P.tags]\nA = \"d\"\n",
1532 )
1533 .unwrap();
1534 let rules = Rules::new(&crate::config::load_config(&path).unwrap()).unwrap();
1535 assert_eq!(Json::Object(rules.effective("P", "A")), json!({"min": 1}));
1536 }
1537
1538 #[test]
1539 fn codegen_uses_the_same_resolution() {
1540 let body = json!({"plc": "PLC1", "version": 1, "tags": tag_list(), "types": types(), "rules": [],
1541 "groups": [], "decorators": decorators(), "udt": {"Axis": {"Jog": "cmd"}},
1542 "tag_settings": {"Word": {"bits": {"EStop": 0}, "use": "cmd"}, "Word.0": {"writable": false}}});
1543 let module = crate::codegen::tree_module(&[body], false).unwrap();
1544 let tags = module.split("export interface PLC1Tags ").nth(1).unwrap();
1545 assert!(tags.contains(" Jog: TagRef<boolean, true>;\n"));
1546 assert!(tags.contains(" Pos: TagRef<number, false>;\n"));
1547 assert!(tags.contains(" Word: IntRef<true> & { EStop: TagRef<boolean, false>; };\n"));
1548 }
1549}