Skip to main content

nichlink/registry_core/declaration/
runtime_checks.rs

1//! Runtime check values and specifications.
2//! 运行期校验的取值与规格。
3
4use super::*;
5
6/// One observation in a provenance chain: which node, object, and operation
7/// produced a value.
8/// 来源链中的一条观测:记录哪个节点、对象与操作产出了某个取值。
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ProvenanceStep {
11    /// Node that performed the operation.
12    /// 执行该操作的节点。
13    pub node: NodeId,
14    /// Object the operation was performed on.
15    /// 该操作所作用的对象。
16    pub object: &'static str,
17    /// Name of the operation that produced the value.
18    /// 产出该取值的操作名称。
19    pub operation: &'static str,
20    /// The observed value rendered as text.
21    /// 以文本形式记录的观测取值。
22    pub value: String,
23}
24
25/// Ordered evidence explaining how a runtime value was produced.
26/// 说明某个运行期取值如何产生的有序证据。
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
28pub struct Provenance {
29    /// Observations in the order they occurred.
30    /// 按发生顺序排列的观测步骤。
31    pub steps: Vec<ProvenanceStep>,
32}
33
34impl Provenance {
35    /// Append one observation and return the extended chain.
36    /// 追加一条观测并返回扩展后的证据链。
37    pub fn push(
38        mut self,
39        node: NodeId,
40        object: &'static str,
41        operation: &'static str,
42        value: impl Into<String>,
43    ) -> Self {
44        self.steps.push(ProvenanceStep {
45            node,
46            object,
47            operation,
48            value: value.into(),
49        });
50        self
51    }
52}
53
54/// A value observed at runtime, tagged with the evidence that produced it.
55/// 运行期观测到的取值,并携带产出它的证据。
56#[derive(Clone, Debug)]
57pub enum RuntimeValue {
58    /// Geometry carrying both its actual and expected coordinate spaces.
59    /// 同时携带实际坐标系与预期坐标系的几何取值。
60    Coordinates(Coordinates),
61    /// A numeric observation plus its provenance.
62    /// 数值观测及其来源。
63    Number {
64        /// The observed number.
65        /// 观测到的数值。
66        value: f64,
67        /// Evidence explaining where the number came from.
68        /// 说明该数值由来的证据。
69        provenance: Provenance,
70    },
71    /// A text observation plus its provenance.
72    /// 文本观测及其来源。
73    Text {
74        /// The observed text.
75        /// 观测到的文本。
76        value: String,
77        /// Evidence explaining where the text came from.
78        /// 说明该文本由来的证据。
79        provenance: Provenance,
80    },
81}
82
83impl RuntimeValue {
84    /// Build a numeric value carrying its provenance.
85    /// 构建携带来源证据的数值。
86    pub fn number(value: f64, provenance: Provenance) -> Self {
87        Self::Number { value, provenance }
88    }
89
90    /// Build a text value carrying its provenance.
91    /// 构建携带来源证据的文本值。
92    pub fn text(value: impl Into<String>, provenance: Provenance) -> Self {
93        Self::Text {
94            value: value.into(),
95            provenance,
96        }
97    }
98
99    /// Borrow the evidence chain shared by every variant.
100    /// 借用各变体共有的证据链。
101    pub fn provenance(&self) -> &Provenance {
102        match self {
103            Self::Coordinates(coordinates) => &coordinates.provenance,
104            Self::Number { provenance, .. } | Self::Text { provenance, .. } => provenance,
105        }
106    }
107
108    /// Stable kind name used in check-failure messages.
109    /// 用于校验失败消息的稳定种类名。
110    pub const fn kind(&self) -> &'static str {
111        match self {
112            Self::Coordinates(_) => "coordinates",
113            Self::Number { .. } => "number",
114            Self::Text { .. } => "text",
115        }
116    }
117}
118
119/// UI coordinates with actual and expected coordinate spaces.
120/// 同时携带实际坐标系和预期坐标系的 UI 坐标。
121#[derive(Clone, Debug)]
122pub struct Coordinates {
123    /// Logical x of the rectangle's left edge.
124    /// 矩形左边缘的逻辑 x 坐标。
125    pub x: f32,
126    /// Logical y of the rectangle's top edge.
127    /// 矩形上边缘的逻辑 y 坐标。
128    pub y: f32,
129    /// Rectangle width in the actual coordinate space.
130    /// 实际坐标系下的矩形宽度。
131    pub width: f32,
132    /// Rectangle height in the actual coordinate space.
133    /// 实际坐标系下的矩形高度。
134    pub height: f32,
135    /// Width of the viewport the rectangle must fit inside.
136    /// 矩形必须落在其中的视口宽度。
137    pub viewport_width: f32,
138    /// Height of the viewport the rectangle must fit inside.
139    /// 矩形必须落在其中的视口高度。
140    pub viewport_height: f32,
141    /// Coordinate space the rectangle was measured in.
142    /// 测量该矩形时所用的坐标系。
143    pub coordinate_space: &'static str,
144    /// Coordinate space the check requires; must match `coordinate_space`.
145    /// 校验要求的坐标系,必须与 `coordinate_space` 相同。
146    pub expected_space: &'static str,
147    /// Evidence explaining where these coordinates came from.
148    /// 说明该坐标由来的证据。
149    pub provenance: Provenance,
150}
151
152impl Coordinates {
153    /// Assemble a coordinate observation for the viewport check to validate.
154    /// 组装坐标观测,交由视口检查校验。
155    #[allow(clippy::too_many_arguments)]
156    pub fn new(
157        x: f32,
158        y: f32,
159        width: f32,
160        height: f32,
161        viewport_width: f32,
162        viewport_height: f32,
163        coordinate_space: &'static str,
164        expected_space: &'static str,
165        provenance: Provenance,
166    ) -> Self {
167        Self {
168            x,
169            y,
170            width,
171            height,
172            viewport_width,
173            viewport_height,
174            coordinate_space,
175            expected_space,
176            provenance,
177        }
178    }
179}
180
181/// Evidence returned when a runtime check rejects a value.
182/// 运行期检查拒绝某个取值时返回的证据。
183#[derive(Clone, Debug)]
184pub struct RuntimeCheckFailure {
185    /// Stable check name that produced the failure.
186    /// 产出该失败的检查稳定名称。
187    pub check: &'static str,
188    /// Human-readable reason the value was rejected.
189    /// 该取值被拒绝的人类可读原因。
190    pub message: String,
191    /// Evidence explaining where the rejected value came from.
192    /// 说明被拒取值由来的证据。
193    pub provenance: Provenance,
194}
195
196/// A named, parameterized check the host runs on produced values.
197/// 宿主对产出取值执行的具名参数化校验。
198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199pub enum RuntimeCheckSpec {
200    /// The rectangle fits inside its viewport and both spaces match.
201    /// 矩形落在视口内,且两个坐标系一致。
202    CoordinatesInViewport,
203    /// The value is a finite number.
204    /// 取值是有限数。
205    FiniteNumber,
206    /// The number lies within an inclusive range; inverted bounds fail.
207    /// 数值落在闭区间内;边界倒置视为失败。
208    NumberInRange {
209        /// Inclusive lower bound.
210        /// 闭区间下界。
211        min: i64,
212        /// Inclusive upper bound.
213        /// 闭区间上界。
214        max: i64,
215    },
216    /// The text is not empty and not whitespace only.
217    /// 文本非空且不全是空白。
218    NonEmptyText,
219    /// The text's character count lies within an inclusive range.
220    /// 文本的字符数落在闭区间内。
221    TextLength {
222        /// Inclusive minimum character count.
223        /// 闭区间的最小字符数。
224        min: usize,
225        /// Inclusive maximum character count.
226        /// 闭区间的最大字符数。
227        max: usize,
228    },
229}
230
231impl RuntimeCheckSpec {
232    /// Stable machine name written into failure evidence.
233    /// 写入失败证据的稳定机器名。
234    pub const fn name(self) -> &'static str {
235        match self {
236            Self::CoordinatesInViewport => "coordinates_in_viewport",
237            Self::FiniteNumber => "finite_number",
238            Self::NumberInRange { .. } => "number_in_range",
239            Self::NonEmptyText => "non_empty_text",
240            Self::TextLength { .. } => "text_length",
241        }
242    }
243
244    /// Rust expression text that reconstructs this check in authored code.
245    /// 在作者代码中重建该检查的 Rust 表达式文本。
246    pub fn expression(self) -> String {
247        match self {
248            Self::CoordinatesInViewport => "crate::COORDINATES_IN_VIEWPORT".to_owned(),
249            Self::FiniteNumber => "crate::FINITE_NUMBER".to_owned(),
250            Self::NumberInRange { min, max } => {
251                format!("crate::RuntimeCheckSpec::number_in_range({min}, {max})")
252            }
253            Self::NonEmptyText => "crate::NON_EMPTY_TEXT".to_owned(),
254            Self::TextLength { min, max } => {
255                format!("crate::RuntimeCheckSpec::text_length({min}, {max})")
256            }
257        }
258    }
259
260    /// Build an inclusive numeric range check.
261    /// 构建闭区间数值校验。
262    pub const fn number_in_range(min: i64, max: i64) -> Self {
263        Self::NumberInRange { min, max }
264    }
265
266    /// Build an inclusive character-length check.
267    /// 构建闭区间字符数校验。
268    pub const fn text_length(min: usize, max: usize) -> Self {
269        Self::TextLength { min, max }
270    }
271
272    /// Parse the compact names written by the authoring form.
273    /// 解析创作表单写入的紧凑检查名称。
274    pub fn parse_list(value: &str) -> Result<Vec<Self>, String> {
275        let value = value.trim().trim_start_matches('[').trim_end_matches(']');
276        if value.is_empty() {
277            return Ok(Vec::new());
278        }
279        split_items(value)
280            .into_iter()
281            .map(|item| Self::parse_one(item.trim()))
282            .collect()
283    }
284
285    fn parse_one(value: &str) -> Result<Self, String> {
286        let value = value.trim();
287        let value = value.strip_prefix("crate::").unwrap_or(value);
288        let value = value
289            .strip_prefix("RuntimeCheckSpec::")
290            .unwrap_or(value)
291            .trim();
292        match value {
293            "COORDINATES_IN_VIEWPORT" | "coordinates_in_viewport" => {
294                return Ok(Self::CoordinatesInViewport);
295            }
296            "FINITE_NUMBER" | "finite_number" => return Ok(Self::FiniteNumber),
297            "NON_EMPTY_TEXT" | "non_empty_text" => return Ok(Self::NonEmptyText),
298            _ => {}
299        }
300        if let Some(arguments) = value
301            .strip_prefix("number_in_range(")
302            .and_then(|rest| rest.strip_suffix(')'))
303        {
304            let mut values = arguments.split(',').map(str::trim);
305            let min = values
306                .next()
307                .ok_or_else(|| "number_in_range requires min and max".to_owned())?
308                .parse::<i64>()
309                .map_err(|_| "number_in_range min must be an integer".to_owned())?;
310            let max = values
311                .next()
312                .ok_or_else(|| "number_in_range requires min and max".to_owned())?
313                .parse::<i64>()
314                .map_err(|_| "number_in_range max must be an integer".to_owned())?;
315            if values.next().is_some() {
316                return Err("number_in_range accepts exactly two integers".to_owned());
317            }
318            return Ok(Self::NumberInRange { min, max });
319        }
320        if let Some(arguments) = value
321            .strip_prefix("text_length(")
322            .and_then(|rest| rest.strip_suffix(')'))
323        {
324            let mut values = arguments.split(',').map(str::trim);
325            let min = values
326                .next()
327                .ok_or_else(|| "text_length requires min and max".to_owned())?
328                .parse::<usize>()
329                .map_err(|_| "text_length min must be an unsigned integer".to_owned())?;
330            let max = values
331                .next()
332                .ok_or_else(|| "text_length requires min and max".to_owned())?
333                .parse::<usize>()
334                .map_err(|_| "text_length max must be an unsigned integer".to_owned())?;
335            if values.next().is_some() {
336                return Err("text_length accepts exactly two integers".to_owned());
337            }
338            return Ok(Self::TextLength { min, max });
339        }
340        Err(format!("unknown runtime check `{value}`"))
341    }
342
343    /// Evaluate the check against one value, reporting failure as evidence.
344    /// 对单个取值执行检查,失败时以证据形式报告。
345    pub fn run(self, value: &RuntimeValue) -> Result<(), RuntimeCheckFailure> {
346        match self {
347            Self::CoordinatesInViewport => check_coordinates(value),
348            Self::FiniteNumber => check_finite_number(value),
349            Self::NumberInRange { min, max } => check_number_range(value, min, max),
350            Self::NonEmptyText => check_non_empty_text(value),
351            Self::TextLength { min, max } => check_text_length(value, min, max),
352        }
353    }
354}
355
356/// The viewport-fitting check, for authoring code to name directly.
357/// 视口适配检查,供创作代码直接引用的常量。
358pub const COORDINATES_IN_VIEWPORT: RuntimeCheckSpec = RuntimeCheckSpec::CoordinatesInViewport;
359
360/// The finite-number check, for authoring code to name directly.
361/// 有限数检查,供创作代码直接引用的常量。
362pub const FINITE_NUMBER: RuntimeCheckSpec = RuntimeCheckSpec::FiniteNumber;
363
364/// The non-empty-text check, for authoring code to name directly.
365/// 非空文本检查,供创作代码直接引用的常量。
366pub const NON_EMPTY_TEXT: RuntimeCheckSpec = RuntimeCheckSpec::NonEmptyText;
367
368fn split_items(value: &str) -> Vec<&str> {
369    let mut items = Vec::new();
370    let mut start = 0;
371    let mut depth = 0usize;
372    for (index, character) in value.char_indices() {
373        match character {
374            '(' => depth += 1,
375            ')' => depth = depth.saturating_sub(1),
376            ',' if depth == 0 => {
377                items.push(value[start..index].trim());
378                start = index + character.len_utf8();
379            }
380            _ => {}
381        }
382    }
383    items.push(value[start..].trim());
384    items
385}
386
387fn wrong_kind(check: &'static str, expected: &str, value: &RuntimeValue) -> RuntimeCheckFailure {
388    RuntimeCheckFailure {
389        check,
390        message: format!("expected {expected} data, received {}", value.kind()),
391        provenance: value.provenance().clone(),
392    }
393}
394
395fn check_coordinates(value: &RuntimeValue) -> Result<(), RuntimeCheckFailure> {
396    let RuntimeValue::Coordinates(coordinates) = value else {
397        return Err(wrong_kind("coordinates_in_viewport", "coordinate", value));
398    };
399    let finite = [
400        coordinates.x,
401        coordinates.y,
402        coordinates.width,
403        coordinates.height,
404        coordinates.viewport_width,
405        coordinates.viewport_height,
406    ]
407    .into_iter()
408    .all(f32::is_finite);
409    if !finite {
410        return Err(RuntimeCheckFailure {
411            check: "coordinates_in_viewport",
412            message: "coordinate contains NaN or infinity".to_owned(),
413            provenance: coordinates.provenance.clone(),
414        });
415    }
416    if coordinates.coordinate_space != coordinates.expected_space {
417        return Err(RuntimeCheckFailure {
418            check: "coordinates_in_viewport",
419            message: format!(
420                "coordinate space `{}` does not match expected `{}`",
421                coordinates.coordinate_space, coordinates.expected_space
422            ),
423            provenance: coordinates.provenance.clone(),
424        });
425    }
426    let inside = coordinates.x >= 0.0
427        && coordinates.y >= 0.0
428        && coordinates.width >= 0.0
429        && coordinates.height >= 0.0
430        && coordinates.x + coordinates.width <= coordinates.viewport_width
431        && coordinates.y + coordinates.height <= coordinates.viewport_height;
432    if inside {
433        Ok(())
434    } else {
435        Err(RuntimeCheckFailure {
436            check: "coordinates_in_viewport",
437            message: format!(
438                "rect ({:.1}, {:.1}, {:.1}, {:.1}) exceeds viewport ({:.1}, {:.1})",
439                coordinates.x,
440                coordinates.y,
441                coordinates.width,
442                coordinates.height,
443                coordinates.viewport_width,
444                coordinates.viewport_height
445            ),
446            provenance: coordinates.provenance.clone(),
447        })
448    }
449}
450
451fn check_finite_number(value: &RuntimeValue) -> Result<(), RuntimeCheckFailure> {
452    let RuntimeValue::Number { value, provenance } = value else {
453        return Err(wrong_kind("finite_number", "number", value));
454    };
455    if value.is_finite() {
456        Ok(())
457    } else {
458        Err(RuntimeCheckFailure {
459            check: "finite_number",
460            message: format!("number `{value}` is NaN or infinite"),
461            provenance: provenance.clone(),
462        })
463    }
464}
465
466fn check_number_range(value: &RuntimeValue, min: i64, max: i64) -> Result<(), RuntimeCheckFailure> {
467    let RuntimeValue::Number { value, provenance } = value else {
468        return Err(wrong_kind("number_in_range", "number", value));
469    };
470    if min > max {
471        return Err(RuntimeCheckFailure {
472            check: "number_in_range",
473            message: format!("invalid check bounds: minimum {min} exceeds maximum {max}"),
474            provenance: provenance.clone(),
475        });
476    }
477    // The observed value is an f64, so a bound f64 cannot represent exactly
478    // cannot be compared against it: `min as f64` rounds, and past 2^53 that
479    // rounding is the whole difference between accepting a number below the
480    // minimum and refusing it. A check whose bounds the runtime cannot state
481    // exactly is an authoring mistake and is reported as one, rather than
482    // answered with a guess. `i128` is the comparison that cannot saturate the
483    // way `as i64` does at the extremes.
484    // 被观测的取值是 f64,因此 f64 无法精确表示的边界也不能拿来与它比较:`min as f64` 会
485    // 舍入,而超过 2^53 之后这次舍入就是"接受一个低于下限的数"与"拒绝它"的全部区别。边界是
486    // 运行期无法精确说出的检查属于创作错误,因此作为错误报告,而不是用猜测作答。用 `i128`
487    // 比较,是因为它在两端都不会像 `as i64` 那样饱和。
488    for bound in [min, max] {
489        if (bound as f64) as i128 != i128::from(bound) {
490            return Err(RuntimeCheckFailure {
491                check: "number_in_range",
492                message: format!(
493                    "invalid check bounds: {bound} cannot be represented exactly as a number"
494                ),
495                provenance: provenance.clone(),
496            });
497        }
498    }
499    if value.is_finite() && *value >= min as f64 && *value <= max as f64 {
500        Ok(())
501    } else {
502        Err(RuntimeCheckFailure {
503            check: "number_in_range",
504            message: format!("number `{value}` is outside inclusive range {min}..={max}"),
505            provenance: provenance.clone(),
506        })
507    }
508}
509
510fn check_non_empty_text(value: &RuntimeValue) -> Result<(), RuntimeCheckFailure> {
511    let RuntimeValue::Text { value, provenance } = value else {
512        return Err(wrong_kind("non_empty_text", "text", value));
513    };
514    if value.trim().is_empty() {
515        Err(RuntimeCheckFailure {
516            check: "non_empty_text",
517            message: "text is empty or whitespace only".to_owned(),
518            provenance: provenance.clone(),
519        })
520    } else {
521        Ok(())
522    }
523}
524
525fn check_text_length(
526    value: &RuntimeValue,
527    min: usize,
528    max: usize,
529) -> Result<(), RuntimeCheckFailure> {
530    let RuntimeValue::Text { value, provenance } = value else {
531        return Err(wrong_kind("text_length", "text", value));
532    };
533    let length = value.chars().count();
534    if min <= max && (min..=max).contains(&length) {
535        Ok(())
536    } else {
537        Err(RuntimeCheckFailure {
538            check: "text_length",
539            message: if min > max {
540                format!("invalid check bounds: minimum {min} exceeds maximum {max}")
541            } else {
542                format!("text length {length} is outside inclusive range {min}..={max}")
543            },
544            provenance: provenance.clone(),
545        })
546    }
547}
548
549#[cfg(test)]
550#[path = "runtime_checks_tests.rs"]
551mod runtime_checks_tests;