max / alloy
2 files changed,
+501 insertions,
-0 deletions
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | mod net; | |
| 15 | 15 | mod pkg; | |
| 16 | 16 | mod run; | |
| 17 | + | mod schema; | |
| 17 | 18 | mod shell; | |
| 18 | 19 | mod theme; | |
| 19 | 20 | mod wizard; |
| @@ -1,0 +1,1144 @@ | |||
| 1 | + | //! Schema-DSL v1: reading a `.schema` file into the form the console renders. | |
| 2 | + | //! | |
| 3 | + | //! docs/CONSOLE.md#the-schema-format is the reference; `schemas/rio.toml.schema` | |
| 4 | + | //! is the worked example this module is tested against. The parser is the first | |
| 5 | + | //! of the two pure halves of `alloy config` (the other is the `toml_edit` bind | |
| 6 | + | //! layer), so nothing here touches a terminal or a target file. | |
| 7 | + | //! | |
| 8 | + | //! Two things it deliberately does more of than "deserialize the file": | |
| 9 | + | //! | |
| 10 | + | //! - **Groups are expanded here.** `[[group]]` is DSL sugar; docs/CONSOLE.md | |
| 11 | + | //! calls it "not a runtime concept". A group of 29 palette entries becomes 29 | |
| 12 | + | //! ordinary [`Field`]s at `colors.<key>`, so every consumer downstream sees | |
| 13 | + | //! one flat list and no view has to know groups existed. | |
| 14 | + | //! - **Defaults and presets are checked against the fields they name.** These | |
| 15 | + | //! files are hand-written, dozens of them eventually, and a `default` outside | |
| 16 | + | //! its own `range` or a preset naming a path that no field declares is an | |
| 17 | + | //! authoring mistake that would otherwise surface as a wrong form at runtime. | |
| 18 | + | //! | |
| 19 | + | //! Every error out of here is a fallback diagnostic: docs/CONSOLE.md routes an | |
| 20 | + | //! unknown `schema_version`, an unknown field type, or an unreadable schema to | |
| 21 | + | //! the text-edit pane with the reason shown. So the messages are written to be | |
| 22 | + | //! read by whoever is editing the schema, and they name the field's path. | |
| 23 | + | //! | |
| 24 | + | //! <!-- wiki: alloy-console --> | |
| 25 | + | ||
| 26 | + | // Step 1 of the build order lands the parser on its own, so nothing in the | |
| 27 | + | // binary reads a `Schema` yet and every type here is dead by the compiler's | |
| 28 | + | // reckoning. The tests are what exercise it until the bind layer and the view | |
| 29 | + | // arrive; this allow comes off with them. | |
| 30 | + | #![allow(dead_code)] | |
| 31 | + | ||
| 32 | + | use std::path::Path; | |
| 33 | + | ||
| 34 | + | use anyhow::{Context, Result, bail, ensure}; | |
| 35 | + | use serde::Deserialize; | |
| 36 | + | use toml::Value; | |
| 37 | + | ||
| 38 | + | /// The schema-DSL version this parser implements. | |
| 39 | + | /// | |
| 40 | + | /// A schema declaring anything else is not a file to guess at: v2 is already | |
| 41 | + | /// sketched (`enabled_when`), and a v1 editor rendering a v2 schema would drop | |
| 42 | + | /// the constraint silently. Fallback is the honest outcome. | |
| 43 | + | const DSL_VERSION: &str = "1"; | |
| 44 | + | ||
| 45 | + | /// A parsed schema: the header, the UI sections in declared order, every field | |
| 46 | + | /// with groups already expanded, and the presets. | |
| 47 | + | #[derive(Debug)] | |
| 48 | + | pub(crate) struct Schema { | |
| 49 | + | pub(crate) header: Header, | |
| 50 | + | pub(crate) sections: Vec<Section>, | |
| 51 | + | pub(crate) fields: Vec<Field>, | |
| 52 | + | pub(crate) presets: Vec<Preset>, | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /// The `[schema]` block. | |
| 56 | + | #[derive(Debug)] | |
| 57 | + | pub(crate) struct Header { | |
| 58 | + | pub(crate) target: String, | |
| 59 | + | pub(crate) target_tool: String, | |
| 60 | + | /// Semver range the schema was written against, e.g. `">=0.2"`. Held as | |
| 61 | + | /// written: matching it against the installed tool needs a version to | |
| 62 | + | /// compare with, which is the view's business and not the parser's. | |
| 63 | + | pub(crate) target_version: Option<String>, | |
| 64 | + | pub(crate) unknown_keys: UnknownKeys, | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | /// What to do about keys in the target file that no field declares. | |
| 68 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 69 | + | pub(crate) enum UnknownKeys { | |
| 70 | + | /// Keep them through the edit round-trip and report an info diagnostic. | |
| 71 | + | /// The default, so that a tool release adding a key cannot brick the editor. | |
| 72 | + | Preserve, | |
| 73 | + | /// Treat an undeclared key as an error. | |
| 74 | + | Error, | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | /// One collapsible pane. Fields fall into it by path prefix. | |
| 78 | + | #[derive(Debug)] | |
| 79 | + | pub(crate) struct Section { | |
| 80 | + | pub(crate) path: String, | |
| 81 | + | pub(crate) description: Option<String>, | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | /// One atomic edit unit: a dotted path into the target file, plus how to render | |
| 85 | + | /// and constrain its value. | |
| 86 | + | #[derive(Debug)] | |
| 87 | + | pub(crate) struct Field { | |
| 88 | + | /// Dotted TOML path. Hyphens are literal key names, not separators. | |
| 89 | + | pub(crate) path: String, | |
| 90 | + | pub(crate) description: Option<String>, | |
| 91 | + | pub(crate) required: bool, | |
| 92 | + | pub(crate) kind: FieldKind, | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | /// The value half of a field: its type, its constraints, and its default. | |
| 96 | + | /// | |
| 97 | + | /// One enum rather than one struct per type, for the reason recorded in | |
| 98 | + | /// [[alloy-console]]: the field type is runtime data read out of a schema file, | |
| 99 | + | /// so there is nothing for the compiler to check, and `AlloyForm` has to hold a | |
| 100 | + | /// heterogeneous row list regardless. | |
| 101 | + | #[derive(Debug)] | |
| 102 | + | pub(crate) enum FieldKind { | |
| 103 | + | Bool { | |
| 104 | + | default: Option<bool>, | |
| 105 | + | }, | |
| 106 | + | Int { | |
| 107 | + | default: Option<i64>, | |
| 108 | + | range: Option<(i64, i64)>, | |
| 109 | + | }, | |
| 110 | + | Float { | |
| 111 | + | default: Option<f64>, | |
| 112 | + | range: Option<(f64, f64)>, | |
| 113 | + | }, | |
| 114 | + | Str { | |
| 115 | + | default: Option<String>, | |
| 116 | + | /// Held as written. Applying it needs a regex engine, which is a | |
| 117 | + | /// dependency the bind layer gets to decide on; no shipped schema uses | |
| 118 | + | /// one yet. | |
| 119 | + | pattern: Option<String>, | |
| 120 | + | }, | |
| 121 | + | Color { | |
| 122 | + | default: Option<String>, | |
| 123 | + | format: ColorFormat, | |
| 124 | + | }, | |
| 125 | + | Path { | |
| 126 | + | default: Option<String>, | |
| 127 | + | format: PathFormat, | |
| 128 | + | must_exist: bool, | |
| 129 | + | }, | |
| 130 | + | Enum { | |
| 131 | + | default: Option<String>, | |
| 132 | + | values: Vec<EnumValue>, | |
| 133 | + | }, | |
| 134 | + | /// A repeating record, e.g. `[[bindings.keys]]`. Read-only in v1. | |
| 135 | + | List { | |
| 136 | + | element: Vec<Field>, | |
| 137 | + | }, | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | impl FieldKind { | |
| 141 | + | /// The `type = ` spelling this kind came from, for diagnostics. | |
| 142 | + | pub(crate) fn type_name(&self) -> &'static str { | |
| 143 | + | match self { | |
| 144 | + | Self::Bool { .. } => "bool", | |
| 145 | + | Self::Int { .. } => "int", | |
| 146 | + | Self::Float { .. } => "float", | |
| 147 | + | Self::Str { .. } => "string", | |
| 148 | + | Self::Color { .. } => "color", | |
| 149 | + | Self::Path { .. } => "path", | |
| 150 | + | Self::Enum { .. } => "enum", | |
| 151 | + | Self::List { .. } => "list", | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 157 | + | pub(crate) enum ColorFormat { | |
| 158 | + | /// `#rrggbb` | |
| 159 | + | Hex, | |
| 160 | + | /// `#rrggbbaa` | |
| 161 | + | HexAlpha, | |
| 162 | + | /// Any CSS color notation; not shape-checked here. | |
| 163 | + | Css, | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 167 | + | pub(crate) enum PathFormat { | |
| 168 | + | File, | |
| 169 | + | Dir, | |
| 170 | + | Any, | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | /// One choice in an `enum` field. | |
| 174 | + | /// | |
| 175 | + | /// Flat schema entries (`values = ["High", "Low"]`) label themselves; the | |
| 176 | + | /// structured form exists for raw values that read badly on their own, which is | |
| 177 | + | /// why rio's `"Disabled"` gets "No decorations (recommended under Sway)". | |
| 178 | + | #[derive(Debug)] | |
| 179 | + | pub(crate) struct EnumValue { | |
| 180 | + | pub(crate) value: String, | |
| 181 | + | pub(crate) label: String, | |
| 182 | + | pub(crate) description: Option<String>, | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | /// A bundle of values applied in one action: one dirty increment, one undo entry. | |
| 186 | + | #[derive(Debug)] | |
| 187 | + | pub(crate) struct Preset { | |
| 188 | + | pub(crate) name: String, | |
| 189 | + | pub(crate) description: Option<String>, | |
| 190 | + | /// Field path to value, sorted by path. Every path here names a declared | |
| 191 | + | /// field; that is checked at parse time. | |
| 192 | + | pub(crate) values: Vec<(String, Value)>, | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | impl Schema { | |
| 196 | + | /// Read and parse a schema file. | |
| 197 | + | pub(crate) fn load(path: &Path) -> Result<Self> { | |
| 198 | + | let text = std::fs::read_to_string(path) | |
| 199 | + | .with_context(|| format!("reading schema {}", path.display()))?; | |
| 200 | + | Self::parse(&text).with_context(|| format!("in schema {}", path.display())) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | pub(crate) fn parse(text: &str) -> Result<Self> { | |
| 204 | + | let raw: RawSchema = toml::from_str(text)?; | |
| 205 | + | ||
| 206 | + | ensure!( | |
| 207 | + | raw.schema.schema_version == DSL_VERSION, | |
| 208 | + | "schema_version is \"{}\", and this console implements schema-DSL v{DSL_VERSION}", | |
| 209 | + | raw.schema.schema_version, | |
| 210 | + | ); | |
| 211 | + | ||
| 212 | + | let unknown_keys = match raw.schema.unknown_keys.as_deref() { | |
| 213 | + | None | Some("preserve") => UnknownKeys::Preserve, | |
| 214 | + | Some("error") => UnknownKeys::Error, | |
| 215 | + | Some(other) => bail!("unknown_keys is \"{other}\", expected \"preserve\" or \"error\""), | |
| 216 | + | }; | |
| 217 | + | ||
| 218 | + | let header = Header { | |
| 219 | + | target: raw.schema.target, | |
| 220 | + | target_tool: raw.schema.target_tool, | |
| 221 | + | target_version: raw.schema.target_version, | |
| 222 | + | unknown_keys, | |
| 223 | + | }; | |
| 224 | + | ||
| 225 | + | let sections = raw | |
| 226 | + | .section | |
| 227 | + | .into_iter() | |
| 228 | + | .map(|section| Section { | |
| 229 | + | path: section.path, | |
| 230 | + | description: section.description, | |
| 231 | + | }) | |
| 232 | + | .collect(); | |
| 233 | + | ||
| 234 | + | // Declaration order across both blocks: fields first, then the groups | |
| 235 | + | // they are sugar for. The schema file already reads that way (rio | |
| 236 | + | // declares `[[group]] colors` after every `[[field]]`), and a form whose | |
| 237 | + | // row order depends on how the author interleaved two block types would | |
| 238 | + | // be a surprise waiting to happen. | |
| 239 | + | let mut fields = Vec::new(); | |
| 240 | + | for field in raw.field { | |
| 241 | + | let path = field.path.clone(); | |
| 242 | + | fields.push( | |
| 243 | + | field | |
| 244 | + | .normalize() | |
| 245 | + | .with_context(|| format!("field `{path}`"))?, | |
| 246 | + | ); | |
| 247 | + | } | |
| 248 | + | for group in raw.group { | |
| 249 | + | fields.extend( | |
| 250 | + | group | |
| 251 | + | .expand() | |
| 252 | + | .map(|expanded| expanded.with_context(|| "group")) | |
| 253 | + | .collect::<Result<Vec<_>>>()?, | |
| 254 | + | ); | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | // A duplicate path is two rows editing one key, where the second edit | |
| 258 | + | // silently wins. Cheap to catch, impossible to see in a rendered form. | |
| 259 | + | for (index, field) in fields.iter().enumerate() { | |
| 260 | + | if let Some(earlier) = fields[..index] | |
| 261 | + | .iter() | |
| 262 | + | .find(|other| other.path == field.path) | |
| 263 | + | { | |
| 264 | + | bail!("`{}` is declared twice", earlier.path); | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | let presets = raw | |
| 269 | + | .preset | |
| 270 | + | .into_iter() | |
| 271 | + | .map(|preset| preset.normalize(&fields)) | |
| 272 | + | .collect::<Result<Vec<_>>>()?; | |
| 273 | + | ||
| 274 | + | Ok(Self { | |
| 275 | + | header, | |
| 276 | + | sections, | |
| 277 | + | fields, | |
| 278 | + | presets, | |
| 279 | + | }) | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | /// The field at a dotted path, if the schema declares one. | |
| 283 | + | pub(crate) fn field(&self, path: &str) -> Option<&Field> { | |
| 284 | + | self.fields.iter().find(|field| field.path == path) | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | /// The section a field's path falls into. | |
| 288 | + | /// | |
| 289 | + | /// Longest match wins, so a schema declaring both `colors` and | |
| 290 | + | /// `colors.bright` puts `colors.bright.red` under the more specific one. | |
| 291 | + | /// Matching is on whole path segments: a section `font` does not swallow | |
| 292 | + | /// `fonts.size`. | |
| 293 | + | pub(crate) fn section_of(&self, path: &str) -> Option<&Section> { | |
| 294 | + | self.sections | |
| 295 | + | .iter() | |
| 296 | + | .filter(|section| { | |
| 297 | + | path == section.path | |
| 298 | + | || path | |
| 299 | + | .strip_prefix(§ion.path) | |
| 300 | + | .is_some_and(|rest| rest.starts_with('.')) | |
| 301 | + | }) | |
| 302 | + | .max_by_key(|section| section.path.len()) | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | // --------------------------------------------------------------------------- | |
| 307 | + | // The file as written. `deny_unknown_fields` throughout: a mistyped `rnage` | |
| 308 | + | // would otherwise drop a constraint without a word, and `schema_version` is | |
| 309 | + | // what carries forward compatibility, so leniency here buys nothing. | |
| 310 | + | // --------------------------------------------------------------------------- | |
| 311 | + | ||
| 312 | + | #[derive(Deserialize)] | |
| 313 | + | #[serde(deny_unknown_fields)] | |
| 314 | + | struct RawSchema { | |
| 315 | + | schema: RawHeader, | |
| 316 | + | #[serde(default)] | |
| 317 | + | section: Vec<RawSection>, | |
| 318 | + | #[serde(default)] | |
| 319 | + | field: Vec<RawField>, | |
| 320 | + | #[serde(default)] | |
| 321 | + | group: Vec<RawGroup>, | |
| 322 | + | #[serde(default)] | |
| 323 | + | preset: Vec<RawPreset>, | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | #[derive(Deserialize)] | |
| 327 | + | #[serde(deny_unknown_fields)] | |
| 328 | + | struct RawHeader { | |
| 329 | + | target: String, | |
| 330 | + | target_tool: String, | |
| 331 | + | target_version: Option<String>, | |
| 332 | + | schema_version: String, | |
| 333 | + | unknown_keys: Option<String>, | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | #[derive(Deserialize)] | |
| 337 | + | #[serde(deny_unknown_fields)] | |
| 338 | + | struct RawSection { | |
| 339 | + | path: String, | |
| 340 | + | description: Option<String>, | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | #[derive(Deserialize)] | |
| 344 | + | #[serde(deny_unknown_fields)] | |
| 345 | + | struct RawField { | |
| 346 | + | path: String, | |
| 347 | + | #[serde(rename = "type")] | |
| 348 | + | ty: String, | |
| 349 | + | description: Option<String>, | |
| 350 | + | default: Option<Value>, | |
| 351 | + | range: Option<Vec<Value>>, | |
| 352 | + | pattern: Option<String>, | |
| 353 | + | values: Option<Vec<Value>>, | |
| 354 | + | format: Option<String>, | |
| 355 | + | must_exist: Option<bool>, | |
| 356 | + | required: Option<bool>, | |
| 357 | + | element: Option<Value>, | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | #[derive(Deserialize)] | |
| 361 | + | #[serde(deny_unknown_fields)] | |
| 362 | + | struct RawElement { | |
| 363 | + | #[serde(rename = "type")] | |
| 364 | + | ty: String, | |
| 365 | + | fields: Vec<RawField>, | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | #[derive(Deserialize)] | |
| 369 | + | #[serde(deny_unknown_fields)] | |
| 370 | + | struct RawGroup { | |
| 371 | + | path: String, | |
| 372 | + | #[serde(rename = "type")] | |
| 373 | + | ty: String, | |
| 374 | + | description: Option<String>, | |
| 375 | + | format: Option<String>, | |
| 376 | + | entries: Vec<RawEntry>, | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | #[derive(Deserialize)] | |
| 380 | + | #[serde(deny_unknown_fields)] | |
| 381 | + | struct RawEntry { | |
| 382 | + | key: String, | |
| 383 | + | default: Option<Value>, | |
| 384 | + | description: Option<String>, | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | #[derive(Deserialize)] | |
| 388 | + | #[serde(deny_unknown_fields)] | |
| 389 | + | struct RawPreset { | |
| 390 | + | name: String, | |
| 391 | + | description: Option<String>, | |
| 392 | + | values: toml::Table, | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | impl RawField { | |
| 396 | + | fn normalize(self) -> Result<Field> { | |
| 397 | + | let kind = self.kind()?; | |
| 398 | + | Ok(Field { | |
| 399 | + | path: self.path, | |
| 400 | + | description: self.description, | |
| 401 | + | required: self.required.unwrap_or(false), | |
| 402 | + | kind, | |
| 403 | + | }) | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | fn kind(&self) -> Result<FieldKind> { | |
| 407 | + | // Every arm rejects the constraints that do not belong to it, so a | |
| 408 | + | // `range` on a string is a parse error rather than a line the author | |
| 409 | + | // believes is doing something. | |
| 410 | + | let kind = match self.ty.as_str() { | |
| 411 | + | "bool" => { | |
| 412 | + | self.reject(&["range", "pattern", "values", "format", "element"])?; | |
| 413 | + | FieldKind::Bool { | |
| 414 | + | default: self.default.as_ref().map(as_bool).transpose()?, | |
| 415 | + | } | |
| 416 | + | } | |
| 417 | + | "int" => { | |
| 418 | + | self.reject(&["pattern", "values", "format", "element"])?; | |
| 419 | + | let range = self.int_range()?; | |
| 420 | + | let default = self.default.as_ref().map(as_int).transpose()?; | |
| 421 | + | if let (Some(value), Some((low, high))) = (default, range) { | |
| 422 | + | ensure!( | |
| 423 | + | (low..=high).contains(&value), | |
| 424 | + | "default {value} is outside range [{low}, {high}]", | |
| 425 | + | ); | |
| 426 | + | } | |
| 427 | + | FieldKind::Int { default, range } | |
| 428 | + | } | |
| 429 | + | "float" => { | |
| 430 | + | self.reject(&["pattern", "values", "format", "element"])?; | |
| 431 | + | let range = self.float_range()?; | |
| 432 | + | let default = self.default.as_ref().map(as_float).transpose()?; | |
| 433 | + | if let (Some(value), Some((low, high))) = (default, range) { | |
| 434 | + | ensure!( | |
| 435 | + | (low..=high).contains(&value), | |
| 436 | + | "default {value} is outside range [{low}, {high}]", | |
| 437 | + | ); | |
| 438 | + | } | |
| 439 | + | FieldKind::Float { default, range } | |
| 440 | + | } | |
| 441 | + | "string" => { | |
| 442 | + | self.reject(&["range", "values", "format", "element"])?; | |
| 443 | + | FieldKind::Str { | |
| 444 | + | default: self.default.as_ref().map(as_string).transpose()?, | |
| 445 | + | pattern: self.pattern.clone(), | |
| 446 | + | } | |
| 447 | + | } | |
| 448 | + | "color" => { | |
| 449 | + | self.reject(&["range", "pattern", "values", "element"])?; | |
| 450 | + | let format = color_format(self.format.as_deref())?; | |
| 451 | + | let default = self.default.as_ref().map(as_string).transpose()?; | |
| 452 | + | if let Some(value) = &default { | |
| 453 | + | check_color(value, format)?; | |
| 454 | + | } | |
| 455 | + | FieldKind::Color { default, format } | |
| 456 | + | } | |
| 457 | + | "path" => { | |
| 458 | + | self.reject(&["range", "pattern", "values", "element"])?; | |
| 459 | + | FieldKind::Path { | |
| 460 | + | default: self.default.as_ref().map(as_string).transpose()?, | |
| 461 | + | format: path_format(self.format.as_deref())?, | |
| 462 | + | must_exist: self.must_exist.unwrap_or(false), | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | "enum" => { | |
| 466 | + | self.reject(&["range", "pattern", "format", "element"])?; | |
| 467 | + | let values = self.enum_values()?; | |
| 468 | + | let default = self.default.as_ref().map(as_string).transpose()?; | |
| 469 | + | if let Some(value) = &default { | |
| 470 | + | ensure!( | |
| 471 | + | values.iter().any(|choice| &choice.value == value), | |
| 472 | + | "default \"{value}\" is not one of the declared values", | |
| 473 | + | ); | |
| 474 | + | } | |
| 475 | + | FieldKind::Enum { default, values } | |
| 476 | + | } | |
| 477 | + | "list" => { | |
| 478 | + | self.reject(&["range", "pattern", "values", "format"])?; | |
| 479 | + | FieldKind::List { | |
| 480 | + | element: self.list_element()?, | |
| 481 | + | } | |
| 482 | + | } | |
| 483 | + | other => bail!( | |
| 484 | + | "type \"{other}\" is not a schema-DSL v{DSL_VERSION} type \ | |
| 485 | + | (bool, int, float, string, color, path, enum, list)", | |
| 486 | + | ), | |
| 487 | + | }; | |
| 488 | + | Ok(kind) | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | /// Error out on any constraint that does not apply to this field's type. | |
| 492 | + | fn reject(&self, names: &[&str]) -> Result<()> { | |
| 493 | + | for name in names { | |
| 494 | + | let present = match *name { | |
| 495 | + | "range" => self.range.is_some(), | |
| 496 | + | "pattern" => self.pattern.is_some(), | |
| 497 | + | "values" => self.values.is_some(), | |
| 498 | + | "format" => self.format.is_some(), | |
| 499 | + | "element" => self.element.is_some(), | |
| 500 | + | _ => false, |
Lines truncated