[realm_builder] Format ErrorList messages

This CL makes the format for cm_fild_analyzer::error::ErrorList
structured, helping it be more readable for users. It turns:

```
[98805.927319][18733956][18733958][realm_builder_server] WARN: unable to replace realm decl: a component manifest failed validation: ErrorList { errs: [InvalidField(DeclField { decl: "ExposeProtocol", field: "source_name" }), InvalidField(DeclField { decl: "ExposeProtocol", field: "target_name" })] }
```

To:

```
[00060.285693][50201][50203][realm_builder_server] WARN: The constructed component declaration is invalid. Please fix all the listed errors:
  1. "secho_server" is referenced in ExposeProtocol.source but it does not appear in children. Declare one in the `children` section of the component declaration with the name "secho_server" in order to reference it.
  2. Field `target` for ExposeProtocol is invalid.
  3. Field `source_name` is missing for ExposeProtocol decl. Add this field to the component declaration.
For a reference as to how component declarations are authored, see https://fuchsia.dev/go/components/declaration. method=Realm.ReplaceRealmDecl
```

Fixed: 100731
Change-Id: I4b8659ca66586fa32bcd34f5a1eeadcdeac16dd6
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/689505
Commit-Queue: Yaneury Fermin <yaneury@google.com>
Reviewed-by: Gary Bressler <geb@google.com>
diff --git a/src/lib/fuchsia-component-test/realm_builder_server/src/main.rs b/src/lib/fuchsia-component-test/realm_builder_server/src/main.rs
index ed161a3..d8e1261 100644
--- a/src/lib/fuchsia-component-test/realm_builder_server/src/main.rs
+++ b/src/lib/fuchsia-component-test/realm_builder_server/src/main.rs
@@ -517,7 +517,10 @@
         options: ftest::ChildOptions,
     ) -> Result<(), RealmBuilderError> {
         if let Err(e) = cm_fidl_validator::validate(&component_decl) {
-            return Err(RealmBuilderError::InvalidComponentDeclWithName(name, e));
+            return Err(RealmBuilderError::InvalidComponentDeclWithName(
+                name,
+                to_tabulated_string(e),
+            ));
         }
         let child_realm_node = RealmNode2::new_from_decl(component_decl.fidl_into_native(), false);
         self.realm_node.add_child(name, options, child_realm_node).await
@@ -798,7 +801,7 @@
             });
         decl.children.get_or_insert(vec![]).extend(child_decls);
         if let Err(e) = cm_fidl_validator::validate(&decl) {
-            return Err(RealmBuilderError::InvalidComponentDecl(e));
+            return Err(RealmBuilderError::InvalidComponentDecl(to_tabulated_string(e)));
         }
         Ok(())
     }
@@ -915,8 +918,12 @@
             let fidl_decl = fuchsia_fs::read_file_fidl::<fcdecl::Component>(&file_proxy)
                 .await
                 .map_err(|e| RealmBuilderError::DeclReadError(relative_url.clone(), e))?;
-            cm_fidl_validator::validate(&fidl_decl)
-                .map_err(|e| RealmBuilderError::InvalidComponentDeclWithName(relative_url, e))?;
+            cm_fidl_validator::validate(&fidl_decl).map_err(|e| {
+                RealmBuilderError::InvalidComponentDeclWithName(
+                    relative_url,
+                    to_tabulated_string(e),
+                )
+            })?;
 
             let mut self_ = RealmNode2::new_from_decl(fidl_decl.fidl_into_native(), false);
             self_.component_loaded_from_pkg = true;
@@ -1061,9 +1068,10 @@
                 .await
             {
                 Ok(url) => Ok(url),
-                Err(e) => {
-                    Err(RealmBuilderError::InvalidComponentDeclWithName(walked_path.join("/"), e))
-                }
+                Err(e) => Err(RealmBuilderError::InvalidComponentDeclWithName(
+                    walked_path.join("/"),
+                    to_tabulated_string(e),
+                )),
             }
         }
         .boxed()
@@ -1630,11 +1638,11 @@
 
     /// A component declaration failed validation.
     #[error("The constructed component declaration is invalid. Please fix all the listed errors:\n{0}\nFor a reference as to how component declarations are authored, see https://fuchsia.dev/go/components/declaration.")]
-    InvalidComponentDecl(cm_fidl_validator::error::ErrorList),
+    InvalidComponentDecl(String),
 
     /// A component declaration failed validation.
     #[error("The component declaration for child \"{0}\" is invalid. Please fix all the listed errors:\n{1}\nFor a reference as to how component declarations are authored, see https://fuchsia.dev/go/components/declaration.")]
-    InvalidComponentDeclWithName(String, cm_fidl_validator::error::ErrorList),
+    InvalidComponentDeclWithName(String, String),
 
     /// The referenced child does not exist.
     #[error("No child exists with the name \"{0}\". Before fetching or changing its component declaration, a child must be added to the realm with the `AddChild` group of methods.")]
