Skip to main content

ciplink_server/
rules.rs

1//! Tag settings: decorators attached by UDT member or tag path, then the match-pattern rules and poll groups.
2
3use 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
11/// Settings of a tag path: `writable`, `min`, `max`, `units`, `description`, `deadband`, `always`, `bits`,
12/// `together`, `rate_ms` (with `group`, the name that set it).
13pub 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];
26/// The only fields a struct or array passes down to its members.
27const 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
34/// Python truthiness of a setting.
35pub 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
46/// `X.<n>` as (`X`, `n`): Logix member names never start with a digit.
47pub 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
53/// Replaces each `[...]` of `path` with `f(inside)`.
54fn 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
72/// `[02]` and `[ 2]` address element 2 in ciplink and the mock, so settings must see `[2]`.
73pub 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
83/// A path without its indexes.
84fn strip_indexes(path: &str) -> String {
85    map_indexes(path, |_| Some(String::new()))
86}
87
88/// Canonical indexes, lowercase.
89pub fn normalize(path: &str) -> String {
90    canonical(path).to_lowercase()
91}
92
93/// Case-insensitive; `*` any run of characters (`.` and `[]` included), `?` one, the rest literal.
94pub 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
117/// The `match` of a rule or group table as a list.
118pub 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
130/// The tag path tokens of `rest`: `[...]` groups and names (Python's `\[[^\]]*\]|[^.\[\]]+`).
131fn 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
157/// (`Program:X.`, rest) of a program-scoped path, ("", path) otherwise.
158fn 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
171/// The decorator fields of a table; unknown keys are logged and dropped, a bad rate_ms is an error.
172fn 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
196/// The fields of a `[udt]` or `[tags]` entry: `"name"`, `["name", ...]` or `{use = ..., <fields>}`. Its
197/// decorators in list order (later wins), then its own fields; `rate_ms` comes with `group`, the name that set it.
198fn 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/// A poll rate and the name it is shown under: a poll group, a decorator, or `"tag"`.
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct Group {
243    /// The group's name.
244    pub name: String,
245    /// The group's poll period.
246    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
257/// (name as the controller spells it, lowercase type name, dims).
258type TypeEntry = (String, String, Option<Vec<u64>>);
259
260/// Effective settings of tag paths in one PLC, from the decorators, its `[udt]` and `[tags]` entries, its rules
261/// and groups, and the controller's types (the `/tags` `tags` and `types`; UDT entries need them).
262#[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    /// A resolver without the controller's types; an unknown decorator or a bad entry is an error naming its place.
279    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    /// From a `GET /tags` body, as codegen sees it.
331    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    /// The controller structure (empty: unknown, so only `[tags]` entries and rules apply).
358    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    /// `[(kind, fields set at that node)]` from the base tag to the leaf.
404    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    /// Settings of a tag path: `rate_ms` and `together` from the closest node that sets them, the other fields
462    /// from the path itself (an element also from its array); then the rules, per field the first rule (file
463    /// order) that sets it. A bit keeps no min, max, units, deadband, bits or together; writable falls back to
464    /// its word's.
465    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    /// The tag's poll rate: from a decorator (named after it, `"tag"` when set inline), else the first matching
517    /// poll group; None if neither sets one.
518    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    /// Tags read at tier 1 even with no subscribers: literal rule names and `[tags]` keys, and every instance of a
541    /// UDT member whose entry says always (once the types are known).
542    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    /// The paths an index-less `[tags]` key stands for: every element of each array it passes through (by the
581    /// types; unknown, the key itself). A key with indexes, or one ending at an array, is the path itself.
582    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    /// Paths of every instance of the (udt, member) targets in the controller, array elements expanded.
616    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); // guards recursive types
647        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
699/// Every index of an array of these dims, in row-major order.
700fn 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/// The resolvers of every PLC in a Config, memoized; `set_types` clears the memo.
726#[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    /// The rules of every PLC of the config (already checked by `parse_config`).
737    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    /// The PLC's controller structure, after a connect or a tag table reload.
767    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    /// See [`Resolver::effective`].
780    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    /// A bit name `X.EStop` as `X.<n>` from the bits of X; anything else unchanged.
800    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    /// The tag's poll rate, see [`Resolver::group`].
822    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    /// The PLC's `[[plc.X.group]]` tables.
828    pub fn groups(&self, plc: &str) -> &[Map<String, Json>] {
829        self.groups.get(plc).map_or(&[], Vec::as_slice)
830    }
831
832    /// See [`Resolver::always`].
833    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        // a bit: writable from its word only
932        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        // rules fill what decorators leave unset
936        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    // ---- ports of ciplink-tagserver's tests/test_rules.py
1010
1011    #[test]
1012    fn match_patterns() {
1013        for (pattern, name, expected) in [
1014            ("Bench.SpeedSP", "bench.speedsp", true), // case-insensitive
1015            ("Tank[*].Level", "Tank[3].Level", true), // brackets are literal
1016            ("Tank[*].Level", "Tank3.Level", false),
1017            ("Arr[[]3]", "Arr[3]", false), // the 0.2 fnmatch escape no longer works
1018            ("Bench.*", "Bench.Axis.Position", true), // * crosses dots
1019            ("PLC1/Speed*", "PLC1/SpeedSP", true),
1020            ("Speed?", "Speed1", true),
1021            ("Speed?", "Speed12", false),
1022            ("Speed", "SpeedSP", false), // whole name
1023            ("a.b", "aXb", false),       // . is literal
1024            ("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        // max never applies to a bit
1063        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        // unknown: unchanged, reads missing
1097        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        // first matching group wins
1119        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        // Tank[*].Level is a pattern: logged and left out
1135        assert_eq!(r.always("P"), ["Bench.StatusWord", "Alarm"]);
1136    }
1137
1138    // ---- ports of ciplink-tagserver's tests/test_decorators.py (resolution and config)
1139
1140    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    /// test_decorators.py's `resolver(udt, tags, rules, groups, types=True)`.
1172    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        // later wins
1219        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        // inline wins over use
1229        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        // no [tags] key
1259        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"); // case and index spelling
1264    }
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()); // UDT entries need the types
1409        assert_eq!(rules.effective("P", "Zone[1]")["units"], "C"); // [tags] entries do not
1410        rules.set_types("P", &tag_list(), &obj(types()));
1411        assert_eq!(rules.effective("P", "Axes[0].Pos")["writable"], true);
1412        // a download changed its type
1413        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        // not Line.Axes[1].Jog: its [tags] entry wins
1438        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        // until the types are known
1476        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        // the unknown key 'unit' is logged and ignored
1529        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}