Add option to output crate update suggestions in JSON.

Test: manual
Change-Id: I941308df042473057a214e4274fe7cdf6d745869
diff --git a/tools/external_crates/crate_tool/src/main.rs b/tools/external_crates/crate_tool/src/main.rs
index bd9a617..9a2e9ea 100644
--- a/tools/external_crates/crate_tool/src/main.rs
+++ b/tools/external_crates/crate_tool/src/main.rs
@@ -120,6 +120,9 @@
         /// How strict to be about enforcing semver compatibility.
         #[arg(long, value_enum, default_value_t = SemverCompatibilityRule::Loose)]
         semver_compatibility: SemverCompatibilityRule,
+
+        #[arg(long, default_value_t = false)]
+        json: bool,
     },
     /// Update a crate to the specified version.
     Update {
@@ -244,8 +247,8 @@
         }
         Cmd::UpdatableCrates {} => managed_repo.updatable_crates(),
         Cmd::AnalyzeUpdates { crate_name } => managed_repo.analyze_updates(crate_name),
-        Cmd::SuggestUpdates { patches, semver_compatibility } => {
-            managed_repo.suggest_updates(patches, semver_compatibility).map(|_x| ())
+        Cmd::SuggestUpdates { patches, semver_compatibility, json } => {
+            managed_repo.suggest_updates(patches, semver_compatibility, json)
         }
         Cmd::Update { crate_name, version } => managed_repo.update(crate_name, version),
         Cmd::Init {} => managed_repo.init(),
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index 78e2e98..23b0410 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -32,6 +32,7 @@
 use name_and_version::{NameAndVersionMap, NameAndVersionRef, NamedAndVersioned};
 use rooted_path::RootedPath;
 use semver::Version;
+use serde::Serialize;
 use spdx::Licensee;
 
 use crate::{
@@ -76,6 +77,19 @@
     ])
 });
 
+#[derive(Serialize, Default, Debug)]
+struct UpdateSuggestions {
+    updates: Vec<UpdateSuggestion>,
+}
+
+#[derive(Serialize, Default, Debug)]
+struct UpdateSuggestion {
+    name: String,
+    #[serde(skip)]
+    old_version: String,
+    version: String,
+}
+
 pub struct ManagedRepo {
     path: RootedPath,
     crates_io: CratesIoIndex,
@@ -827,8 +841,9 @@
         &self,
         consider_patched_crates: bool,
         semver_compatibility: SemverCompatibilityRule,
-    ) -> Result<Vec<(String, String)>> {
-        let mut suggestions = Vec::new();
+        json: bool,
+    ) -> Result<()> {
+        let mut suggestions = UpdateSuggestions::default();
         let mut managed_crates = self.new_cc();
         managed_crates.add_from(self.managed_dir().rel())?;
         let legacy_crates = self.legacy_crates()?;
@@ -838,11 +853,13 @@
 
             let base_version = cio_crate.get_version(krate.version());
             if base_version.is_none() {
-                println!(
-                    "Skipping crate {} v{} because it was not found in crates.io",
-                    krate.name(),
-                    krate.version()
-                );
+                if !json {
+                    println!(
+                        "Skipping crate {} v{} because it was not found in crates.io",
+                        krate.name(),
+                        krate.version()
+                    );
+                }
                 continue;
             }
             let base_version = base_version.unwrap();
@@ -850,11 +867,13 @@
 
             let patch_dir = krate.path().join("patches").unwrap();
             if patch_dir.abs().exists() && !consider_patched_crates {
-                println!(
-                    "Skipping crate {} v{} because it has patches",
-                    krate.name(),
-                    krate.version()
-                );
+                if !json {
+                    println!(
+                        "Skipping crate {} v{} because it has patches",
+                        krate.name(),
+                        krate.version()
+                    );
+                }
                 continue;
             }
 
@@ -882,18 +901,28 @@
                     }
                     true
                 }) {
-                    println!(
-                        "Upgrade crate {} v{} to {}",
-                        krate.name(),
-                        krate.version(),
-                        version.version()
-                    );
-                    suggestions.push((krate.name().to_string(), version.version().to_string()));
+                    suggestions.updates.push(UpdateSuggestion {
+                        name: krate.name().to_string(),
+                        old_version: krate.version().to_string(),
+                        version: version.version().to_string(),
+                    });
                     break;
                 }
             }
         }
-        Ok(suggestions)
+
+        if json {
+            println!("{}", serde_json::to_string_pretty(&suggestions)?)
+        } else {
+            for suggestion in suggestions.updates {
+                println!(
+                    "Upgrade crate {} v{} to {}",
+                    suggestion.name, suggestion.old_version, suggestion.version,
+                );
+            }
+        }
+
+        Ok(())
     }
     pub fn update(&self, crate_name: impl AsRef<str>, version: impl AsRef<str>) -> Result<()> {
         let pseudo_crate = self.pseudo_crate();