@@ -1752,6 +1760,21 @@
     url.trim().ends_with(".cmx")
 }
 
+// Formats an ErrorList into a tabulated string. This format is used to create
+// more readable user error messages.
+fn to_tabulated_string(errors: cm_fidl_validator::error::ErrorList) -> String {
+    let mut output = String::new();
+    for (i, err) in errors.errs.iter().enumerate() {
+        let is_last_element = errors.errs.len() - i == 1;
+        output.push_str(&format!("  {}. {}", i + 1, err));
+        if !is_last_element {
+            output.push('\n');
+        }
+    }
+
+    output
+}
+
 #[cfg(test)]
 mod tests {
     use {
diff --git a/src/sys/lib/cm_fidl_validator/src/error.rs b/src/sys/lib/cm_fidl_validator/src/error.rs
index e93c495..5cc3138 100644
--- a/src/sys/lib/cm_fidl_validator/src/error.rs
+++ b/src/sys/lib/cm_fidl_validator/src/error.rs
@@ -7,47 +7,67 @@
 /// Enum type that can represent any error encountered during validation.
 #[derive(Debug, Error, PartialEq, Clone)]
 pub enum Error {
-    #[error("{} missing {}", .0.decl, .0.field)]
+    #[error("Field `{}` is missing for {}.", .0.field, .0.decl)]
     MissingField(DeclField),
-    #[error("{} has empty {}", .0.decl, .0.field)]
+
+    #[error("Field `{}` is empty for {}.", .0.field, .0.decl)]
     EmptyField(DeclField),
-    #[error("{} has extraneous {}", .0.decl, .0.field)]
+
+    #[error("{} has unnecessary field `{}`.", .0.decl, .0.field)]
     ExtraneousField(DeclField),
-    #[error("\"{1}\" is a duplicate {} {}", .0.decl, .0.field)]
+
+    #[error("\"{1}\" is duplicated for field `{}` in {}.", .0.field, .0.decl)]
     DuplicateField(DeclField, String),
-    #[error("{} has invalid {}", .0.decl, .0.field)]
+
+    #[error("Field `{}` for {} is invalid.",  .0.field, .0.decl)]
     InvalidField(DeclField),
-    #[error("{}'s {} is too long", .0.decl, .0.field)]
-    FieldTooLong(DeclField),
-    #[error("\"{0}\" cannot declare a capability of type {1}")]
+
+    #[error("Field `{}` for {} is too long. The field must be up to {1} characters.", .0.field, .0.decl)]
+    FieldTooLong(DeclField, usize),
+
+    #[error("\"{0}\" cannot declare a capability of type `{1}`.")]
     InvalidCapabilityType(DeclField, String),
-    #[error("\"{0}\" target \"{1}\" is same as source")]
+
+    #[error("\"{0}\" target \"{1}\" is same as source.")]
     OfferTargetEqualsSource(String, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in children")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in children.")]
     InvalidChild(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in collections")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in collections.")]
     InvalidCollection(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in storage")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in storage.")]
     InvalidStorage(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in environments")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in environments.")]
     InvalidEnvironment(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in capabilities")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in capabilities.")]
     InvalidCapability(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in runners")]
+
+    #[error("\"{1}\" is referenced in {0} but it does not appear in runners.")]
     InvalidRunner(DeclField, String),
-    #[error("\"{1}\" is referenced in {0} but it does not appear in events")]
+
+    #[error("\"{1}\" is referenced in {0} but there is no corresponding event provided to the component. Specifically, the event has to be `use`d.")]
     EventStreamEventNotFound(DeclField, String),
-    #[error("dependency cycle(s) exist: {0}")]
+
+    #[error("There are dependency cycle(s): {0}.")]
     DependencyCycle(String),
-    #[error("{} \"{}\" path overlaps with {} \"{}\"", decl, path, other_decl, other_path)]
+
+    #[error("Path \"{path}\" from {decl} overlaps with \"{other_path}\" path from {other_decl}. Paths across decls must be unique in order to avoid namespace collisions.")]
     InvalidPathOverlap { decl: DeclField, path: String, other_decl: DeclField, other_path: String },
-    #[error("built-in capability decl {0} should not specify a source path, found \"{1}\"")]
+
+    #[error("Source path \"{1}\" provided to {0} decl is unnecessary. Built-in capabilities don't need this field as they originate from the framework.")]
     ExtraneousSourcePath(DeclField, String),
-    #[error("configuration schema defines a vector nested in another vector")]
+
+    #[error("Configuration schema defines a vector nested inside another vector. Vector can only contain numbers, booleans, and strings.")]
     NestedVector,
-    #[error("the availability in {0} for {1} must be set to \"required\" because the source is not \"parent\" or \"void\"")]
+
+    #[error("The `availability` field in {0} for {1} must be set to \"required\" because the source is not parent.")]
     AvailabilityMustBeRequired(DeclField, String),
-    #[error("the availability in {0} for {1} must be set to \"optional\" because the source is \"void\"")]
+
+    #[error("The `availability` field in {0} for {1} must be set to \"optional\" because the source is \"void\".")]
     AvailabilityMustBeOptional(DeclField, String),
 }
 
