dynamic_cli/config/schema.rs
1//! Configuration schema definitions
2//!
3//! This module defines all data structures for representing
4//! CLI/REPL configurations loaded from YAML or JSON files.
5//!
6//! # Main Components
7//!
8//! - [`CommandsConfig`]: Root configuration structure
9//! - [`CommandDefinition`]: Individual command specification
10//! - [`ArgumentType`]: Supported argument types
11//! - [`ValidationRule`]: Validation constraints
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Complete configuration for CLI/REPL commands
17///
18/// This is the root structure deserialized from YAML/JSON files.
19/// It contains metadata about the interface and all command definitions.
20///
21/// # Example YAML
22///
23/// ```yaml
24/// metadata:
25/// version: "1.0.0"
26/// prompt: "myapp"
27/// prompt_suffix: " > "
28/// commands:
29/// - name: hello
30/// description: "Say hello"
31/// # ... more fields
32/// global_options: []
33/// ```
34#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
35pub struct CommandsConfig {
36 /// Metadata about the application interface
37 pub metadata: Metadata,
38
39 /// List of all available commands
40 pub commands: Vec<CommandDefinition>,
41
42 /// Global options available to all commands
43 #[serde(default)]
44 pub global_options: Vec<OptionDefinition>,
45}
46
47/// Metadata for the CLI/REPL interface
48///
49/// Contains information about the application version
50/// and prompt customization for REPL mode.
51///
52/// # Fields
53///
54/// - `version`: Application version string
55/// - `prompt`: Command prompt prefix (e.g., "myapp")
56/// - `prompt_suffix`: Suffix after prompt (e.g., " > ")
57#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
58pub struct Metadata {
59 /// Application version (e.g., "1.0.0")
60 pub version: String,
61
62 /// Prompt prefix displayed in REPL mode
63 ///
64 /// Example: "chrom-rs" will display as "chrom-rs > "
65 pub prompt: String,
66
67 /// Prompt suffix (typically " > " or ": ")
68 #[serde(default = "default_prompt_suffix")]
69 pub prompt_suffix: String,
70}
71
72/// Default prompt suffix
73fn default_prompt_suffix() -> String {
74 " > ".to_string()
75}
76
77/// Definition of a single command
78///
79/// Describes a command with its arguments, options, and validation rules.
80/// Each command must have a corresponding handler implementation.
81///
82/// # Example
83///
84/// ```yaml
85/// name: simulate
86/// aliases: [sim, run]
87/// description: "Run a simulation"
88/// required: true
89/// arguments:
90/// - name: input_file
91/// arg_type: path
92/// required: true
93/// description: "Input configuration file"
94/// validation:
95/// - must_exist: true
96/// - extensions: [yaml, json]
97/// options: []
98/// implementation: "simulate_handler"
99/// continue_on_failure: false
100/// requires_success: false
101/// ```
102#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
103pub struct CommandDefinition {
104 /// Command name (used for invocation)
105 pub name: String,
106
107 /// Alternative names for the command
108 #[serde(default)]
109 pub aliases: Vec<String>,
110
111 /// Human-readable description for help text
112 pub description: String,
113
114 /// Whether this command is required to be implemented
115 ///
116 /// If true, the application will fail to start if no handler is registered.
117 #[serde(default)]
118 pub required: bool,
119
120 /// Positional arguments
121 #[serde(default)]
122 pub arguments: Vec<ArgumentDefinition>,
123
124 /// Named options (flags)
125 #[serde(default)]
126 pub options: Vec<OptionDefinition>,
127
128 /// Name of the handler implementation
129 ///
130 /// This string is used to match the command with its
131 /// registered handler in the CommandRegistry.
132 pub implementation: String,
133
134 /// Whether the chain continues when *this* command fails (DD-026).
135 ///
136 /// Governs the chain's behaviour when this command is one segment of a
137 /// multi-command CLI invocation (or a chained `run_script()` line) and
138 /// its handler returns an error:
139 ///
140 /// - `false` (default): stop-on-first-error — identical to today's
141 /// single-command exit behaviour, just applied per segment.
142 /// - `true`: the chain-position error is reported but execution
143 /// proceeds to the next segment. Intended for genuinely optional
144 /// steps (e.g. a non-critical secondary export).
145 ///
146 /// Has no effect outside a chain (a single, non-chained command
147 /// behaves exactly as it does today regardless of this value).
148 #[serde(default)]
149 pub continue_on_failure: bool,
150
151 /// Whether *this* command requires every earlier command in the chain
152 /// to have succeeded before it is allowed to run (DD-026).
153 ///
154 /// Not to be confused with [`Self::required`], which is an unrelated,
155 /// startup-time check ("a handler must be registered for this
156 /// command") — `requires_success` is a per-invocation, chain-position
157 /// concern evaluated at dispatch time, and only observable downstream
158 /// of an earlier command whose own `continue_on_failure` is `true`
159 /// (otherwise the chain has already stopped by the time this command
160 /// would run).
161 ///
162 /// - `false` (default): runs regardless of any earlier failure in the
163 /// chain (moot under the default stop-on-first policy).
164 /// - `true`: if any earlier command in the chain has already failed
165 /// (tracked via a running `chain_has_failure` flag, regardless of
166 /// that failure's own `continue_on_failure` value), this command is
167 /// skipped — not executed, not counted as an additional failure —
168 /// and reported distinctly from a chain-position error.
169 #[serde(default)]
170 pub requires_success: bool,
171}
172
173/// Definition of a positional argument
174///
175/// Positional arguments are required in order and don't have
176/// a flag prefix (unlike options).
177///
178/// # Example
179///
180/// ```yaml
181/// name: input_file
182/// arg_type: path
183/// required: true
184/// description: "Path to input file"
185/// validation:
186/// - must_exist: true
187/// - extensions: [yaml, yml]
188/// ```
189///
190/// To prevent a sensitive argument value from being written to the REPL
191/// history file, set `secure: true`:
192///
193/// ```yaml
194/// name: password
195/// arg_type: string
196/// required: true
197/// description: "User password"
198/// secure: true
199/// ```
200///
201/// When a command line contains at least one argument marked `secure: true`,
202/// the entire line is silently omitted from the history file. The command
203/// name itself is not filtered — only lines with a secure argument value
204/// are suppressed.
205#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
206pub struct ArgumentDefinition {
207 /// Argument name (used in error messages and documentation)
208 pub name: String,
209
210 /// Expected type of the argument
211 pub arg_type: ArgumentType,
212
213 /// Whether the argument is mandatory
214 pub required: bool,
215
216 /// Human-readable description
217 pub description: String,
218
219 /// Validation rules to apply
220 #[serde(default)]
221 pub validation: Vec<ValidationRule>,
222
223 /// Whether this argument carries a sensitive value.
224 ///
225 /// When `true`, any REPL command line that provides a value for this
226 /// argument is not written to the history file. Defaults to `false`.
227 #[serde(default)]
228 pub secure: bool,
229}
230
231/// Definition of a named option (flag)
232///
233/// Options are optional (by default) and can be specified
234/// with short (`-o`) or long (`--option`) forms.
235///
236/// # Example
237///
238/// ```yaml
239/// name: output
240/// short: o
241/// long: output
242/// option_type: path
243/// required: false
244/// default: "output.txt"
245/// description: "Output file path"
246/// choices: []
247/// ```
248#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
249pub struct OptionDefinition {
250 /// Option name (internal identifier)
251 pub name: String,
252
253 /// Short form (single character, e.g., "o" for -o)
254 pub short: Option<String>,
255
256 /// Long form (e.g., "output" for --output)
257 pub long: Option<String>,
258
259 /// Expected type of the option value
260 pub option_type: ArgumentType,
261
262 /// Whether this option is mandatory
263 #[serde(default)]
264 pub required: bool,
265
266 /// Default value if not specified
267 pub default: Option<String>,
268
269 /// Human-readable description
270 pub description: String,
271
272 /// Restricted set of allowed values
273 ///
274 /// If non-empty, the value must be one of these choices. When
275 /// `repeatable` is `true`, this list also doubles as the set of
276 /// valid discriminants for each occurrence (see [`Self::option_parameters`]).
277 #[serde(default)]
278 pub choices: Vec<String>,
279
280 /// Whether this option can appear more than once on a single
281 /// command line, each occurrence carrying its own discriminant
282 /// (from `choices`) and `key=value` sub-parameters.
283 ///
284 /// Defaults to `false` — fully backward-compatible with existing
285 /// scalar options.
286 #[serde(default)]
287 pub repeatable: bool,
288
289 /// Per-discriminant shape of the `key=value` sub-parameters accepted
290 /// by a repeatable option.
291 ///
292 /// Keys must exactly match the entries in [`Self::choices`] (enforced
293 /// by the config validator, not at deserialization time). Each value
294 /// reuses [`ArgumentDefinition`] — the same vocabulary already used
295 /// for positional `arguments:` — rather than a dedicated mini-schema
296 /// type.
297 ///
298 /// Ignored (and should be empty) when `repeatable` is `false`.
299 #[serde(default)]
300 pub option_parameters: HashMap<String, Vec<ArgumentDefinition>>,
301}
302
303/// Supported argument and option types
304///
305/// These types are used for automatic parsing and validation
306/// of user input.
307///
308/// # Serialization
309///
310/// Types are serialized as lowercase strings in YAML/JSON:
311/// - `String` → "string"
312/// - `Integer` → "integer"
313/// - `Float` → "float"
314/// - `Bool` → "bool"
315/// - `Path` → "path"
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
317#[serde(rename_all = "lowercase")]
318pub enum ArgumentType {
319 /// UTF-8 string
320 String,
321
322 /// Signed integer (i64)
323 Integer,
324
325 /// Floating-point number (f64)
326 Float,
327
328 /// Boolean value (true/false, yes/no, 1/0)
329 Bool,
330
331 /// File system path
332 ///
333 /// Represents a path that may or may not exist,
334 /// depending on validation rules.
335 Path,
336}
337
338impl ArgumentType {
339 /// Get the type name as a string for error messages
340 ///
341 /// # Example
342 ///
343 /// ```
344 /// use dynamic_cli::config::schema::ArgumentType;
345 ///
346 /// assert_eq!(ArgumentType::Integer.as_str(), "integer");
347 /// assert_eq!(ArgumentType::Path.as_str(), "path");
348 /// ```
349 pub fn as_str(&self) -> &'static str {
350 match self {
351 ArgumentType::String => "string",
352 ArgumentType::Integer => "integer",
353 ArgumentType::Float => "float",
354 ArgumentType::Bool => "bool",
355 ArgumentType::Path => "path",
356 }
357 }
358}
359
360/// Validation rules for arguments and options
361///
362/// These rules are applied after type parsing to enforce
363/// additional constraints on values.
364///
365/// # Variants
366///
367/// - `MustExist`: For paths, require that the file/directory exists
368/// - `Extensions`: For paths, restrict to specific file extensions
369/// - `Range`: For numbers, enforce min/max bounds
370///
371/// # Serialization
372///
373/// Rules use untagged enum serialization:
374///
375/// ```yaml
376/// # MustExist
377/// - must_exist: true
378///
379/// # Extensions
380/// - extensions: [yaml, yml, json]
381///
382/// # Range
383/// - min: 0.0
384/// max: 100.0
385/// ```
386#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
387#[serde(untagged)]
388pub enum ValidationRule {
389 /// Require that a path exists on the file system
390 MustExist { must_exist: bool },
391
392 /// Restrict file extensions (for path arguments)
393 ///
394 /// Extensions should be specified without the leading dot.
395 /// Example: `["yaml", "yml"]` matches "config.yaml" and "data.yml"
396 Extensions { extensions: Vec<String> },
397
398 /// Enforce numeric range constraints
399 ///
400 /// Either or both bounds can be specified:
401 /// - `min: Some(0.0), max: None` → x ≥ 0
402 /// - `min: None, max: Some(100.0)` → x ≤ 100
403 /// - `min: Some(0.0), max: Some(100.0)` → 0 ≤ x ≤ 100
404 Range { min: Option<f64>, max: Option<f64> },
405}
406
407impl CommandsConfig {
408 /// Create a minimal valid configuration for testing
409 ///
410 /// This is useful for unit tests and examples.
411 ///
412 /// # Example
413 ///
414 /// ```
415 /// use dynamic_cli::config::schema::CommandsConfig;
416 ///
417 /// let config = CommandsConfig::minimal();
418 /// assert_eq!(config.metadata.version, "0.1.0");
419 /// assert!(config.commands.is_empty());
420 /// ```
421 #[cfg(test)]
422 pub fn minimal() -> Self {
423 Self {
424 metadata: Metadata {
425 version: "0.1.0".to_string(),
426 prompt: "test".to_string(),
427 prompt_suffix: " > ".to_string(),
428 },
429 commands: vec![],
430 global_options: vec![],
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_argument_type_as_str() {
441 assert_eq!(ArgumentType::String.as_str(), "string");
442 assert_eq!(ArgumentType::Integer.as_str(), "integer");
443 assert_eq!(ArgumentType::Float.as_str(), "float");
444 assert_eq!(ArgumentType::Bool.as_str(), "bool");
445 assert_eq!(ArgumentType::Path.as_str(), "path");
446 }
447
448 #[test]
449 fn test_default_prompt_suffix() {
450 assert_eq!(default_prompt_suffix(), " > ");
451 }
452
453 #[test]
454 fn test_minimal_config() {
455 let config = CommandsConfig::minimal();
456
457 assert_eq!(config.metadata.version, "0.1.0");
458 assert_eq!(config.metadata.prompt, "test");
459 assert_eq!(config.metadata.prompt_suffix, " > ");
460 assert!(config.commands.is_empty());
461 assert!(config.global_options.is_empty());
462 }
463
464 #[test]
465 fn test_deserialize_argument_type() {
466 // Test YAML deserialization of ArgumentType
467 let yaml = r#"
468 type: string
469 "#;
470
471 #[derive(Deserialize)]
472 struct TestStruct {
473 #[serde(rename = "type")]
474 type_field: ArgumentType,
475 }
476
477 let result: TestStruct = serde_yaml::from_str(yaml).unwrap();
478 assert_eq!(result.type_field, ArgumentType::String);
479 }
480
481 #[test]
482 fn test_deserialize_metadata() {
483 let yaml = r#"
484 version: "1.0.0"
485 prompt: "myapp"
486 prompt_suffix: " $ "
487 "#;
488
489 let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
490
491 assert_eq!(metadata.version, "1.0.0");
492 assert_eq!(metadata.prompt, "myapp");
493 assert_eq!(metadata.prompt_suffix, " $ ");
494 }
495
496 #[test]
497 fn test_deserialize_metadata_with_default() {
498 // Test that prompt_suffix gets default value if not specified
499 let yaml = r#"
500 version: "1.0.0"
501 prompt: "myapp"
502 "#;
503
504 let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
505
506 assert_eq!(metadata.prompt_suffix, " > ");
507 }
508
509 #[test]
510 fn test_deserialize_command_definition() {
511 let yaml = r#"
512 name: test_cmd
513 aliases: [tc, test]
514 description: "A test command"
515 required: true
516 arguments: []
517 options: []
518 implementation: "test_handler"
519 "#;
520
521 let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
522
523 assert_eq!(cmd.name, "test_cmd");
524 assert_eq!(cmd.aliases, vec!["tc", "test"]);
525 assert_eq!(cmd.description, "A test command");
526 assert!(cmd.required);
527 assert_eq!(cmd.implementation, "test_handler");
528 // continue_on_failure / requires_success absent from YAML: both
529 // must default to false (DD-026, #53).
530 assert!(!cmd.continue_on_failure);
531 assert!(!cmd.requires_success);
532 }
533
534 #[test]
535 fn test_deserialize_command_definition_continue_on_failure_and_requires_success_default_false()
536 {
537 // Explicit companion to test_deserialize_command_definition,
538 // mirroring the secure-field default test below: confirms serde's
539 // #[serde(default)] applies even when the fields are wholly absent
540 // from an otherwise-complete config, not just incidentally true in
541 // the general fixture above.
542 let yaml = r#"
543 name: solve
544 description: "Run the solver"
545 implementation: "solve_handler"
546 "#;
547
548 let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
549
550 assert!(!cmd.continue_on_failure);
551 assert!(!cmd.requires_success);
552 }
553
554 #[test]
555 fn test_deserialize_command_definition_continue_on_failure_and_requires_success_explicit() {
556 let yaml = r#"
557 name: export
558 description: "Optional secondary export"
559 implementation: "export_handler"
560 continue_on_failure: true
561 requires_success: true
562 "#;
563
564 let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
565
566 assert!(cmd.continue_on_failure);
567 assert!(cmd.requires_success);
568 }
569
570 #[test]
571 fn test_command_definition_requires_success_distinct_from_required() {
572 // Guards against the two fields being confused with one another
573 // (DD-026): `required` is the pre-existing "handler must be
574 // registered at startup" check; `requires_success` is the new,
575 // unrelated per-invocation chain guard.
576 let yaml = r#"
577 name: cleanup
578 description: "Cleanup step"
579 required: true
580 implementation: "cleanup_handler"
581 requires_success: false
582 "#;
583
584 let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
585
586 assert!(cmd.required);
587 assert!(!cmd.requires_success);
588 }
589
590 #[test]
591 fn test_deserialize_argument_definition() {
592 let yaml = r#"
593 name: input_file
594 arg_type: path
595 required: true
596 description: "Input file"
597 validation:
598 - must_exist: true
599 - extensions: [yaml, yml]
600 "#;
601
602 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
603
604 assert_eq!(arg.name, "input_file");
605 assert_eq!(arg.arg_type, ArgumentType::Path);
606 assert!(arg.required);
607 assert_eq!(arg.description, "Input file");
608 assert_eq!(arg.validation.len(), 2);
609 // secure defaults to false when absent from YAML
610 assert!(!arg.secure);
611 }
612
613 #[test]
614 fn test_deserialize_argument_definition_secure_default_false() {
615 // When `secure` is absent from the YAML, serde must default to false.
616 let yaml = r#"
617 name: username
618 arg_type: string
619 required: true
620 description: "User name"
621 "#;
622
623 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
624 assert!(!arg.secure, "secure must default to false when absent");
625 }
626
627 #[test]
628 fn test_deserialize_argument_definition_secure_true() {
629 // When `secure: true` is present, it must be deserialised correctly.
630 let yaml = r#"
631 name: password
632 arg_type: string
633 required: true
634 description: "User password"
635 secure: true
636 "#;
637
638 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
639 assert!(arg.secure, "secure must be true when set in YAML");
640 }
641
642 #[test]
643 fn test_deserialize_argument_definition_secure_false_explicit() {
644 // Explicit `secure: false` must round-trip correctly.
645 let yaml = r#"
646 name: output
647 arg_type: path
648 required: false
649 description: "Output path"
650 secure: false
651 "#;
652
653 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
654 assert!(!arg.secure);
655 }
656
657 #[test]
658 fn test_serialize_argument_definition_secure_roundtrip() {
659 let original = ArgumentDefinition {
660 name: "secret".to_string(),
661 arg_type: ArgumentType::String,
662 required: true,
663 description: "A secret value".to_string(),
664 validation: vec![],
665 secure: true,
666 };
667
668 let yaml = serde_yaml::to_string(&original).unwrap();
669 let deserialized: ArgumentDefinition = serde_yaml::from_str(&yaml).unwrap();
670
671 assert_eq!(original, deserialized);
672 assert!(deserialized.secure);
673 }
674
675 #[test]
676 fn test_deserialize_option_definition() {
677 let yaml = r#"
678 name: output
679 short: o
680 long: output
681 option_type: path
682 required: false
683 default: "out.txt"
684 description: "Output file"
685 choices: []
686 "#;
687
688 let opt: OptionDefinition = serde_yaml::from_str(yaml).unwrap();
689
690 assert_eq!(opt.name, "output");
691 assert_eq!(opt.short, Some("o".to_string()));
692 assert_eq!(opt.long, Some("output".to_string()));
693 assert_eq!(opt.option_type, ArgumentType::Path);
694 assert!(!opt.required);
695 assert_eq!(opt.default, Some("out.txt".to_string()));
696 }
697
698 #[test]
699 fn test_deserialize_validation_rule_must_exist() {
700 let yaml = r#"
701 must_exist: true
702 "#;
703
704 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
705
706 assert_eq!(rule, ValidationRule::MustExist { must_exist: true });
707 }
708
709 #[test]
710 fn test_deserialize_validation_rule_extensions() {
711 let yaml = r#"
712 extensions: [yaml, yml, json]
713 "#;
714
715 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
716
717 match rule {
718 ValidationRule::Extensions { extensions } => {
719 assert_eq!(extensions, vec!["yaml", "yml", "json"]);
720 }
721 _ => panic!("Wrong variant"),
722 }
723 }
724
725 #[test]
726 fn test_deserialize_validation_rule_range() {
727 let yaml = r#"
728 min: 0.0
729 max: 100.0
730 "#;
731
732 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
733
734 match rule {
735 ValidationRule::Range { min, max } => {
736 assert_eq!(min, Some(0.0));
737 assert_eq!(max, Some(100.0));
738 }
739 _ => panic!("Wrong variant"),
740 }
741 }
742
743 #[test]
744 fn test_deserialize_full_config() {
745 let yaml = r#"
746 metadata:
747 version: "1.0.0"
748 prompt: "test"
749 prompt_suffix: " > "
750 commands:
751 - name: hello
752 aliases: []
753 description: "Say hello"
754 required: false
755 arguments: []
756 options: []
757 implementation: "hello_handler"
758 global_options: []
759 "#;
760
761 let config: CommandsConfig = serde_yaml::from_str(yaml).unwrap();
762
763 assert_eq!(config.metadata.version, "1.0.0");
764 assert_eq!(config.commands.len(), 1);
765 assert_eq!(config.commands[0].name, "hello");
766 }
767
768 #[test]
769 fn test_serialize_and_deserialize_roundtrip() {
770 let original = CommandsConfig {
771 metadata: Metadata {
772 version: "1.0.0".to_string(),
773 prompt: "test".to_string(),
774 prompt_suffix: " > ".to_string(),
775 },
776 commands: vec![CommandDefinition {
777 name: "cmd1".to_string(),
778 aliases: vec!["c1".to_string()],
779 description: "Test command".to_string(),
780 required: true,
781 arguments: vec![],
782 options: vec![],
783 implementation: "handler1".to_string(),
784 continue_on_failure: false,
785 requires_success: false,
786 }],
787 global_options: vec![],
788 };
789
790 // Serialize to YAML
791 let yaml = serde_yaml::to_string(&original).unwrap();
792
793 // Deserialize back
794 let deserialized: CommandsConfig = serde_yaml::from_str(&yaml).unwrap();
795
796 assert_eq!(original, deserialized);
797 }
798
799 #[test]
800 fn test_json_deserialization() {
801 let json = r#"
802 {
803 "metadata": {
804 "version": "1.0.0",
805 "prompt": "test",
806 "prompt_suffix": " > "
807 },
808 "commands": [],
809 "global_options": []
810 }
811 "#;
812
813 let config: CommandsConfig = serde_json::from_str(json).unwrap();
814
815 assert_eq!(config.metadata.version, "1.0.0");
816 assert_eq!(config.commands.len(), 0);
817 }
818}