oxedyne/fe2o3/fe2o3_sbj/src/validate.rs
66.6 KiB, 1 run
created by r1870400018:22228, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Schema validation and the limits that are not the decoder's. See `SPEC.md` §4, §5. |
| 2 | //! |
| 3 | //! The decoder has already enforced the depth limit and the tree region size before anything gets |
| 4 | //! here, so what remains is the node schema, the node count, and the rules that only make sense |
| 5 | //! once a tree exists: the root is a `doc`, every kind code is one the schema admits, every field is |
| 6 | //! present and correctly typed, and every child is one its parent admits. |
| 7 | //! |
| 8 | //! The schema is a vocabulary and not a constant. A document (`oxeweb/doc/0`) admits the kinds 1 to |
| 9 | //! 13; the browser's chrome admits those and the `edit` node; an application's tree admits those and |
| 10 | //! the `surface` node as well. One walk validates all three, and which kinds it admits is |
| 11 | //! [`Schema::admits`] and nothing else, so "a document is never a program" is a fact about this |
| 12 | //! function rather than a promise made elsewhere. |
| 13 | //! |
| 14 | //! The style vocabulary is the schema's in exactly the same way ([`Schema::admits_style`]): a |
| 15 | //! document admits the eight properties of §4.4, and a chrome and an application admit more, |
| 16 | //! because the chrome is a real interface and needs to look like one. The browser's own trees are |
| 17 | //! held to this walk like any document, so the chrome is legal because it conforms, and not because |
| 18 | //! nobody checked it. |
| 19 | //! |
| 20 | //! Nodes are numbered by a depth-first, pre-order walk from 0 at the root (`SPEC.md` §4.6), and |
| 21 | //! every rejection names the node, its kind, and the rule it broke (`SPEC.md` §6). |
| 22 | |
| 23 | use crate::{ |
| 24 | kinds::{ |
| 25 | check_address, |
| 26 | check_border, |
| 27 | icon_names_label, |
| 28 | known_icon, |
| 29 | known_style_field, |
| 30 | Content, |
| 31 | Field, |
| 32 | FieldType, |
| 33 | NodeKind, |
| 34 | ReservedKind, |
| 35 | Schema, |
| 36 | StyleCheck, |
| 37 | StyleField, |
| 38 | ALIGNMENTS, |
| 39 | DIRECTIONS, |
| 40 | PALETTE, |
| 41 | KEY_ALT, |
| 42 | KEY_CHILDREN, |
| 43 | KEY_FALLBACK, |
| 44 | KEY_STYLE, |
| 45 | KEY_STYLES, |
| 46 | }, |
| 47 | limit, |
| 48 | }; |
| 49 | |
| 50 | use oxedyne_fe2o3_core::prelude::*; |
| 51 | use oxedyne_fe2o3_jdat::prelude::*; |
| 52 | |
| 53 | /// What a validated tree turned out to contain. |
| 54 | #[derive(Clone, Copy, Debug, Default)] |
| 55 | pub struct Stats { |
| 56 | /// Number of nodes. |
| 57 | pub nodes: usize, |
| 58 | /// Greatest nesting depth reached. The root alone is a depth of 1. |
| 59 | pub depth: usize, |
| 60 | } |
| 61 | |
| 62 | /// Validates a decoded tree against the vocabulary its schema admits, naming the failing node. |
| 63 | /// |
| 64 | /// The walk is iterative rather than recursive, so a tree that arrived by some route other than the |
| 65 | /// decoder, and therefore never met the decoder's depth limit, cannot exhaust the stack here. It |
| 66 | /// walks the tree by reference for the same reason: cloning a node clones every node under it, and |
| 67 | /// the clone a derived implementation makes recurses as deep as the tree goes, which would spend |
| 68 | /// the stack this walk was written to save. |
| 69 | pub fn validate(tree: &Dat, schema: &str) -> Outcome<Stats> { |
| 70 | |
| 71 | let schema = res!(Schema::from_name(schema)); |
| 72 | |
| 73 | let mut stats = Stats::default(); |
| 74 | let mut id: usize = 0; |
| 75 | |
| 76 | // How many surfaces the tree has opened, against the ceiling of §5. A surface is a live |
| 77 | // application instance, and a tree that may open unboundedly many is a tree that exhausts the host |
| 78 | // by being opened. |
| 79 | let mut surfaces: usize = 0; |
| 80 | |
| 81 | // The names the document's style table defines, filled in when the root doc is reached (§4.4). A |
| 82 | // node's `style` field must name one of these, and since the root is popped before any of its |
| 83 | // descendants, the table is known before any style reference is checked. |
| 84 | let mut styles: Vec<String> = Vec::new(); |
| 85 | |
| 86 | // The names some node actually references. A style defined but never used renders identically to |
| 87 | // one that was never defined, so an unreferenced entry would give one document two addresses; it |
| 88 | // is caught after the walk, once every reference is known. |
| 89 | let mut referenced: Vec<String> = Vec::new(); |
| 90 | |
| 91 | // A pending node: the daticle, the kind of its parent, its depth, and whether it sits inside a |
| 92 | // surface's alternative. Children are pushed in reverse, so popping yields the depth-first |
| 93 | // pre-order that numbers the nodes. |
| 94 | // |
| 95 | // The `inert` flag is what makes an alternative inert. It is set on the nodes of an alternative |
| 96 | // and carried down to everything beneath them, so no schema, and no fallback, can put an `edit` or |
| 97 | // a `surface` inside the content that stands in for an application. An alternative that could |
| 98 | // itself hold a surface would be a hole with another hole behind it. |
| 99 | let mut stack: Vec<(&Dat, Option<NodeKind>, usize, bool)> = vec![(tree, None, 1, false)]; |
| 100 | |
| 101 | while let Some((node, parent, depth, inert)) = stack.pop() { |
| 102 | |
| 103 | if stats.nodes == limit::NODES { |
| 104 | return Err(err!( |
| 105 | "Node {}: the tree exceeds the limit of {} nodes.", id, limit::NODES; |
| 106 | Invalid, Input, TooBig, LimitReached)); |
| 107 | } |
| 108 | stats.nodes += 1; |
| 109 | if depth > stats.depth { |
| 110 | stats.depth = depth; |
| 111 | } |
| 112 | |
| 113 | // A node is a usr daticle: a kind code, then a payload. |
| 114 | let (uid, payload_opt) = match node { |
| 115 | Dat::Usr(uid, payload_opt) => (uid, payload_opt), |
| 116 | d => return Err(err!( |
| 117 | "Node {} is a {} daticle; every node is a usr daticle carrying a kind code.", |
| 118 | id, d.kind(); |
| 119 | Invalid, Input)), |
| 120 | }; |
| 121 | |
| 122 | let kind = match NodeKind::from_code(uid.code()) { |
| 123 | Ok(kind) => kind, |
| 124 | Err(_) => { |
| 125 | // SPEC §4.2 and §4.5: a code the reader KNOWS, and the schema does not ADMIT, is |
| 126 | // refused unconditionally. It is checked before the fallback rule below and never |
| 127 | // falls through to it, because the fallback rule is forward compatibility for a code |
| 128 | // this version has never heard of, and this is not that: the reader knows exactly what |
| 129 | // it has been handed. Admitting a reserved kind on a fallback would let an author put |
| 130 | // a surface in a document today, under a fallback that renders innocently, and have |
| 131 | // every reader that later learned what code 15 meant begin honouring it -- a document |
| 132 | // that became a program by waiting. Anyone who later "simplifies" these two paths back |
| 133 | // into one reopens that. |
| 134 | if let Some(reserved) = ReservedKind::from_code(uid.code()) { |
| 135 | res!(check_reserved( |
| 136 | id, reserved, schema, parent, payload_opt, depth, inert, |
| 137 | &mut surfaces, &styles, &mut referenced, &mut stack)); |
| 138 | id += 1; |
| 139 | continue; |
| 140 | } |
| 141 | // SPEC §4.5: a kind outside the vocabulary is permitted only when its payload is a |
| 142 | // map carrying a non-empty `fallback` of known nodes, which stand in for it. The |
| 143 | // root is never one, since it must be a doc. |
| 144 | if id == 0 { |
| 145 | return Err(err!( |
| 146 | "Node 0 declares the unknown kind code {}; the root of an '{}' payload \ |
| 147 | must be a doc.", uid.code(), schema.name(); |
| 148 | Invalid, Input)); |
| 149 | } |
| 150 | res!(push_fallback(id, uid.code(), parent, payload_opt, depth, inert, &mut stack)); |
| 151 | id += 1; |
| 152 | continue; |
| 153 | }, |
| 154 | }; |
| 155 | |
| 156 | // The root of a payload is always a doc, whichever schema it declares. A chrome and an |
| 157 | // application are documents that may say more, not trees of another shape. |
| 158 | if id == 0 && kind != NodeKind::Doc { |
| 159 | return Err(err!( |
| 160 | "Node 0 is a {}; the root of an '{}' payload must be a doc.", |
| 161 | kind.label(), schema.name(); |
| 162 | Invalid, Input)); |
| 163 | } |
| 164 | |
| 165 | // A child appears only where its parent admits it. |
| 166 | if let Some(pkind) = parent { |
| 167 | if !pkind.allows(&kind) { |
| 168 | return Err(err!( |
| 169 | "Node {} is a {} inside a {}, which admits {} content only.", |
| 170 | id, kind.label(), pkind.label(), content_label(pkind.content()); |
| 171 | Invalid, Input)); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | let payload = match payload_opt { |
| 176 | Some(payload) => payload.as_ref(), |
| 177 | None => return Err(err!( |
| 178 | "Node {} ({}) carries no payload; a {} carries {}.", |
| 179 | id, kind.label(), kind.label(), payload_label(kind); |
| 180 | Invalid, Input, Missing)), |
| 181 | }; |
| 182 | |
| 183 | // Every payload is a map, except a text run's, which is the string itself. |
| 184 | if kind.payload_is_str() { |
| 185 | match payload { |
| 186 | Dat::Str(_) => (), |
| 187 | d => return Err(err!( |
| 188 | "Node {} ({}) carries a {} payload; a {} carries a str.", |
| 189 | id, kind.label(), d.kind(), kind.label(); |
| 190 | Invalid, Input)), |
| 191 | } |
| 192 | } else { |
| 193 | match payload { |
| 194 | Dat::Map(map) => { |
| 195 | res!(check_fields( |
| 196 | id, kind.label(), kind.fields(), kind.content(), kind == NodeKind::Doc, map)); |
| 197 | // A heading's level is the one field with a range as well as a width. |
| 198 | if kind == NodeKind::Heading { |
| 199 | res!(check_heading_level(id, map)); |
| 200 | } |
| 201 | // The root doc's style table is validated once, and its names collected so |
| 202 | // that every later `style` reference can be resolved against them (§4.4). The |
| 203 | // schema goes with it: which properties a record may name is the schema's |
| 204 | // business, exactly as which kinds the tree may carry is. |
| 205 | if kind == NodeKind::Doc { |
| 206 | styles = res!(check_styles_table(id, schema, map)); |
| 207 | } |
| 208 | // A node naming a style must name one the table defines (§4.4). |
| 209 | if let Some(sv) = map.get(&dat!(KEY_STYLE)) { |
| 210 | res!(check_style_ref(id, kind.label(), sv, &styles)); |
| 211 | if let Dat::Str(s) = sv { |
| 212 | referenced.push(s.clone()); |
| 213 | } |
| 214 | } |
| 215 | }, |
| 216 | d => return Err(err!( |
| 217 | "Node {} ({}) carries a {} payload; a {} carries a map.", |
| 218 | id, kind.label(), d.kind(), kind.label(); |
| 219 | Invalid, Input)), |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | let kids = match children(payload) { |
| 224 | Ok(kids) => kids, |
| 225 | Err(e) => return Err(err!(e, |
| 226 | "Node {} ({}): its children must be a list.", id, kind.label(); |
| 227 | Invalid, Input)), |
| 228 | }; |
| 229 | // SPEC §4.2: a list is marked `item+` and carries at least one child. A doc and a section are |
| 230 | // `flow*` and may be empty, since a stub with only a title is a legitimate thing to publish. |
| 231 | if kind.requires_child() && kids.is_empty() { |
| 232 | return Err(err!( |
| 233 | "Node {} ({}) carries no children, but a {} must carry at least one.", |
| 234 | id, kind.label(), kind.label(); |
| 235 | Invalid, Input, Missing)); |
| 236 | } |
| 237 | for kid in kids.iter().rev() { |
| 238 | stack.push((kid, Some(kind), depth + 1, inert)); |
| 239 | } |
| 240 | |
| 241 | id += 1; |
| 242 | } |
| 243 | |
| 244 | // Every defined style must be used. An unreferenced entry has no rendering effect, so a document |
| 245 | // carrying it and one without it are the same document at two addresses (§4.4). |
| 246 | for name in &styles { |
| 247 | if !referenced.contains(name) { |
| 248 | return Err(err!( |
| 249 | "Node 0 (doc): style '{}' is defined in the table but no node references it, which \ |
| 250 | would give one document two addresses; remove it (SPEC.md §4.4).", name; |
| 251 | Invalid, Input)); |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | Ok(stats) |
| 256 | } |
| 257 | |
| 258 | /// The children a node payload carries, borrowed rather than cloned. |
| 259 | /// |
| 260 | /// This is [`children_of`](crate::kinds::children_of) without the clone, which matters because a |
| 261 | /// validator walks every node of a tree that arrived from somewhere else. |
| 262 | fn children(payload: &Dat) -> Outcome<&[Dat]> { |
| 263 | match payload { |
| 264 | Dat::Map(map) => match map.get(&dat!(KEY_CHILDREN)) { |
| 265 | None => Ok(&[]), |
| 266 | Some(Dat::List(v)) => Ok(&v[..]), |
| 267 | Some(d) => Err(err!( |
| 268 | "Node children must be a list, found {:?}.", d.kind(); |
| 269 | Invalid, Input)), |
| 270 | }, |
| 271 | _ => Ok(&[]), |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | /// Checks a node's payload map: no unknown keys, every field correctly typed, every required field |
| 276 | /// present. |
| 277 | /// |
| 278 | /// The kind is passed as its label, its field table and its content class rather than as a |
| 279 | /// `NodeKind`, because a reserved kind (§4.2) has a payload schema of its own and is held to it by |
| 280 | /// exactly this routine. One check, and no second implementation to drift from the first. |
| 281 | fn check_fields( |
| 282 | id: usize, |
| 283 | label: &str, |
| 284 | fields: &'static [Field], |
| 285 | content: Content, |
| 286 | table: bool, |
| 287 | map: &DaticleMap, |
| 288 | ) |
| 289 | -> Outcome<()> |
| 290 | { |
| 291 | // Every key present is either a declared field or the children list. |
| 292 | for (k, v) in map.iter() { |
| 293 | let key = match k { |
| 294 | Dat::Str(s) => s.as_str(), |
| 295 | d => return Err(err!( |
| 296 | "Node {} ({}) has a map key of kind {}; every map key is a str.", |
| 297 | id, label, d.kind(); |
| 298 | Invalid, Input)), |
| 299 | }; |
| 300 | if key == KEY_CHILDREN { |
| 301 | if content == Content::None { |
| 302 | return Err(err!( |
| 303 | "Node {} ({}) carries a '{}' field, but a {} admits no children.", |
| 304 | id, label, KEY_CHILDREN, label; |
| 305 | Invalid, Input)); |
| 306 | } |
| 307 | continue; |
| 308 | } |
| 309 | // The universal `style` field (§4.4) is legal on any map payload and is resolved against the |
| 310 | // document's style table elsewhere, so it is never an unknown field. |
| 311 | if key == KEY_STYLE { |
| 312 | continue; |
| 313 | } |
| 314 | // The `styles` table is the doc node's own field (§4.4), validated separately; on any other |
| 315 | // kind it falls through and is rejected as unknown below. |
| 316 | if key == KEY_STYLES && table { |
| 317 | continue; |
| 318 | } |
| 319 | match fields.iter().find(|f| f.name == key) { |
| 320 | Some(f) => res!(check_field(id, label, f, v)), |
| 321 | None => return Err(err!( |
| 322 | "Node {} ({}) carries the unknown field '{}'; a {} declares {}.", |
| 323 | id, label, key, label, field_list(fields); |
| 324 | Invalid, Input)), |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | // Every required field is present. |
| 329 | for f in fields { |
| 330 | if !f.opt && map.get(&dat!(f.name)).is_none() { |
| 331 | return Err(err!( |
| 332 | "Node {} ({}) is missing the required field '{}' of type {}.", |
| 333 | id, label, f.name, type_label(f.typ); |
| 334 | Invalid, Input, Missing)); |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | Ok(()) |
| 339 | } |
| 340 | |
| 341 | /// Checks one field's value against the type its schema declares for it. |
| 342 | fn check_field( |
| 343 | id: usize, |
| 344 | label: &str, |
| 345 | f: &Field, |
| 346 | v: &Dat, |
| 347 | ) |
| 348 | -> Outcome<()> |
| 349 | { |
| 350 | // An address is a typed sub-structure rather than a scalar, so it is checked by its own routine, |
| 351 | // which tells a name from a hash and refuses a malformed target (§4.3). |
| 352 | if f.typ == FieldType::Address { |
| 353 | return match check_address(v) { |
| 354 | Ok(_) => Ok(()), |
| 355 | Err(e) => Err(err!(e, |
| 356 | "Node {} ({}) field '{}' is not a valid link address.", |
| 357 | id, label, f.name; |
| 358 | Invalid, Input)), |
| 359 | }; |
| 360 | } |
| 361 | |
| 362 | // A field carrying nodes is a list the walk will validate, node by node, like any other. What is |
| 363 | // checked here is that it is a list at all, and that it is not empty: a `surface`'s alternative is |
| 364 | // what a screen reader reads, what a search indexes, and what the reader sees when the application |
| 365 | // is not running, so an empty one is a hole in the document with nothing behind it (§4.2). |
| 366 | if f.typ == FieldType::Nodes { |
| 367 | return match v { |
| 368 | Dat::List(list) if !list.is_empty() => Ok(()), |
| 369 | Dat::List(_) => Err(err!( |
| 370 | "Node {} ({}) field '{}' is an empty list; it carries at least one node. An \ |
| 371 | alternative with nothing in it is a hole with nothing behind it (SPEC.md §4.2).", |
| 372 | id, label, f.name; |
| 373 | Invalid, Input, Missing)), |
| 374 | d => Err(err!( |
| 375 | "Node {} ({}) field '{}' is a {} daticle; the schema declares {}.", |
| 376 | id, label, f.name, d.kind(), type_label(f.typ); |
| 377 | Invalid, Input, Mismatch)), |
| 378 | }; |
| 379 | } |
| 380 | |
| 381 | let typed = match (f.typ, v) { |
| 382 | (FieldType::Str, Dat::Str(_)) => true, |
| 383 | (FieldType::U8, Dat::U8(_)) => true, |
| 384 | (FieldType::I8, Dat::I8(_)) => true, |
| 385 | (FieldType::U32, Dat::U32(_)) => true, |
| 386 | (FieldType::Bool, Dat::Bool(_)) => true, |
| 387 | (FieldType::Hash32, Dat::B32(_)) => true, |
| 388 | _ => false, |
| 389 | }; |
| 390 | if !typed { |
| 391 | return Err(err!( |
| 392 | "Node {} ({}) field '{}' is a {} daticle; the schema declares {}.", |
| 393 | id, label, f.name, v.kind(), type_label(f.typ); |
| 394 | Invalid, Input, Mismatch)); |
| 395 | } |
| 396 | |
| 397 | Ok(()) |
| 398 | } |
| 399 | |
| 400 | /// Checks a heading's level, the one field with a range as well as a width: it runs from 1 to 6. |
| 401 | /// |
| 402 | /// The width is already pinned by [`check_field`], so a level that is here at all is a `u8`. |
| 403 | fn check_heading_level( |
| 404 | id: usize, |
| 405 | map: &DaticleMap, |
| 406 | ) |
| 407 | -> Outcome<()> |
| 408 | { |
| 409 | if let Some(Dat::U8(level)) = map.get(&dat!("level")) { |
| 410 | if *level < 1 || *level > 6 { |
| 411 | return Err(err!( |
| 412 | "Node {} (heading) has level {}, which is outside the range 1..=6.", id, level; |
| 413 | Invalid, Input, Range)); |
| 414 | } |
| 415 | } |
| 416 | Ok(()) |
| 417 | } |
| 418 | |
| 419 | /// The schema name of a field type, as an error message spells it. |
| 420 | fn type_label(typ: FieldType) -> &'static str { |
| 421 | match typ { |
| 422 | FieldType::Str => "str", |
| 423 | FieldType::U8 => "u8", |
| 424 | FieldType::I8 => "i8", |
| 425 | FieldType::U32 => "u32", |
| 426 | FieldType::Bool => "bool", |
| 427 | FieldType::Hash32 => "b32", |
| 428 | FieldType::Address => "address", |
| 429 | FieldType::Nodes => "a non-empty list of nodes", |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | /// The name of a content class, as an error message spells it. |
| 434 | fn content_label(content: Content) -> &'static str { |
| 435 | match content { |
| 436 | Content::None => "no", |
| 437 | Content::Flow => "flow", |
| 438 | Content::Inline => "inline", |
| 439 | Content::Items => "item", |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | /// What a kind's payload is, as an error message spells it. |
| 444 | fn payload_label(kind: NodeKind) -> &'static str { |
| 445 | if kind.payload_is_str() { |
| 446 | "a str" |
| 447 | } else { |
| 448 | "a map" |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | /// The fields a kind declares, listed for an error message. |
| 453 | fn field_list(fields: &'static [Field]) -> String { |
| 454 | if fields.is_empty() { |
| 455 | return fmt!("no fields"); |
| 456 | } |
| 457 | let mut s = String::new(); |
| 458 | for (i, f) in fields.iter().enumerate() { |
| 459 | if i > 0 { |
| 460 | s.push_str(", "); |
| 461 | } |
| 462 | s.push_str(&fmt!("'{}': {}{}", |
| 463 | f.name, |
| 464 | type_label(f.typ), |
| 465 | if f.opt { "?" } else { "" }, |
| 466 | )); |
| 467 | } |
| 468 | s |
| 469 | } |
| 470 | |
| 471 | /// Validates a node of a reserved kind (§4.2), and pushes a surface's alternative onto the walk. |
| 472 | /// |
| 473 | /// The refusals here are the security boundary of the whole format, and they are ordered so that the |
| 474 | /// strongest one is reached first. A reserved kind inside an alternative is refused whatever the |
| 475 | /// schema; a reserved kind the schema does not admit is refused whatever it carries; and only then is |
| 476 | /// what it carries looked at all. |
| 477 | fn check_reserved<'a>( |
| 478 | id: usize, |
| 479 | reserved: ReservedKind, |
| 480 | schema: Schema, |
| 481 | parent: Option<NodeKind>, |
| 482 | payload_opt: &'a Option<Box<Dat>>, |
| 483 | depth: usize, |
| 484 | inert: bool, |
| 485 | surfaces: &mut usize, |
| 486 | styles: &[String], |
| 487 | referenced: &mut Vec<String>, |
| 488 | stack: &mut Vec<(&'a Dat, Option<NodeKind>, usize, bool)>, |
| 489 | ) |
| 490 | -> Outcome<()> |
| 491 | { |
| 492 | // An alternative is inert content, always. It stands in for an application that is not running, so |
| 493 | // a live thing inside it would be a hole with another hole behind it, and a surface that could |
| 494 | // nest could open one instance per alternative, per instance, without end. |
| 495 | if inert { |
| 496 | return Err(err!( |
| 497 | "Node {}: a surface's alternative may not carry {} node. The alternative is what a \ |
| 498 | screen reader reads and what the reader sees when the application is not running, so it \ |
| 499 | carries inert content only, whatever the schema '{}' admits elsewhere (SPEC.md §4.2).", |
| 500 | id, reserved.with_article(), schema.name(); |
| 501 | Invalid, Input, Security)); |
| 502 | } |
| 503 | |
| 504 | // SPEC §4.2 and §4.5: a code the reader knows, and the schema does not admit, is refused |
| 505 | // unconditionally, fallback or no fallback. This is not the fallback rule's business and never |
| 506 | // falls through to it. |
| 507 | if !schema.admits(reserved) { |
| 508 | return Err(err!( |
| 509 | "Node {}: the kind code {} is the reserved kind '{}', which is legal in {}. The schema \ |
| 510 | '{}' admits {}, so {} may not carry {} node, whether or not it carries a fallback \ |
| 511 | (SPEC.md §4.2, §4.5).", |
| 512 | id, reserved.code(), reserved.label(), reserved.legal_in(), |
| 513 | schema.name(), schema.admits_label(), schema.tree_label(), reserved.with_article(); |
| 514 | Invalid, Input, Security)); |
| 515 | } |
| 516 | |
| 517 | // Where a reserved kind may sit is its own business (§4.2): a field and a pane are blocks and go |
| 518 | // where a box goes, and an icon is a glyph and goes where a text run goes. The root is never one, |
| 519 | // since the root is always a doc. |
| 520 | let pkind = match parent { |
| 521 | Some(pkind) => pkind, |
| 522 | None => return Err(err!( |
| 523 | "Node 0 is {}; the root of an '{}' payload must be a doc.", |
| 524 | reserved.with_article(), schema.name(); |
| 525 | Invalid, Input)), |
| 526 | }; |
| 527 | if pkind.content() != reserved.sits_in() { |
| 528 | return Err(err!( |
| 529 | "Node {} is {} inside a {}, which admits {} content only. {} is {} content.", |
| 530 | id, reserved.with_article(), pkind.label(), content_label(pkind.content()), |
| 531 | reserved.with_article(), content_label(reserved.sits_in()); |
| 532 | Invalid, Input)); |
| 533 | } |
| 534 | |
| 535 | let payload = match payload_opt { |
| 536 | Some(payload) => payload.as_ref(), |
| 537 | None => return Err(err!( |
| 538 | "Node {} ({}) carries no payload; {} carries a map.", |
| 539 | id, reserved.label(), reserved.with_article(); |
| 540 | Invalid, Input, Missing)), |
| 541 | }; |
| 542 | let map = match payload { |
| 543 | Dat::Map(map) => map, |
| 544 | d => return Err(err!( |
| 545 | "Node {} ({}) carries a {} payload; {} carries a map.", |
| 546 | id, reserved.label(), d.kind(), reserved.with_article(); |
| 547 | Invalid, Input)), |
| 548 | }; |
| 549 | |
| 550 | // Neither reserved kind carries children, and each has its own reason. The generic refusal in |
| 551 | // `check_fields` would catch this too; it is caught here so that the refusal says which reason. |
| 552 | if map.get(&dat!(KEY_CHILDREN)).is_some() { |
| 553 | return Err(err!( |
| 554 | "Node {} ({}) carries a '{}' key, and takes none: {} (SPEC.md §4.2).", |
| 555 | id, reserved.label(), KEY_CHILDREN, reserved.no_children(); |
| 556 | Invalid, Input)); |
| 557 | } |
| 558 | |
| 559 | res!(check_fields( |
| 560 | id, reserved.label(), reserved.fields(), reserved.content(), false, map)); |
| 561 | |
| 562 | // The universal `style` field (§4.4) is legal on a reserved kind's map payload like any other. |
| 563 | if let Some(sv) = map.get(&dat!(KEY_STYLE)) { |
| 564 | res!(check_style_ref(id, reserved.label(), sv, styles)); |
| 565 | if let Dat::Str(s) = sv { |
| 566 | referenced.push(s.clone()); |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | if reserved == ReservedKind::Icon { |
| 571 | // The set is closed (§4.2). An icon names the engine's own drawing rather than carrying any, |
| 572 | // so a name the engine does not draw has nothing faithful to stand in for it, and is a fault |
| 573 | // in whoever built the tree rather than a gap a reader should paper over. |
| 574 | let name = match map.get(&dat!("name")) { |
| 575 | Some(Dat::Str(s)) => s.clone(), |
| 576 | // `check_fields` has already established that the field is present and is a string, so |
| 577 | // this arm is unreachable, and it is written out rather than assumed. |
| 578 | _ => return Err(err!( |
| 579 | "Node {} (icon): the 'name' field is not the string the schema requires, and the \ |
| 580 | field check did not catch it. This is a fault in the validator.", id; |
| 581 | Bug, Invalid)), |
| 582 | }; |
| 583 | if !known_icon(&name) { |
| 584 | return Err(err!( |
| 585 | "Node {} (icon): '{}' is not an icon this engine draws. The set is closed at '{}' \ |
| 586 | (SPEC.md §4.2).", id, name, icon_names_label(); |
| 587 | Invalid, Input, Mismatch)); |
| 588 | } |
| 589 | } |
| 590 | |
| 591 | if reserved == ReservedKind::Surface { |
| 592 | *surfaces += 1; |
| 593 | if *surfaces > limit::SURFACES { |
| 594 | return Err(err!( |
| 595 | "Node {} (surface): the tree carries more than {} surfaces, which is the ceiling \ |
| 596 | (SPEC.md §5). Every surface is a live application instance the host must lay out, \ |
| 597 | budget and present.", id, limit::SURFACES; |
| 598 | Invalid, Input, TooBig, LimitReached)); |
| 599 | } |
| 600 | // The alternative is walked as ordinary content, under the surface's own parent, so its nodes |
| 601 | // meet exactly the child rules that governed where the surface sat, and are counted and |
| 602 | // numbered like any others. They are marked inert: nothing beneath them may come alive. |
| 603 | let alt = match map.get(&dat!(KEY_ALT)) { |
| 604 | Some(Dat::List(alt)) => alt, |
| 605 | // `check_fields` has already established that the field is present and is a non-empty |
| 606 | // list, so this arm is unreachable, and it is written out rather than assumed. |
| 607 | _ => return Err(err!( |
| 608 | "Node {} (surface): the '{}' field is not the non-empty list of nodes the schema \ |
| 609 | requires, and the field check did not catch it. This is a fault in the validator.", |
| 610 | id, KEY_ALT; |
| 611 | Bug, Invalid)), |
| 612 | }; |
| 613 | for kid in alt.iter().rev() { |
| 614 | stack.push((kid, parent, depth + 1, true)); |
| 615 | } |
| 616 | } |
| 617 | |
| 618 | Ok(()) |
| 619 | } |
| 620 | |
| 621 | /// Validates an unknown kind's fallback and pushes its children onto the walk (§4.5). |
| 622 | /// |
| 623 | /// An unknown kind is permitted only when its payload is a map carrying a non-empty `fallback` list, |
| 624 | /// which the reader renders and validates in the unknown node's place. The children are pushed under |
| 625 | /// the unknown node's own parent, so they meet exactly the child rules that governed where the |
| 626 | /// unknown node sat, and they are counted and numbered as ordinary nodes when the walk reaches them. |
| 627 | /// The unknown kind's other fields are not interpreted here. |
| 628 | /// |
| 629 | /// A fallback inside an alternative stays inert, so a kind the reader has never heard of cannot smuggle |
| 630 | /// a live node into the content that stands in for an application. |
| 631 | fn push_fallback<'a>( |
| 632 | id: usize, |
| 633 | code: u16, |
| 634 | parent: Option<NodeKind>, |
| 635 | payload_opt: &'a Option<Box<Dat>>, |
| 636 | depth: usize, |
| 637 | inert: bool, |
| 638 | stack: &mut Vec<(&'a Dat, Option<NodeKind>, usize, bool)>, |
| 639 | ) |
| 640 | -> Outcome<()> |
| 641 | { |
| 642 | let payload = match payload_opt { |
| 643 | Some(payload) => payload.as_ref(), |
| 644 | None => return Err(err!( |
| 645 | "Node {}: the unknown kind code {} carries no payload; an unknown kind is permitted \ |
| 646 | only when its payload is a map carrying a non-empty '{}' (SPEC.md §4.5).", |
| 647 | id, code, KEY_FALLBACK; |
| 648 | Invalid, Input, Missing)), |
| 649 | }; |
| 650 | let map = match payload { |
| 651 | Dat::Map(map) => map, |
| 652 | d => return Err(err!( |
| 653 | "Node {}: the unknown kind code {} carries a {} payload; an unknown kind is permitted \ |
| 654 | only when its payload is a map carrying a non-empty '{}' (SPEC.md §4.5).", |
| 655 | id, code, d.kind(), KEY_FALLBACK; |
| 656 | Invalid, Input)), |
| 657 | }; |
| 658 | let list = match map.get(&dat!(KEY_FALLBACK)) { |
| 659 | Some(Dat::List(list)) if !list.is_empty() => list, |
| 660 | Some(Dat::List(_)) => return Err(err!( |
| 661 | "Node {}: the unknown kind code {} carries an empty '{}'; an unknown kind is permitted \ |
| 662 | only with a non-empty fallback of known nodes (SPEC.md §4.5).", |
| 663 | id, code, KEY_FALLBACK; |
| 664 | Invalid, Input)), |
| 665 | Some(d) => return Err(err!( |
| 666 | "Node {}: the unknown kind code {} carries a '{}' of kind {}; a fallback is a \ |
| 667 | non-empty list of known nodes (SPEC.md §4.5).", |
| 668 | id, code, KEY_FALLBACK, d.kind(); |
| 669 | Invalid, Input)), |
| 670 | None => return Err(err!( |
| 671 | "Node {}: the unknown kind code {} carries no '{}'; an unknown kind is permitted only \ |
| 672 | when its payload is a map carrying a non-empty fallback of known nodes (SPEC.md §4.5).", |
| 673 | id, code, KEY_FALLBACK; |
| 674 | Invalid, Input, Missing)), |
| 675 | }; |
| 676 | for child in list.iter().rev() { |
| 677 | stack.push((child, parent, depth + 1, inert)); |
| 678 | } |
| 679 | Ok(()) |
| 680 | } |
| 681 | |
| 682 | /// Validates the doc node's style table, returning the style names it defines (§4.4). |
| 683 | /// |
| 684 | /// The table is a map from style name to style record. Each key must be a str, and each value a |
| 685 | /// record whose properties are all known and correctly valued. An absent table is simply an empty |
| 686 | /// set of names. |
| 687 | fn check_styles_table( |
| 688 | id: usize, |
| 689 | schema: Schema, |
| 690 | map: &DaticleMap, |
| 691 | ) |
| 692 | -> Outcome<Vec<String>> |
| 693 | { |
| 694 | let table = match map.get(&dat!(KEY_STYLES)) { |
| 695 | None => return Ok(Vec::new()), |
| 696 | Some(Dat::Map(table)) => table, |
| 697 | Some(d) => return Err(err!( |
| 698 | "Node {} (doc): the '{}' style table is a map from name to style record, found a {} \ |
| 699 | (SPEC.md §4.4).", id, KEY_STYLES, d.kind(); |
| 700 | Invalid, Input)), |
| 701 | }; |
| 702 | let mut names = Vec::with_capacity(table.len()); |
| 703 | for (k, v) in table.iter() { |
| 704 | let name = match k { |
| 705 | Dat::Str(s) => s.as_str(), |
| 706 | d => return Err(err!( |
| 707 | "Node {} (doc): the '{}' table has a key of kind {}; a style name is a str \ |
| 708 | (SPEC.md §4.4).", id, KEY_STYLES, d.kind(); |
| 709 | Invalid, Input)), |
| 710 | }; |
| 711 | let record = match v { |
| 712 | Dat::Map(record) => record, |
| 713 | d => return Err(err!( |
| 714 | "Node {} (doc): style '{}' is a {}; a style record is a map of style properties \ |
| 715 | (SPEC.md §4.4).", id, name, d.kind(); |
| 716 | Invalid, Input)), |
| 717 | }; |
| 718 | res!(check_style_record(id, schema, name, record)); |
| 719 | names.push(name.to_string()); |
| 720 | } |
| 721 | Ok(names) |
| 722 | } |
| 723 | |
| 724 | /// Validates one style record: every property the schema admits, every value satisfying its check |
| 725 | /// (§4.4). |
| 726 | /// |
| 727 | /// A property this schema does not admit and a property that does not exist are refused by the same |
| 728 | /// rule and named by different messages, because they are different mistakes. "Unknown style property |
| 729 | /// 'grid'" told to an author whose chrome draws a grid every day would be a lie, so the refusal says |
| 730 | /// which schema turned it down and where it would have been legal. |
| 731 | fn check_style_record( |
| 732 | id: usize, |
| 733 | schema: Schema, |
| 734 | name: &str, |
| 735 | record: &DaticleMap, |
| 736 | ) |
| 737 | -> Outcome<()> |
| 738 | { |
| 739 | for (k, v) in record.iter() { |
| 740 | let prop = match k { |
| 741 | Dat::Str(s) => s.as_str(), |
| 742 | d => return Err(err!( |
| 743 | "Node {} (doc): style '{}' has a property key of kind {}; a style property name \ |
| 744 | is a str (SPEC.md §4.4).", id, name, d.kind(); |
| 745 | Invalid, Input)), |
| 746 | }; |
| 747 | let field = match schema.style_field(prop) { |
| 748 | Some(field) => field, |
| 749 | // The property is a real one, and this schema does not admit it. A document naming an |
| 750 | // interface property is refused here, and this is the whole of the reason: the v0 document |
| 751 | // vocabulary is frozen at eight properties, so a document cannot dress as an interface, and |
| 752 | // cannot grow into one by waiting for a reader that draws more. |
| 753 | None => match known_style_field(prop) { |
| 754 | Some(known) => return Err(err!( |
| 755 | "Node {} (doc): style '{}' names the style property '{}', which is legal in {}. \ |
| 756 | The schema '{}' admits {}, so {} may not name it (SPEC.md §4.4).", |
| 757 | id, name, prop, known.scope.legal_in(), |
| 758 | schema.name(), schema.style_admits_label(), schema.tree_label(); |
| 759 | Invalid, Input)), |
| 760 | None => return Err(err!( |
| 761 | "Node {} (doc): style '{}' carries the unknown style property '{}' (SPEC.md \ |
| 762 | §4.4).", id, name, prop; |
| 763 | Invalid, Input)), |
| 764 | }, |
| 765 | }; |
| 766 | res!(check_style_value(id, name, field, v)); |
| 767 | } |
| 768 | |
| 769 | // `grid` and `pack` are two layouts, and a style naming both has asked for both. A renderer must |
| 770 | // then pick one, and which it picks is a fact about that renderer rather than about the tree -- so |
| 771 | // two readers could lay the same legal chrome out differently and both be right, which is the |
| 772 | // thing a frozen format exists to prevent. Refusing the pair keeps the tree's meaning the tree's. |
| 773 | // |
| 774 | // This is the only rule here that reads a style record as a WHOLE rather than a property at a |
| 775 | // time, because it is the only one about a combination. |
| 776 | if record.contains_key(&dat!("grid")) && record.contains_key(&dat!("pack")) { |
| 777 | return Err(err!( |
| 778 | "Node {} (doc): style '{}' names both 'grid' and 'pack'. A grid shares its width out \ |
| 779 | among its tiles and a packed row leaves each at the width it names, so a style naming \ |
| 780 | both has asked for two different layouts and no reader can honour it (SPEC.md §4.4).", |
| 781 | id, name; |
| 782 | Invalid, Input)); |
| 783 | } |
| 784 | Ok(()) |
| 785 | } |
| 786 | |
| 787 | /// Validates one style-record value against the enum or width its property declares (§4.4). |
| 788 | fn check_style_value( |
| 789 | id: usize, |
| 790 | name: &str, |
| 791 | field: &StyleField, |
| 792 | v: &Dat, |
| 793 | ) |
| 794 | -> Outcome<()> |
| 795 | { |
| 796 | // A border is a typed sub-structure rather than a scalar, so it is checked by its own routine, |
| 797 | // exactly as a link address is (§4.3), and a malformed one is refused here rather than misread by |
| 798 | // the renderer. |
| 799 | if field.check == StyleCheck::Border { |
| 800 | return match check_border(v) { |
| 801 | Ok(_) => Ok(()), |
| 802 | Err(e) => Err(err!(e, |
| 803 | "Node {} (doc): style '{}' property '{}' is not a valid border (SPEC.md §4.4).", |
| 804 | id, name, field.name; |
| 805 | Invalid, Input)), |
| 806 | }; |
| 807 | } |
| 808 | |
| 809 | let ok = match field.check { |
| 810 | StyleCheck::Palette => matches!(v, Dat::Str(s) if PALETTE.contains(&s.as_str())), |
| 811 | StyleCheck::Direction => matches!(v, Dat::Str(s) if DIRECTIONS.contains(&s.as_str())), |
| 812 | StyleCheck::Alignment => matches!(v, Dat::Str(s) if ALIGNMENTS.contains(&s.as_str())), |
| 813 | StyleCheck::ScaleStep => matches!(v, Dat::I8(_)), |
| 814 | StyleCheck::Spacing => matches!(v, Dat::U8(_)), |
| 815 | StyleCheck::Lang => matches!(v, Dat::Str(_)), |
| 816 | StyleCheck::Tile => matches!(v, Dat::U16(_)), |
| 817 | StyleCheck::Share => matches!(v, Dat::U8(_)), |
| 818 | StyleCheck::Elevation => matches!(v, Dat::U8(_)), |
| 819 | // Checked above, and written out rather than caught by a wildcard, so that a check added later |
| 820 | // and forgotten here is a compile error and not an accepted value. |
| 821 | StyleCheck::Border => false, |
| 822 | }; |
| 823 | if !ok { |
| 824 | return Err(err!( |
| 825 | "Node {} (doc): style '{}' property '{}' carries {}; the schema requires {} \ |
| 826 | (SPEC.md §4.4).", |
| 827 | id, name, field.name, style_value_label(v), style_check_label(field.check); |
| 828 | Invalid, Input)); |
| 829 | } |
| 830 | Ok(()) |
| 831 | } |
| 832 | |
| 833 | /// A style reference names a str that resolves to an entry in the document's style table (§4.4). |
| 834 | fn check_style_ref( |
| 835 | id: usize, |
| 836 | label: &str, |
| 837 | sv: &Dat, |
| 838 | styles: &[String], |
| 839 | ) |
| 840 | -> Outcome<()> |
| 841 | { |
| 842 | let name = match sv { |
| 843 | Dat::Str(s) => s.as_str(), |
| 844 | d => return Err(err!( |
| 845 | "Node {} ({}): the '{}' field is a {}; a style reference is a str naming an entry in \ |
| 846 | the document's style table (SPEC.md §4.4).", |
| 847 | id, label, KEY_STYLE, d.kind(); |
| 848 | Invalid, Input)), |
| 849 | }; |
| 850 | if !styles.iter().any(|n| n == name) { |
| 851 | return Err(err!( |
| 852 | "Node {} ({}): the '{}' field names the style '{}', which the document's style table \ |
| 853 | does not define (SPEC.md §4.4).", |
| 854 | id, label, KEY_STYLE, name; |
| 855 | Invalid, Input)); |
| 856 | } |
| 857 | Ok(()) |
| 858 | } |
| 859 | |
| 860 | /// Describes a style value for an error message, spelling out a string or scalar and naming a kind. |
| 861 | fn style_value_label(v: &Dat) -> String { |
| 862 | match v { |
| 863 | Dat::Str(s) => fmt!("the string \"{}\"", s), |
| 864 | Dat::U8(n) => fmt!("the u8 {}", n), |
| 865 | Dat::I8(n) => fmt!("the i8 {}", n), |
| 866 | other => fmt!("a {} daticle", other.kind()), |
| 867 | } |
| 868 | } |
| 869 | |
| 870 | /// Names what a style check permits, for an error message. |
| 871 | fn style_check_label(check: StyleCheck) -> String { |
| 872 | match check { |
| 873 | StyleCheck::Palette => fmt!("a palette name ({})", PALETTE.join(", ")), |
| 874 | StyleCheck::Direction => fmt!("a direction ({})", DIRECTIONS.join(", ")), |
| 875 | StyleCheck::Alignment => fmt!("an alignment ({})", ALIGNMENTS.join(", ")), |
| 876 | StyleCheck::ScaleStep => fmt!("a type scale step (i8)"), |
| 877 | StyleCheck::Spacing => fmt!("a spacing index (u8)"), |
| 878 | StyleCheck::Lang => fmt!("a language tag (str)"), |
| 879 | StyleCheck::Tile => fmt!("a grid tile width as a percentage of a base size (u16)"), |
| 880 | StyleCheck::Share => fmt!("a share of a packed row's leftover room (u8)"), |
| 881 | StyleCheck::Elevation => fmt!("an elevation, in whole steps off the surface behind (u8)"), |
| 882 | StyleCheck::Border => fmt!("a palette name and a width in pixels ([str, u8])"), |
| 883 | } |
| 884 | } |
| 885 | |
| 886 | #[cfg(test)] |
| 887 | mod tests { |
| 888 | use super::*; |
| 889 | use crate::{ |
| 890 | SCHEMA_APP, |
| 891 | SCHEMA_CHROME, |
| 892 | SCHEMA_DOC, |
| 893 | }; |
| 894 | |
| 895 | use oxedyne_fe2o3_jdat::usr::UsrKindId; |
| 896 | |
| 897 | /// Builds a node of the given kind with the given payload. |
| 898 | fn node(kind: NodeKind, payload: Dat) -> Dat { |
| 899 | Dat::Usr( |
| 900 | UsrKindId::new(kind.code(), Some(kind.label()), None), |
| 901 | Some(Box::new(payload)), |
| 902 | ) |
| 903 | } |
| 904 | |
| 905 | /// Builds a text run. |
| 906 | fn text(s: &str) -> Dat { |
| 907 | node(NodeKind::Text, dat!(s)) |
| 908 | } |
| 909 | |
| 910 | /// Builds a 32-byte content hash, the width of a v0 hash reference. |
| 911 | fn hash32() -> Dat { |
| 912 | Dat::B32(B32([0x9fu8; 32])) |
| 913 | } |
| 914 | |
| 915 | /// A style may name a grid or a packed row, and not both. |
| 916 | /// |
| 917 | /// The two are different layouts of the same children, so a style naming both leaves the reader to |
| 918 | /// choose, and a legal tree whose appearance depends on which reader drew it is the thing a frozen |
| 919 | /// format exists to prevent. Each ALONE is legal chrome, which is what says the pair is refused for |
| 920 | /// being a pair rather than either being wrong. |
| 921 | #[test] |
| 922 | fn test_a_style_naming_both_a_grid_and_a_packed_row_is_refused_33() -> Outcome<()> { |
| 923 | let tree = |props: Dat| -> Dat { |
| 924 | node(NodeKind::Doc, mapdat!{ |
| 925 | "title" => dat!("A Title"), |
| 926 | "lang" => dat!("en"), |
| 927 | "styles" => mapdat!{ "shelf" => props }, |
| 928 | "children" => Dat::List(vec![ |
| 929 | node(NodeKind::Boxx, mapdat!{ |
| 930 | "style" => dat!("shelf"), |
| 931 | "children" => Dat::List(vec![ |
| 932 | node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![text("a")]) }), |
| 933 | ]), |
| 934 | }), |
| 935 | ]), |
| 936 | }) |
| 937 | }; |
| 938 | // Either alone is legal chrome. |
| 939 | res!(validate(&tree(mapdat!{ "grid" => dat!(1400u16) }), SCHEMA_CHROME)); |
| 940 | res!(validate(&tree(mapdat!{ "pack" => dat!(600u16) }), SCHEMA_CHROME)); |
| 941 | |
| 942 | // Both together is not, in any schema that admits them at all. |
| 943 | for schema in [SCHEMA_CHROME, SCHEMA_APP] { |
| 944 | assert!( |
| 945 | validate(&tree(mapdat!{ "grid" => dat!(1400u16), "pack" => dat!(600u16) }), schema).is_err(), |
| 946 | "a style naming both a grid and a packed row is refused in {}", schema, |
| 947 | ); |
| 948 | } |
| 949 | // And a document may name neither, pair or no pair. |
| 950 | assert!( |
| 951 | validate(&tree(mapdat!{ "pack" => dat!(600u16) }), SCHEMA_DOC).is_err(), |
| 952 | "a document may not name a packed row: it is an interface property", |
| 953 | ); |
| 954 | Ok(()) |
| 955 | } |
| 956 | |
| 957 | /// Builds a document whose children are the given flow nodes. |
| 958 | fn doc(kids: Vec<Dat>) -> Dat { |
| 959 | node(NodeKind::Doc, mapdat!{ |
| 960 | "title" => dat!("A Title"), |
| 961 | "lang" => dat!("en"), |
| 962 | "children" => Dat::List(kids), |
| 963 | }) |
| 964 | } |
| 965 | |
| 966 | /// A document using every v0 node kind at least once. |
| 967 | fn every_kind() -> Dat { |
| 968 | let heading = node(NodeKind::Heading, mapdat!{ |
| 969 | "level" => dat!(2u8), |
| 970 | "children" => Dat::List(vec![text("Style without a cascade")]), |
| 971 | }); |
| 972 | let para = node(NodeKind::Para, mapdat!{ |
| 973 | "children" => Dat::List(vec![ |
| 974 | text("A run, "), |
| 975 | node(NodeKind::Emph, mapdat!{ |
| 976 | "strong" => dat!(true), |
| 977 | "children" => Dat::List(vec![text("emphasised")]), |
| 978 | }), |
| 979 | node(NodeKind::Link, mapdat!{ |
| 980 | "to" => mapdat!{ "name" => dat!("news.cricket") }, |
| 981 | "children" => Dat::List(vec![text("a link")]), |
| 982 | }), |
| 983 | ]), |
| 984 | }); |
| 985 | let list = node(NodeKind::List, mapdat!{ |
| 986 | "ordered" => dat!(false), |
| 987 | "children" => Dat::List(vec![ |
| 988 | node(NodeKind::Item, mapdat!{ |
| 989 | "children" => Dat::List(vec![ |
| 990 | node(NodeKind::Para, mapdat!{ |
| 991 | "children" => Dat::List(vec![text("An item")]), |
| 992 | }), |
| 993 | ]), |
| 994 | }), |
| 995 | ]), |
| 996 | }); |
| 997 | let image = node(NodeKind::Image, mapdat!{ |
| 998 | "hash" => hash32(), |
| 999 | "alt" => dat!("A diagram of the tree"), |
| 1000 | "w" => dat!(640u32), |
| 1001 | "h" => dat!(480u32), |
| 1002 | }); |
| 1003 | let boxx = node(NodeKind::Boxx, mapdat!{ |
| 1004 | "style" => dat!("note"), |
| 1005 | "children" => Dat::List(vec![image]), |
| 1006 | }); |
| 1007 | let section = node(NodeKind::Section, mapdat!{ |
| 1008 | "title" => dat!("A Section"), |
| 1009 | "children" => Dat::List(vec![heading, para, list, boxx]), |
| 1010 | }); |
| 1011 | // The doc carries a style table so the box's "note" style resolves (§4.4). |
| 1012 | node(NodeKind::Doc, mapdat!{ |
| 1013 | "title" => dat!("A Title"), |
| 1014 | "lang" => dat!("en"), |
| 1015 | "styles" => mapdat!{ |
| 1016 | "note" => mapdat!{ "bg" => dat!("muted"), "pad" => dat!(3u8) }, |
| 1017 | }, |
| 1018 | "children" => Dat::List(vec![section]), |
| 1019 | }) |
| 1020 | } |
| 1021 | |
| 1022 | #[test] |
| 1023 | fn test_valid_every_kind_00() -> Outcome<()> { |
| 1024 | let stats = res!(validate(&every_kind(), SCHEMA_DOC)); |
| 1025 | // doc, section, heading, text, para, text, emph, text, link, text, list, item, para, text, |
| 1026 | // box, image. |
| 1027 | assert_eq!(stats.nodes, 16); |
| 1028 | // The deepest run is doc, section, list, item, para, text. |
| 1029 | assert_eq!(stats.depth, 6); |
| 1030 | Ok(()) |
| 1031 | } |
| 1032 | |
| 1033 | #[test] |
| 1034 | fn test_wrong_schema_01() -> Outcome<()> { |
| 1035 | let e = match validate(&every_kind(), "oxeweb/doc/1") { |
| 1036 | Ok(_) => return Err(err!("A foreign schema was accepted."; Test)), |
| 1037 | Err(e) => fmt!("{}", e), |
| 1038 | }; |
| 1039 | assert!(e.contains("oxeweb/doc/1"), "Error must name the schema it got: {}", e); |
| 1040 | Ok(()) |
| 1041 | } |
| 1042 | |
| 1043 | #[test] |
| 1044 | fn test_para_in_para_02() -> Outcome<()> { |
| 1045 | let inner = node(NodeKind::Para, mapdat!{ |
| 1046 | "children" => Dat::List(vec![text("Inner")]), |
| 1047 | }); |
| 1048 | let outer = node(NodeKind::Para, mapdat!{ |
| 1049 | "children" => Dat::List(vec![inner]), |
| 1050 | }); |
| 1051 | let e = match validate(&doc(vec![outer]), SCHEMA_DOC) { |
| 1052 | Ok(_) => return Err(err!("A para inside a para was accepted."; Test)), |
| 1053 | Err(e) => fmt!("{}", e), |
| 1054 | }; |
| 1055 | assert!(e.contains("Node 2"), "Error must name node 2: {}", e); |
| 1056 | assert!(e.contains("para"), "Error must name the kind: {}", e); |
| 1057 | assert!(e.contains("inline"), "Error must name the rule broken: {}", e); |
| 1058 | Ok(()) |
| 1059 | } |
| 1060 | |
| 1061 | #[test] |
| 1062 | fn test_unknown_kind_code_03() -> Outcome<()> { |
| 1063 | let alien = Dat::Usr( |
| 1064 | UsrKindId::new(99, None, None), |
| 1065 | Some(Box::new(mapdat!{})), |
| 1066 | ); |
| 1067 | let e = match validate(&doc(vec![alien]), SCHEMA_DOC) { |
| 1068 | Ok(_) => return Err(err!("An unknown kind code was accepted."; Test)), |
| 1069 | Err(e) => fmt!("{}", e), |
| 1070 | }; |
| 1071 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1072 | assert!(e.contains("99"), "Error must name the code: {}", e); |
| 1073 | Ok(()) |
| 1074 | } |
| 1075 | |
| 1076 | #[test] |
| 1077 | fn test_missing_required_field_04() -> Outcome<()> { |
| 1078 | // A list without its ordered field. |
| 1079 | let list = node(NodeKind::List, mapdat!{ |
| 1080 | "children" => Dat::List(vec![ |
| 1081 | node(NodeKind::Item, mapdat!{}), |
| 1082 | ]), |
| 1083 | }); |
| 1084 | let e = match validate(&doc(vec![list]), SCHEMA_DOC) { |
| 1085 | Ok(_) => return Err(err!("A list without 'ordered' was accepted."; Test)), |
| 1086 | Err(e) => fmt!("{}", e), |
| 1087 | }; |
| 1088 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1089 | assert!(e.contains("ordered"), "Error must name the field: {}", e); |
| 1090 | Ok(()) |
| 1091 | } |
| 1092 | |
| 1093 | #[test] |
| 1094 | fn test_unknown_field_05() -> Outcome<()> { |
| 1095 | let para = node(NodeKind::Para, mapdat!{ |
| 1096 | "align" => dat!("centre"), |
| 1097 | "children" => Dat::List(vec![text("Text")]), |
| 1098 | }); |
| 1099 | let e = match validate(&doc(vec![para]), SCHEMA_DOC) { |
| 1100 | Ok(_) => return Err(err!("An unknown field was accepted."; Test)), |
| 1101 | Err(e) => fmt!("{}", e), |
| 1102 | }; |
| 1103 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1104 | assert!(e.contains("align"), "Error must name the field: {}", e); |
| 1105 | Ok(()) |
| 1106 | } |
| 1107 | |
| 1108 | #[test] |
| 1109 | fn test_wrong_typed_field_06() -> Outcome<()> { |
| 1110 | // A heading level as an i32, not the declared u8: two encodings, two addresses. |
| 1111 | let heading = node(NodeKind::Heading, mapdat!{ |
| 1112 | "level" => dat!(2i32), |
| 1113 | "children" => Dat::List(vec![text("Heading")]), |
| 1114 | }); |
| 1115 | let e = match validate(&doc(vec![heading]), SCHEMA_DOC) { |
| 1116 | Ok(_) => return Err(err!("A wrongly typed field was accepted."; Test)), |
| 1117 | Err(e) => fmt!("{}", e), |
| 1118 | }; |
| 1119 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1120 | assert!(e.contains("level"), "Error must name the field: {}", e); |
| 1121 | assert!(e.contains("u8"), "Error must name the declared type: {}", e); |
| 1122 | Ok(()) |
| 1123 | } |
| 1124 | |
| 1125 | #[test] |
| 1126 | fn test_heading_level_zero_07() -> Outcome<()> { |
| 1127 | let heading = node(NodeKind::Heading, mapdat!{ |
| 1128 | "level" => dat!(0u8), |
| 1129 | "children" => Dat::List(vec![text("Heading")]), |
| 1130 | }); |
| 1131 | let e = match validate(&doc(vec![heading]), SCHEMA_DOC) { |
| 1132 | Ok(_) => return Err(err!("A heading of level 0 was accepted."; Test)), |
| 1133 | Err(e) => fmt!("{}", e), |
| 1134 | }; |
| 1135 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1136 | assert!(e.contains("1..=6"), "Error must name the range: {}", e); |
| 1137 | Ok(()) |
| 1138 | } |
| 1139 | |
| 1140 | #[test] |
| 1141 | fn test_heading_level_seven_08() -> Outcome<()> { |
| 1142 | let heading = node(NodeKind::Heading, mapdat!{ |
| 1143 | "level" => dat!(7u8), |
| 1144 | "children" => Dat::List(vec![text("Heading")]), |
| 1145 | }); |
| 1146 | let e = match validate(&doc(vec![heading]), SCHEMA_DOC) { |
| 1147 | Ok(_) => return Err(err!("A heading of level 7 was accepted."; Test)), |
| 1148 | Err(e) => fmt!("{}", e), |
| 1149 | }; |
| 1150 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1151 | assert!(e.contains("1..=6"), "Error must name the range: {}", e); |
| 1152 | Ok(()) |
| 1153 | } |
| 1154 | |
| 1155 | #[test] |
| 1156 | fn test_heading_levels_one_to_six_09() -> Outcome<()> { |
| 1157 | for level in 1u8..=6 { |
| 1158 | let heading = node(NodeKind::Heading, mapdat!{ |
| 1159 | "level" => dat!(level), |
| 1160 | "children" => Dat::List(vec![text("Heading")]), |
| 1161 | }); |
| 1162 | let stats = res!(validate(&doc(vec![heading]), SCHEMA_DOC)); |
| 1163 | assert_eq!(stats.nodes, 3); |
| 1164 | } |
| 1165 | Ok(()) |
| 1166 | } |
| 1167 | |
| 1168 | #[test] |
| 1169 | fn test_missing_alt_10() -> Outcome<()> { |
| 1170 | let image = node(NodeKind::Image, mapdat!{ |
| 1171 | "hash" => hash32(), |
| 1172 | }); |
| 1173 | let e = match validate(&doc(vec![image]), SCHEMA_DOC) { |
| 1174 | Ok(_) => return Err(err!("An image without alt text was accepted."; Test)), |
| 1175 | Err(e) => fmt!("{}", e), |
| 1176 | }; |
| 1177 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1178 | assert!(e.contains("alt"), "Error must name the field: {}", e); |
| 1179 | Ok(()) |
| 1180 | } |
| 1181 | |
| 1182 | #[test] |
| 1183 | fn test_node_count_over_limit_11() -> Outcome<()> { |
| 1184 | // One doc plus enough paragraphs to pass the limit. |
| 1185 | let mut kids = Vec::new(); |
| 1186 | for _ in 0..limit::NODES { |
| 1187 | kids.push(node(NodeKind::Para, mapdat!{})); |
| 1188 | } |
| 1189 | let e = match validate(&doc(kids), SCHEMA_DOC) { |
| 1190 | Ok(_) => return Err(err!("A tree over the node limit was accepted."; Test)), |
| 1191 | Err(e) => fmt!("{}", e), |
| 1192 | }; |
| 1193 | assert!(e.contains(&fmt!("{}", limit::NODES)), "Error must name the limit: {}", e); |
| 1194 | Ok(()) |
| 1195 | } |
| 1196 | |
| 1197 | #[test] |
| 1198 | fn test_node_count_at_limit_12() -> Outcome<()> { |
| 1199 | let mut kids = Vec::new(); |
| 1200 | for _ in 0..(limit::NODES - 1) { |
| 1201 | kids.push(node(NodeKind::Para, mapdat!{})); |
| 1202 | } |
| 1203 | let stats = res!(validate(&doc(kids), SCHEMA_DOC)); |
| 1204 | assert_eq!(stats.nodes, limit::NODES); |
| 1205 | Ok(()) |
| 1206 | } |
| 1207 | |
| 1208 | #[test] |
| 1209 | fn test_non_doc_root_13() -> Outcome<()> { |
| 1210 | let root = node(NodeKind::Para, mapdat!{ |
| 1211 | "children" => Dat::List(vec![text("Not a document")]), |
| 1212 | }); |
| 1213 | let e = match validate(&root, SCHEMA_DOC) { |
| 1214 | Ok(_) => return Err(err!("A para as root was accepted."; Test)), |
| 1215 | Err(e) => fmt!("{}", e), |
| 1216 | }; |
| 1217 | assert!(e.contains("Node 0"), "Error must name node 0: {}", e); |
| 1218 | assert!(e.contains("doc"), "Error must name the rule broken: {}", e); |
| 1219 | Ok(()) |
| 1220 | } |
| 1221 | |
| 1222 | #[test] |
| 1223 | fn test_text_payload_must_be_str_14() -> Outcome<()> { |
| 1224 | let para = node(NodeKind::Para, mapdat!{ |
| 1225 | "children" => Dat::List(vec![ |
| 1226 | node(NodeKind::Text, mapdat!{ "value" => dat!("Wrapped") }), |
| 1227 | ]), |
| 1228 | }); |
| 1229 | let e = match validate(&doc(vec![para]), SCHEMA_DOC) { |
| 1230 | Ok(_) => return Err(err!("A text run with a map payload was accepted."; Test)), |
| 1231 | Err(e) => fmt!("{}", e), |
| 1232 | }; |
| 1233 | assert!(e.contains("Node 2"), "Error must name node 2: {}", e); |
| 1234 | assert!(e.contains("str"), "Error must name the payload rule: {}", e); |
| 1235 | Ok(()) |
| 1236 | } |
| 1237 | |
| 1238 | #[test] |
| 1239 | fn test_non_map_payload_15() -> Outcome<()> { |
| 1240 | let para = Dat::Usr( |
| 1241 | UsrKindId::new(NodeKind::Para.code(), Some("para"), None), |
| 1242 | Some(Box::new(dat!("Bare string"))), |
| 1243 | ); |
| 1244 | let e = match validate(&doc(vec![para]), SCHEMA_DOC) { |
| 1245 | Ok(_) => return Err(err!("A para with a string payload was accepted."; Test)), |
| 1246 | Err(e) => fmt!("{}", e), |
| 1247 | }; |
| 1248 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1249 | assert!(e.contains("map"), "Error must name the payload rule: {}", e); |
| 1250 | Ok(()) |
| 1251 | } |
| 1252 | |
| 1253 | #[test] |
| 1254 | fn test_children_where_none_admitted_16() -> Outcome<()> { |
| 1255 | let image = node(NodeKind::Image, mapdat!{ |
| 1256 | "hash" => Dat::BU8(vec![1]), |
| 1257 | "alt" => dat!("An image"), |
| 1258 | "children" => Dat::List(vec![text("Impossible")]), |
| 1259 | }); |
| 1260 | let e = match validate(&doc(vec![image]), SCHEMA_DOC) { |
| 1261 | Ok(_) => return Err(err!("An image with children was accepted."; Test)), |
| 1262 | Err(e) => fmt!("{}", e), |
| 1263 | }; |
| 1264 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1265 | assert!(e.contains("children"), "Error must name the field: {}", e); |
| 1266 | Ok(()) |
| 1267 | } |
| 1268 | |
| 1269 | #[test] |
| 1270 | fn test_node_ids_are_pre_order_17() -> Outcome<()> { |
| 1271 | // doc(0), section(1), para(2), text(3), para(4), text(5). The forbidden node is the second |
| 1272 | // para's child, so a text run misplaced there must be named 5, not 4. |
| 1273 | let bad = node(NodeKind::Section, mapdat!{ |
| 1274 | "title" => dat!("S"), |
| 1275 | "children" => Dat::List(vec![ |
| 1276 | node(NodeKind::Para, mapdat!{ |
| 1277 | "children" => Dat::List(vec![text("One")]), |
| 1278 | }), |
| 1279 | node(NodeKind::Para, mapdat!{ |
| 1280 | "children" => Dat::List(vec![ |
| 1281 | node(NodeKind::Heading, mapdat!{ |
| 1282 | "level" => dat!(1u8), |
| 1283 | "children" => Dat::List(vec![]), |
| 1284 | }), |
| 1285 | ]), |
| 1286 | }), |
| 1287 | ]), |
| 1288 | }); |
| 1289 | let e = match validate(&doc(vec![bad]), SCHEMA_DOC) { |
| 1290 | Ok(_) => return Err(err!("A heading inside a para was accepted."; Test)), |
| 1291 | Err(e) => fmt!("{}", e), |
| 1292 | }; |
| 1293 | assert!(e.contains("Node 5"), "Error must name node 5: {}", e); |
| 1294 | Ok(()) |
| 1295 | } |
| 1296 | |
| 1297 | #[test] |
| 1298 | fn test_non_usr_node_18() -> Outcome<()> { |
| 1299 | let e = match validate(&doc(vec![dat!("Loose string")]), SCHEMA_DOC) { |
| 1300 | Ok(_) => return Err(err!("A bare string as a node was accepted."; Test)), |
| 1301 | Err(e) => fmt!("{}", e), |
| 1302 | }; |
| 1303 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1304 | assert!(e.contains("usr"), "Error must name the rule broken: {}", e); |
| 1305 | Ok(()) |
| 1306 | } |
| 1307 | |
| 1308 | /// Builds a doc whose one paragraph carries a single link with the given address. |
| 1309 | fn doc_with_link(to: Dat) -> Dat { |
| 1310 | doc(vec![ |
| 1311 | node(NodeKind::Para, mapdat!{ |
| 1312 | "children" => Dat::List(vec![ |
| 1313 | node(NodeKind::Link, mapdat!{ |
| 1314 | "to" => to, |
| 1315 | "children" => Dat::List(vec![text("here")]), |
| 1316 | }), |
| 1317 | ]), |
| 1318 | }), |
| 1319 | ]) |
| 1320 | } |
| 1321 | |
| 1322 | /// Builds a doc carrying a style table of one record and one box that names a style. |
| 1323 | fn doc_with_style(record: Dat, style_name: &str) -> Dat { |
| 1324 | node(NodeKind::Doc, mapdat!{ |
| 1325 | "title" => dat!("T"), |
| 1326 | "lang" => dat!("en"), |
| 1327 | "styles" => mapdat!{ "callout" => record }, |
| 1328 | "children" => Dat::List(vec![ |
| 1329 | node(NodeKind::Boxx, mapdat!{ |
| 1330 | "style" => dat!(style_name), |
| 1331 | "children" => Dat::List(vec![ |
| 1332 | node(NodeKind::Para, mapdat!{ |
| 1333 | "children" => Dat::List(vec![text("in a box")]), |
| 1334 | }), |
| 1335 | ]), |
| 1336 | }), |
| 1337 | ]), |
| 1338 | }) |
| 1339 | } |
| 1340 | |
| 1341 | #[test] |
| 1342 | fn test_code_accepted_19() -> Outcome<()> { |
| 1343 | let code = node(NodeKind::Code, mapdat!{ |
| 1344 | "lang" => dat!("rust"), |
| 1345 | "text" => dat!("fn main() {}"), |
| 1346 | }); |
| 1347 | let stats = res!(validate(&doc(vec![code]), SCHEMA_DOC)); |
| 1348 | // doc, code. |
| 1349 | assert_eq!(stats.nodes, 2); |
| 1350 | Ok(()) |
| 1351 | } |
| 1352 | |
| 1353 | #[test] |
| 1354 | fn test_quote_accepted_20() -> Outcome<()> { |
| 1355 | let quote = node(NodeKind::Quote, mapdat!{ |
| 1356 | "cite" => dat!("A. Author"), |
| 1357 | "children" => Dat::List(vec![ |
| 1358 | node(NodeKind::Para, mapdat!{ |
| 1359 | "children" => Dat::List(vec![text("Quoted")]), |
| 1360 | }), |
| 1361 | ]), |
| 1362 | }); |
| 1363 | let stats = res!(validate(&doc(vec![quote]), SCHEMA_DOC)); |
| 1364 | // doc, quote, para, text. |
| 1365 | assert_eq!(stats.nodes, 4); |
| 1366 | Ok(()) |
| 1367 | } |
| 1368 | |
| 1369 | #[test] |
| 1370 | fn test_link_by_name_accepted_21() -> Outcome<()> { |
| 1371 | let tree = doc_with_link(mapdat!{ "name" => dat!("news.cricket") }); |
| 1372 | let stats = res!(validate(&tree, SCHEMA_DOC)); |
| 1373 | // doc, para, link, text. |
| 1374 | assert_eq!(stats.nodes, 4); |
| 1375 | Ok(()) |
| 1376 | } |
| 1377 | |
| 1378 | #[test] |
| 1379 | fn test_link_by_hash_accepted_22() -> Outcome<()> { |
| 1380 | let tree = doc_with_link(mapdat!{ "hash" => hash32() }); |
| 1381 | let stats = res!(validate(&tree, SCHEMA_DOC)); |
| 1382 | assert_eq!(stats.nodes, 4); |
| 1383 | Ok(()) |
| 1384 | } |
| 1385 | |
| 1386 | #[test] |
| 1387 | fn test_link_address_two_entries_23() -> Outcome<()> { |
| 1388 | let tree = doc_with_link(mapdat!{ |
| 1389 | "name" => dat!("news.cricket"), |
| 1390 | "hash" => hash32(), |
| 1391 | }); |
| 1392 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1393 | Ok(_) => return Err(err!("A two-entry link address was accepted."; Test)), |
| 1394 | Err(e) => fmt!("{}", e), |
| 1395 | }; |
| 1396 | assert!(e.contains("Node 2"), "Error must name node 2: {}", e); |
| 1397 | assert!(e.contains("to") || e.contains("address"), |
| 1398 | "Error must name the address rule: {}", e); |
| 1399 | Ok(()) |
| 1400 | } |
| 1401 | |
| 1402 | #[test] |
| 1403 | fn test_style_names_missing_entry_24() -> Outcome<()> { |
| 1404 | // The table defines "callout", but the box names "ghost". |
| 1405 | let tree = doc_with_style(mapdat!{ "bg" => dat!("muted") }, "ghost"); |
| 1406 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1407 | Ok(_) => return Err(err!("A style naming an absent entry was accepted."; Test)), |
| 1408 | Err(e) => fmt!("{}", e), |
| 1409 | }; |
| 1410 | assert!(e.contains("Node 1"), "Error must name node 1, the box: {}", e); |
| 1411 | assert!(e.contains("ghost"), "Error must name the missing style: {}", e); |
| 1412 | Ok(()) |
| 1413 | } |
| 1414 | |
| 1415 | #[test] |
| 1416 | fn test_style_defined_but_unreferenced() -> Outcome<()> { |
| 1417 | // The table defines "callout", but no node names it. An unused style has no effect, so a |
| 1418 | // document with it and one without render alike, which would be two addresses for one |
| 1419 | // document (§4.4). |
| 1420 | let tree = node(NodeKind::Doc, mapdat!{ |
| 1421 | "title" => dat!("T"), |
| 1422 | "lang" => dat!("en"), |
| 1423 | "styles" => mapdat!{ "callout" => mapdat!{ "bg" => dat!("muted") } }, |
| 1424 | "children" => Dat::List(vec![ |
| 1425 | node(NodeKind::Para, mapdat!{ |
| 1426 | "children" => Dat::List(vec![text("no style here")]), |
| 1427 | }), |
| 1428 | ]), |
| 1429 | }); |
| 1430 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1431 | Ok(_) => return Err(err!("An unreferenced style entry was accepted."; Test)), |
| 1432 | Err(e) => fmt!("{}", e), |
| 1433 | }; |
| 1434 | assert!(e.contains("callout"), "Error must name the unused style: {}", e); |
| 1435 | Ok(()) |
| 1436 | } |
| 1437 | |
| 1438 | #[test] |
| 1439 | fn test_style_unknown_property_25() -> Outcome<()> { |
| 1440 | let tree = doc_with_style(mapdat!{ "wibble" => dat!("x") }, "callout"); |
| 1441 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1442 | Ok(_) => return Err(err!("A style record with an unknown property was accepted."; Test)), |
| 1443 | Err(e) => fmt!("{}", e), |
| 1444 | }; |
| 1445 | // The style table is validated at the doc, node 0. |
| 1446 | assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e); |
| 1447 | assert!(e.contains("callout"), "Error must name the style: {}", e); |
| 1448 | assert!(e.contains("wibble"), "Error must name the property: {}", e); |
| 1449 | Ok(()) |
| 1450 | } |
| 1451 | |
| 1452 | #[test] |
| 1453 | fn test_style_bg_out_of_palette_26() -> Outcome<()> { |
| 1454 | let tree = doc_with_style(mapdat!{ "bg" => dat!("purple") }, "callout"); |
| 1455 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1456 | Ok(_) => return Err(err!("A bg outside the palette was accepted."; Test)), |
| 1457 | Err(e) => fmt!("{}", e), |
| 1458 | }; |
| 1459 | assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e); |
| 1460 | assert!(e.contains("callout"), "Error must name the style: {}", e); |
| 1461 | assert!(e.contains("bg"), "Error must name the property: {}", e); |
| 1462 | Ok(()) |
| 1463 | } |
| 1464 | |
| 1465 | #[test] |
| 1466 | fn test_style_align_out_of_enum_27() -> Outcome<()> { |
| 1467 | let tree = doc_with_style(mapdat!{ "align" => dat!("middle") }, "callout"); |
| 1468 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1469 | Ok(_) => return Err(err!("An align outside the enum was accepted."; Test)), |
| 1470 | Err(e) => fmt!("{}", e), |
| 1471 | }; |
| 1472 | assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e); |
| 1473 | assert!(e.contains("callout"), "Error must name the style: {}", e); |
| 1474 | assert!(e.contains("align"), "Error must name the property: {}", e); |
| 1475 | Ok(()) |
| 1476 | } |
| 1477 | |
| 1478 | #[test] |
| 1479 | fn test_unknown_kind_with_fallback_accepted_28() -> Outcome<()> { |
| 1480 | // A kind 20 the reader does not know, carrying a valid fallback and an uninterpreted field. |
| 1481 | let alien = Dat::Usr( |
| 1482 | UsrKindId::new(20, None, None), |
| 1483 | Some(Box::new(mapdat!{ |
| 1484 | "fallback" => Dat::List(vec![ |
| 1485 | node(NodeKind::Para, mapdat!{ |
| 1486 | "children" => Dat::List(vec![text("A fallback paragraph")]), |
| 1487 | }), |
| 1488 | ]), |
| 1489 | "rows" => dat!("only a reader that knows kind 20 uses this"), |
| 1490 | })), |
| 1491 | ); |
| 1492 | let stats = res!(validate(&doc(vec![alien]), SCHEMA_DOC)); |
| 1493 | // doc, the unknown node, its fallback para, and that para's text all count toward the limit. |
| 1494 | assert_eq!(stats.nodes, 4); |
| 1495 | Ok(()) |
| 1496 | } |
| 1497 | |
| 1498 | #[test] |
| 1499 | fn test_unknown_kind_without_fallback_rejected_29() -> Outcome<()> { |
| 1500 | let alien = Dat::Usr( |
| 1501 | UsrKindId::new(20, None, None), |
| 1502 | Some(Box::new(mapdat!{ "rows" => dat!("no fallback here") })), |
| 1503 | ); |
| 1504 | let e = match validate(&doc(vec![alien]), SCHEMA_DOC) { |
| 1505 | Ok(_) => return Err(err!("An unknown kind with no fallback was accepted."; Test)), |
| 1506 | Err(e) => fmt!("{}", e), |
| 1507 | }; |
| 1508 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1509 | assert!(e.contains("fallback"), "Error must name the fallback rule: {}", e); |
| 1510 | Ok(()) |
| 1511 | } |
| 1512 | |
| 1513 | #[test] |
| 1514 | fn test_unknown_kind_empty_fallback_rejected_30() -> Outcome<()> { |
| 1515 | let alien = Dat::Usr( |
| 1516 | UsrKindId::new(20, None, None), |
| 1517 | Some(Box::new(mapdat!{ "fallback" => Dat::List(Vec::new()) })), |
| 1518 | ); |
| 1519 | let e = match validate(&doc(vec![alien]), SCHEMA_DOC) { |
| 1520 | Ok(_) => return Err(err!("An unknown kind with an empty fallback was accepted."; Test)), |
| 1521 | Err(e) => fmt!("{}", e), |
| 1522 | }; |
| 1523 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1524 | assert!(e.contains("fallback"), "Error must name the fallback rule: {}", e); |
| 1525 | Ok(()) |
| 1526 | } |
| 1527 | |
| 1528 | /// A node of a reserved kind (§4.2), built as a document author would have to build one. |
| 1529 | fn reserved(kind: ReservedKind, payload: Dat) -> Dat { |
| 1530 | Dat::Usr( |
| 1531 | UsrKindId::new(kind.code(), Some(kind.label()), None), |
| 1532 | Some(Box::new(payload)), |
| 1533 | ) |
| 1534 | } |
| 1535 | |
| 1536 | #[test] |
| 1537 | fn test_reserved_edit_in_doc_rejected_31() -> Outcome<()> { |
| 1538 | let edit = reserved(ReservedKind::Edit, mapdat!{ |
| 1539 | "placeholder" => dat!("Search the oxeweb"), |
| 1540 | }); |
| 1541 | let e = match validate(&doc(vec![edit]), SCHEMA_DOC) { |
| 1542 | Ok(_) => return Err(err!("A document carrying an edit node was accepted."; Test)), |
| 1543 | Err(e) => fmt!("{}", e), |
| 1544 | }; |
| 1545 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1546 | assert!(e.contains("edit"), "Error must name the kind: {}", e); |
| 1547 | assert!(e.contains("may not carry an edit node"), |
| 1548 | "Error must say a document may not carry it: {}", e); |
| 1549 | Ok(()) |
| 1550 | } |
| 1551 | |
| 1552 | #[test] |
| 1553 | fn test_reserved_surface_in_doc_rejected_32() -> Outcome<()> { |
| 1554 | let surface = reserved(ReservedKind::Surface, mapdat!{ |
| 1555 | "app" => dat!("app.modeller"), |
| 1556 | }); |
| 1557 | let e = match validate(&doc(vec![surface]), SCHEMA_DOC) { |
| 1558 | Ok(_) => return Err(err!("A document carrying a surface node was accepted."; Test)), |
| 1559 | Err(e) => fmt!("{}", e), |
| 1560 | }; |
| 1561 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1562 | assert!(e.contains("surface"), "Error must name the kind: {}", e); |
| 1563 | assert!(e.contains("may not carry a surface node"), |
| 1564 | "Error must say a document may not carry it: {}", e); |
| 1565 | Ok(()) |
| 1566 | } |
| 1567 | |
| 1568 | #[test] |
| 1569 | fn test_reserved_surface_with_fallback_still_rejected_33() -> Outcome<()> { |
| 1570 | // The hole §4.5 would leave open if a reserved code were treated as merely unknown. The |
| 1571 | // fallback is valid, non-empty, and made of known nodes, and it buys the surface nothing: a |
| 1572 | // reader that knows what code 15 is refuses it whether or not it is offered a stand-in. |
| 1573 | let surface = reserved(ReservedKind::Surface, mapdat!{ |
| 1574 | "fallback" => Dat::List(vec![ |
| 1575 | node(NodeKind::Para, mapdat!{ |
| 1576 | "children" => Dat::List(vec![text("A picture of a teapot.")]), |
| 1577 | }), |
| 1578 | ]), |
| 1579 | "app" => dat!("app.modeller"), |
| 1580 | }); |
| 1581 | let e = match validate(&doc(vec![surface]), SCHEMA_DOC) { |
| 1582 | Ok(_) => return Err(err!( |
| 1583 | "A document carrying a surface node with a valid fallback was accepted. A fallback \ |
| 1584 | admits an unknown kind, and never a reserved one."; Test)), |
| 1585 | Err(e) => fmt!("{}", e), |
| 1586 | }; |
| 1587 | assert!(e.contains("Node 1"), "Error must name node 1: {}", e); |
| 1588 | assert!(e.contains("may not carry a surface node"), |
| 1589 | "Error must say a document may not carry it: {}", e); |
| 1590 | assert!(e.contains("whether or not it carries a fallback"), |
| 1591 | "Error must say the fallback does not admit it: {}", e); |
| 1592 | Ok(()) |
| 1593 | } |
| 1594 | |
| 1595 | /// A valid border, as a chrome writes one: a palette name and a width in pixels. |
| 1596 | fn border() -> Dat { |
| 1597 | Dat::List(vec![dat!("muted"), dat!(1u8)]) |
| 1598 | } |
| 1599 | |
| 1600 | #[test] |
| 1601 | fn test_style_interface_property_in_doc_rejected_35() -> Outcome<()> { |
| 1602 | // The v0 document vocabulary is closed. A document naming an interface property is refused, |
| 1603 | // whatever the reader can draw, so a document cannot dress as an interface. |
| 1604 | // |
| 1605 | // `radius` is NOT among these and belongs with the document's own eight: it decorates a surface |
| 1606 | // a document already draws with `bg` and `pad`, and it means something in prose, which the |
| 1607 | // layout of a bar does not. `shadow` is elevation -- a paragraph claiming to float above its |
| 1608 | // page IS a document dressing as an interface -- and `border` is the line round a control with |
| 1609 | // an edge. See [`STYLE_FIELDS`](oxedyne_fe2o3_sbj::kinds::STYLE_FIELDS) for the whole of that argument. |
| 1610 | for (prop, value) in [ |
| 1611 | ("grid", dat!(1400u16)), |
| 1612 | ("shadow", dat!(2u8)), |
| 1613 | ("border", border()), |
| 1614 | ] { |
| 1615 | let tree = doc_with_style(mapdat!{ prop => value }, "callout"); |
| 1616 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1617 | Ok(_) => return Err(err!( |
| 1618 | "A document naming the interface style property '{}' was accepted. The v0 \ |
| 1619 | document vocabulary is frozen at eight.", prop; Test)), |
| 1620 | Err(e) => fmt!("{}", e), |
| 1621 | }; |
| 1622 | assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e); |
| 1623 | assert!(e.contains("callout"), "Error must name the style: {}", e); |
| 1624 | assert!(e.contains(prop), "Error must name the property: {}", e); |
| 1625 | // The refusal must not call a real property unknown: a member whose chrome draws a grid |
| 1626 | // every day is being misled by "unknown style property 'grid'". |
| 1627 | assert!(!e.contains("unknown"), |
| 1628 | "'{}' is a real property, and the refusal must not call it unknown: {}", prop, e); |
| 1629 | assert!(e.contains(SCHEMA_DOC), "Error must name the schema that refused it: {}", e); |
| 1630 | assert!(e.contains("a chrome or an application tree"), |
| 1631 | "Error must say where '{}' is legal: {}", prop, e); |
| 1632 | } |
| 1633 | Ok(()) |
| 1634 | } |
| 1635 | |
| 1636 | #[test] |
| 1637 | fn test_style_interface_property_in_chrome_and_app_accepted_36() -> Outcome<()> { |
| 1638 | // The chrome is a real interface and needs to look like one: a grid of tiles, an edge, a |
| 1639 | // rounded corner. An application's tree is drawn by the same engine and admits the same. |
| 1640 | for schema in [SCHEMA_CHROME, SCHEMA_APP] { |
| 1641 | let tree = doc_with_style(mapdat!{ |
| 1642 | "grid" => dat!(1400u16), |
| 1643 | "radius" => dat!(2u8), |
| 1644 | "border" => border(), |
| 1645 | "bg" => dat!("muted"), |
| 1646 | "pad" => dat!(3u8), |
| 1647 | }, "callout"); |
| 1648 | let stats = res!(validate(&tree, schema)); |
| 1649 | // doc, box, para, text. |
| 1650 | assert_eq!(stats.nodes, 4, "'{}' admits the interface properties", schema); |
| 1651 | } |
| 1652 | Ok(()) |
| 1653 | } |
| 1654 | |
| 1655 | #[test] |
| 1656 | fn test_style_unknown_property_in_chrome_still_rejected_37() -> Outcome<()> { |
| 1657 | // A wider vocabulary is not an open one: the chrome's admitted set is closed like any other. |
| 1658 | let tree = doc_with_style(mapdat!{ "wibble" => dat!("x") }, "callout"); |
| 1659 | let e = match validate(&tree, SCHEMA_CHROME) { |
| 1660 | Ok(_) => return Err(err!("A chrome style with an unknown property was accepted."; Test)), |
| 1661 | Err(e) => fmt!("{}", e), |
| 1662 | }; |
| 1663 | assert!(e.contains("wibble"), "Error must name the property: {}", e); |
| 1664 | assert!(e.contains("unknown"), "An invented property is unknown, and is named so: {}", e); |
| 1665 | Ok(()) |
| 1666 | } |
| 1667 | |
| 1668 | #[test] |
| 1669 | fn test_style_document_properties_admitted_by_every_schema_38() -> Outcome<()> { |
| 1670 | // The eight are the floor, not the document's alone: a chrome is a document that may say more. |
| 1671 | for schema in [SCHEMA_DOC, SCHEMA_CHROME, SCHEMA_APP] { |
| 1672 | let tree = doc_with_style(mapdat!{ |
| 1673 | "fill" => dat!("ink"), |
| 1674 | "size" => dat!(1i8), |
| 1675 | "lang" => dat!("en-GB"), |
| 1676 | "dir" => dat!("ltr"), |
| 1677 | "bg" => dat!("muted"), |
| 1678 | "pad" => dat!(3u8), |
| 1679 | "align" => dat!("start"), |
| 1680 | "radius" => dat!(2u8), |
| 1681 | }, "callout"); |
| 1682 | let stats = res!(validate(&tree, schema)); |
| 1683 | assert_eq!(stats.nodes, 4, "'{}' admits the eight document properties", schema); |
| 1684 | } |
| 1685 | Ok(()) |
| 1686 | } |
| 1687 | |
| 1688 | #[test] |
| 1689 | fn test_a_document_may_round_the_corner_of_a_box_it_already_draws_44() -> Outcome<()> { |
| 1690 | // The question that moved `radius` out of the interface's list: is a corner radius a property a |
| 1691 | // DOCUMENT legitimately has? It decorates a surface a document already draws -- a style may name |
| 1692 | // `bg` and `pad`, so a tinted padded box is already a thing a document makes -- and it adds no |
| 1693 | // element, no geometry and no authority. A soft-cornered callout is typography, not an |
| 1694 | // interface. |
| 1695 | let callout = doc_with_style(mapdat!{ |
| 1696 | "bg" => dat!("muted"), |
| 1697 | "pad" => dat!(3u8), |
| 1698 | "radius" => dat!(2u8), |
| 1699 | }, "callout"); |
| 1700 | let stats = res!(validate(&callout, SCHEMA_DOC)); |
| 1701 | assert_eq!(stats.nodes, 4, "a document rounds the corner of its own box"); |
| 1702 | |
| 1703 | // And the rule it seemed to break holds where it was actually doing work: a document still |
| 1704 | // cannot claim to stand off its page, nor draw the edge of a control. |
| 1705 | for prop in ["shadow", "grow"] { |
| 1706 | let tree = doc_with_style(mapdat!{ prop => dat!(2u8) }, "callout"); |
| 1707 | assert!( |
| 1708 | validate(&tree, SCHEMA_DOC).is_err(), |
| 1709 | "'{}' is the interface's and a document must not name it", prop, |
| 1710 | ); |
| 1711 | } |
| 1712 | Ok(()) |
| 1713 | } |
| 1714 | |
| 1715 | #[test] |
| 1716 | fn test_style_border_is_a_palette_name_and_a_width_39() -> Outcome<()> { |
| 1717 | // The validator and the renderer must agree about what a border is, since a border the |
| 1718 | // validator accepts and Kiln cannot draw is a chrome tree that passes and then fails. These |
| 1719 | // are the shapes Kiln refuses, and they are refused here too. |
| 1720 | let bad: Vec<(&str, Dat)> = vec![ |
| 1721 | ("a border of one thing", Dat::List(vec![dat!("muted")])), |
| 1722 | ("a border of three", Dat::List(vec![dat!("muted"), dat!(1u8), dat!(2u8)])), |
| 1723 | ("a border of nothing", Dat::List(Vec::new())), |
| 1724 | ("a border the wrong way round", Dat::List(vec![dat!(1u8), dat!("muted")])), |
| 1725 | ("a border that is a bare colour", dat!("muted")), |
| 1726 | ("a border of a colour outside the palette", Dat::List(vec![dat!("puce"), dat!(1u8)])), |
| 1727 | ("a border whose width is not a u8", Dat::List(vec![dat!("muted"), dat!(1i8)])), |
| 1728 | ]; |
| 1729 | for (what, value) in bad { |
| 1730 | let tree = doc_with_style(mapdat!{ "border" => value }, "callout"); |
| 1731 | let e = match validate(&tree, SCHEMA_CHROME) { |
| 1732 | Ok(_) => return Err(err!("{} was accepted, and is not a border.", what; Test)), |
| 1733 | Err(e) => fmt!("{}", e), |
| 1734 | }; |
| 1735 | assert!(e.contains("border"), "{}: the error must name the property: {}", what, e); |
| 1736 | } |
| 1737 | // And the one shape that is a border is accepted. |
| 1738 | let tree = doc_with_style(mapdat!{ "border" => border() }, "callout"); |
| 1739 | res!(validate(&tree, SCHEMA_CHROME)); |
| 1740 | Ok(()) |
| 1741 | } |
| 1742 | |
| 1743 | #[test] |
| 1744 | fn test_style_grid_and_radius_are_whole_scalars_40() -> Outcome<()> { |
| 1745 | // Kiln reads each at ONE declared width and nothing else, so a negative step or a string is |
| 1746 | // refused here rather than reaching a renderer that cannot read it. The widths differ -- a grid |
| 1747 | // tile is a percentage of a base size and a radius a step on the spacing scale -- and the error |
| 1748 | // must name the one it wanted, or an author cannot tell which they got wrong. |
| 1749 | for (prop, width) in [("grid", "u16"), ("radius", "u8")] { |
| 1750 | for value in [dat!("2"), dat!(-2i8), dat!(2i32), dat!(2u32)] { |
| 1751 | let tree = doc_with_style(mapdat!{ prop => value }, "callout"); |
| 1752 | let e = match validate(&tree, SCHEMA_CHROME) { |
| 1753 | Ok(_) => return Err(err!( |
| 1754 | "A '{}' that is not a whole scalar was accepted.", prop; Test)), |
| 1755 | Err(e) => fmt!("{}", e), |
| 1756 | }; |
| 1757 | assert!(e.contains(prop), "Error must name the property: {}", e); |
| 1758 | assert!(e.contains(width), "Error must name the declared width: {}", e); |
| 1759 | } |
| 1760 | } |
| 1761 | Ok(()) |
| 1762 | } |
| 1763 | |
| 1764 | #[test] |
| 1765 | fn test_reserved_icon_in_doc_rejected_41() -> Outcome<()> { |
| 1766 | // An icon names the reader's own drawing, so a document carrying one would let whichever |
| 1767 | // reader opened it supply the document's content. Refused by the same rule as the other two. |
| 1768 | let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!("home") }); |
| 1769 | let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]); |
| 1770 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1771 | Ok(_) => return Err(err!("A document carrying an icon node was accepted."; Test)), |
| 1772 | Err(e) => fmt!("{}", e), |
| 1773 | }; |
| 1774 | // doc 0, para 1, icon 2. |
| 1775 | assert!(e.contains("Node 2"), "Error must name the icon's node: {}", e); |
| 1776 | assert!(e.contains("icon"), "Error must name the kind: {}", e); |
| 1777 | assert!(e.contains("may not carry an icon node"), |
| 1778 | "Error must say a document may not carry it: {}", e); |
| 1779 | Ok(()) |
| 1780 | } |
| 1781 | |
| 1782 | #[test] |
| 1783 | fn test_reserved_icon_in_chrome_and_app_accepted_42() -> Outcome<()> { |
| 1784 | // A chrome is a real interface and its buttons are icons. An application's tree is drawn by |
| 1785 | // the same engine and reaches the same set. |
| 1786 | for schema in [SCHEMA_CHROME, SCHEMA_APP] { |
| 1787 | for name in crate::kinds::ICON_NAMES { |
| 1788 | let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!(*name) }); |
| 1789 | let tree = doc(vec![ |
| 1790 | node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) }), |
| 1791 | ]); |
| 1792 | let stats = res!(validate(&tree, schema)); |
| 1793 | // doc, para, icon. |
| 1794 | assert_eq!(stats.nodes, 3, "'{}' admits the icon '{}'", schema, name); |
| 1795 | } |
| 1796 | } |
| 1797 | Ok(()) |
| 1798 | } |
| 1799 | |
| 1800 | #[test] |
| 1801 | fn test_reserved_icon_of_an_unknown_name_rejected_43() -> Outcome<()> { |
| 1802 | // The set is closed. An icon carries no drawing of its own, so a name the engine has never |
| 1803 | // heard of has nothing faithful to stand in for it: there is no gap to render, only a caller |
| 1804 | // that asked for something that does not exist. |
| 1805 | for schema in [SCHEMA_CHROME, SCHEMA_APP] { |
| 1806 | let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!("wibble") }); |
| 1807 | let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]); |
| 1808 | let e = match validate(&tree, schema) { |
| 1809 | Ok(_) => return Err(err!( |
| 1810 | "An icon named 'wibble' was accepted by '{}'.", schema; Test)), |
| 1811 | Err(e) => fmt!("{}", e), |
| 1812 | }; |
| 1813 | assert!(e.contains("wibble"), "Error must name the icon asked for: {}", e); |
| 1814 | assert!(e.contains("back"), "Error must spell the set it would have taken: {}", e); |
| 1815 | } |
| 1816 | Ok(()) |
| 1817 | } |
| 1818 | |
| 1819 | #[test] |
| 1820 | fn test_reserved_icon_with_fallback_still_rejected_44() -> Outcome<()> { |
| 1821 | // The §4.5 hole, for the icon as for the surface: a reader that knows what code 16 is refuses |
| 1822 | // it in a document whether or not it is offered a stand-in. |
| 1823 | let icon = reserved(ReservedKind::Icon, mapdat!{ |
| 1824 | "name" => dat!("home"), |
| 1825 | "fallback" => Dat::List(vec![ |
| 1826 | Dat::Usr( |
| 1827 | UsrKindId::new(NodeKind::Para.code(), Some("para"), None), |
| 1828 | Some(Box::new(mapdat!{})), |
| 1829 | ), |
| 1830 | ]), |
| 1831 | }); |
| 1832 | let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]); |
| 1833 | let e = match validate(&tree, SCHEMA_DOC) { |
| 1834 | Ok(_) => return Err(err!("An icon with a fallback was accepted in a document."; Test)), |
| 1835 | Err(e) => fmt!("{}", e), |
| 1836 | }; |
| 1837 | assert!(e.contains("icon"), "Error must name the kind: {}", e); |
| 1838 | Ok(()) |
| 1839 | } |
| 1840 | |
| 1841 | #[test] |
| 1842 | fn test_reserved_surface_as_root_rejected_34() -> Outcome<()> { |
| 1843 | let root = reserved(ReservedKind::Surface, mapdat!{ |
| 1844 | "app" => dat!("app.modeller"), |
| 1845 | }); |
| 1846 | let e = match validate(&root, SCHEMA_DOC) { |
| 1847 | Ok(_) => return Err(err!("A surface as the root of a document was accepted."; Test)), |
| 1848 | Err(e) => fmt!("{}", e), |
| 1849 | }; |
| 1850 | assert!(e.contains("Node 0"), "Error must name node 0: {}", e); |
| 1851 | assert!(e.contains("may not carry a surface node"), |
| 1852 | "Error must say a document may not carry it: {}", e); |
| 1853 | Ok(()) |
| 1854 | } |
| 1855 | } |