@@ -80,7 +100,18 @@
     }
 
     pub fn field_too_long(decl_type: impl Into<String>, keyword: impl Into<String>) -> Self {
-        Error::FieldTooLong(DeclField { decl: decl_type.into(), field: keyword.into() })
+        Error::FieldTooLong(
+            DeclField { decl: decl_type.into(), field: keyword.into() },
+            cm_types::MAX_NAME_LENGTH,
+        )
+    }
+
+    pub fn field_too_long_with_max(
+        decl_type: impl Into<String>,
+        keyword: impl Into<String>,
+        max: usize,
+    ) -> Self {
+        Error::FieldTooLong(DeclField { decl: decl_type.into(), field: keyword.into() }, max)
     }
 
     pub fn invalid_capability_type(
@@ -265,31 +296,37 @@
 
     #[test]
     fn test_errors() {
-        assert_eq!(format!("{}", Error::missing_field("Decl", "keyword")), "Decl missing keyword");
-        assert_eq!(format!("{}", Error::empty_field("Decl", "keyword")), "Decl has empty keyword");
+        assert_eq!(
+            format!("{}", Error::missing_field("Decl", "keyword")),
+            "Field `keyword` is missing for Decl."
+        );
+        assert_eq!(
+            format!("{}", Error::empty_field("Decl", "keyword")),
+            "Field `keyword` is empty for Decl."
+        );
         assert_eq!(
             format!("{}", Error::duplicate_field("Decl", "keyword", "foo")),
-            "\"foo\" is a duplicate Decl keyword"
+            "\"foo\" is duplicated for field `keyword` in Decl."
         );
         assert_eq!(
             format!("{}", Error::invalid_field("Decl", "keyword")),
-            "Decl has invalid keyword"
+            "Field `keyword` for Decl is invalid."
         );
         assert_eq!(
-            format!("{}", Error::field_too_long("Decl", "keyword")),
-            "Decl's keyword is too long"
+            format!("{}", Error::field_too_long_with_max("Decl", "keyword", 100)),
+            "Field `keyword` for Decl is too long. The field must be up to 100 characters."
         );
         assert_eq!(
             format!("{}", Error::invalid_child("Decl", "source", "child")),
-            "\"child\" is referenced in Decl.source but it does not appear in children"
+            "\"child\" is referenced in Decl.source but it does not appear in children."
         );
         assert_eq!(
             format!("{}", Error::invalid_collection("Decl", "source", "child")),
-            "\"child\" is referenced in Decl.source but it does not appear in collections"
+            "\"child\" is referenced in Decl.source but it does not appear in collections."
         );
         assert_eq!(
             format!("{}", Error::invalid_storage("Decl", "source", "name")),
-            "\"name\" is referenced in Decl.source but it does not appear in storage"
+            "\"name\" is referenced in Decl.source but it does not appear in storage."
         );
     }
 }
diff --git a/src/sys/lib/cm_fidl_validator/src/lib.rs b/src/sys/lib/cm_fidl_validator/src/lib.rs
index 0cc8930..91efc09 100644
--- a/src/sys/lib/cm_fidl_validator/src/lib.rs
+++ b/src/sys/lib/cm_fidl_validator/src/lib.rs
@@ -4518,15 +4518,15 @@
                 decl
             },
             result = Err(ErrorList::new(vec![
-                Error::field_too_long("UseEvent", "source_name"),
-                Error::field_too_long("UseEvent", "target_name"),
-                Error::field_too_long("UseService", "source_name"),
-                Error::field_too_long("UseService", "target_path"),
-                Error::field_too_long("UseProtocol", "source_name"),
-                Error::field_too_long("UseProtocol", "target_path"),
-                Error::field_too_long("UseDirectory", "source_name"),
-                Error::field_too_long("UseDirectory", "target_path"),
-                Error::field_too_long("UseStorage", "target_path"),
+                Error::field_too_long_with_max("UseEvent", "source_name", 100),
+                Error::field_too_long_with_max("UseEvent", "target_name", 100),
+                Error::field_too_long_with_max("UseService", "source_name", 100),
+                Error::field_too_long_with_max("UseService", "target_path", 1024),
+                Error::field_too_long_with_max("UseProtocol", "source_name", 100),
+                Error::field_too_long_with_max("UseProtocol", "target_path", 1024),
+                Error::field_too_long_with_max("UseDirectory", "source_name", 100),
+                Error::field_too_long_with_max("UseDirectory", "target_path", 1024),
+                Error::field_too_long_with_max("UseStorage", "target_path", 1024),
             ])),
         },
         test_validate_conflicting_paths => {
@@ -7828,9 +7828,9 @@
                 decl
             },
             result = Err(ErrorList::new(vec![
-                Error::field_too_long("Child", "name"),
-                Error::field_too_long("Child", "url"),
-                Error::field_too_long("Child", "environment"),
+                Error::field_too_long_with_max("Child", "name", 100),
+                Error::field_too_long_with_max("Child", "url", 4096),
+                Error::field_too_long_with_max("Child", "environment", 100),
                 Error::invalid_environment("Child", "environment", "a".repeat(1025)),
             ])),
         },
@@ -8108,19 +8108,19 @@
                 decl
             },
             result = Err(ErrorList::new(vec![
-                Error::field_too_long("Service", "name"),
-                Error::field_too_long("Service", "source_path"),
-                Error::field_too_long("Protocol", "name"),
-                Error::field_too_long("Protocol", "source_path"),
-                Error::field_too_long("Directory", "name"),
-                Error::field_too_long("Directory", "source_path"),
-                Error::field_too_long("Storage", "source.child.name"),
-                Error::field_too_long("Storage", "name"),
-                Error::field_too_long("Storage", "backing_dir"),
-                Error::field_too_long("Runner", "name"),
-                Error::field_too_long("Runner", "source_path"),
-                Error::field_too_long("Resolver", "name"),
-                Error::field_too_long("Resolver", "source_path"),
+                Error::field_too_long_with_max("Service", "name", 100),
+                Error::field_too_long_with_max("Service", "source_path", 1024),
+                Error::field_too_long_with_max("Protocol", "name", 100),
+                Error::field_too_long_with_max("Protocol", "source_path", 1024),
+                Error::field_too_long_with_max("Directory", "name", 100),
+                Error::field_too_long_with_max("Directory", "source_path", 1024),
+                Error::field_too_long_with_max("Storage", "source.child.name", 100),
+                Error::field_too_long_with_max("Storage", "name", 100),
+                Error::field_too_long_with_max("Storage", "backing_dir", 100),
+                Error::field_too_long_with_max("Runner", "name", 100),
+                Error::field_too_long_with_max("Runner", "source_path", 1024),
+                Error::field_too_long_with_max("Resolver", "name", 100),
+                Error::field_too_long_with_max("Resolver", "source_path", 1024),
             ])),
         },
         test_validate_capabilities_duplicate_name => {
diff --git a/src/sys/lib/cm_fidl_validator/src/util.rs b/src/sys/lib/cm_fidl_validator/src/util.rs
index b81ed4f..b8316da 100644
--- a/src/sys/lib/cm_fidl_validator/src/util.rs
+++ b/src/sys/lib/cm_fidl_validator/src/util.rs
@@ -45,7 +45,7 @@
     match prop {
         Some(prop) if prop.len() == 0 => errors.push(Error::empty_field(decl_type, keyword)),
         Some(prop) if prop.len() > max_len => {
-            errors.push(Error::field_too_long(decl_type, keyword))
+            errors.push(Error::field_too_long_with_max(decl_type, keyword, max_len))
         }
         Some(_) => (),
         None => errors.push(Error::missing_field(decl_type, keyword)),
@@ -438,7 +438,7 @@
         test_identifier_path_too_long => {
             check_fn = check_path,
             input = &format!("/{}", "a".repeat(1024)),
-            result = Err(ErrorList::new(vec![Error::field_too_long("FooDecl", "foo")])),
+            result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", /*max=*/1024usize)])),
         },
 
         // name
@@ -460,12 +460,12 @@
         test_identifier_name_too_long => {
             check_fn = check_name,
             input = &format!("{}", "a".repeat(MAX_NAME_LENGTH + 1)),
-            result = Err(ErrorList::new(vec![Error::field_too_long("FooDecl", "foo")])),
+            result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", MAX_NAME_LENGTH)])),
         },
         test_identifier_dynamic_name_too_long => {
             check_fn = check_dynamic_name,
             input = &format!("{}", "a".repeat(MAX_DYNAMIC_NAME_LENGTH + 1)),
-            result = Err(ErrorList::new(vec![Error::field_too_long("FooDecl", "foo")])),
+            result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", MAX_DYNAMIC_NAME_LENGTH)])),
         },
 
         // url
@@ -482,7 +482,7 @@
         test_identifier_url_too_long => {
             check_fn = check_url,
             input = &format!("fuchsia-pkg://{}", "a".repeat(4083)),
-            result = Err(ErrorList::new(vec![Error::field_too_long("FooDecl", "foo")])),
+            result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", 4096)])),
         },
     }
 }