Add `schema_generator` for generating JSON schemas (#23991)

Marshall Bowers created

This PR adds a `schema_generator` crate that can be used to generate our
various JSON schemas for publishing elsewhere.

Currently it does the simplest thing possible and just prints the JSON
schema to stdout. We can make this a but more robust later.

I also removed the schema-printing facilities from the `theme_importer`,
as they don't really make sense there.

Release Notes:

- N/A

Change summary

Cargo.lock                          | 14 ++++++
Cargo.toml                          |  1 
crates/schema_generator/Cargo.toml  | 18 +++++++++
crates/schema_generator/LICENSE-GPL |  1 
crates/schema_generator/src/main.rs | 26 +++++++++++++
crates/theme_importer/Cargo.toml    |  1 
crates/theme_importer/src/main.rs   | 61 ++++++++----------------------
7 files changed, 76 insertions(+), 46 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11438,6 +11438,19 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "schema_generator"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "env_logger 0.11.6",
+ "schemars",
+ "serde",
+ "serde_json",
+ "theme",
+]
+
 [[package]]
 name = "schemars"
 version = "0.8.21"
@@ -13278,7 +13291,6 @@ dependencies = [
  "log",
  "palette",
  "rust-embed",
- "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",

Cargo.toml 🔗

@@ -108,6 +108,7 @@ members = [
     "crates/rich_text",
     "crates/rope",
     "crates/rpc",
+    "crates/schema_generator",
     "crates/search",
     "crates/semantic_index",
     "crates/semantic_version",

crates/schema_generator/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "schema_generator"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { workspace = true, features = ["derive"] }
+env_logger.workspace = true
+schemars = { workspace = true, features = ["indexmap2"] }
+serde.workspace = true
+serde_json.workspace = true
+theme.workspace = true

crates/schema_generator/src/main.rs 🔗

@@ -0,0 +1,26 @@
+use anyhow::Result;
+use clap::Parser;
+use schemars::schema_for;
+use theme::{IconThemeFamilyContent, ThemeFamilyContent};
+
+#[derive(Parser, Debug)]
+struct Args {}
+
+fn main() -> Result<()> {
+    env_logger::init();
+
+    let _args = Args::parse();
+
+    let theme_family_schema = schema_for!(ThemeFamilyContent);
+    println!("Theme Schema:");
+    println!("{}", serde_json::to_string_pretty(&theme_family_schema)?);
+
+    let icon_theme_family_schema = schema_for!(IconThemeFamilyContent);
+    println!("Icon Theme Schema:");
+    println!(
+        "{}",
+        serde_json::to_string_pretty(&icon_theme_family_schema)?
+    );
+
+    Ok(())
+}

crates/theme_importer/Cargo.toml 🔗

@@ -16,7 +16,6 @@ indexmap.workspace = true
 log.workspace = true
 palette.workspace = true
 rust-embed.workspace = true
-schemars = { workspace = true, features = ["indexmap2"] }
 serde.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true

crates/theme_importer/src/main.rs 🔗

@@ -7,14 +7,13 @@ use std::io::Write;
 use std::path::PathBuf;
 
 use anyhow::{Context as _, Result};
-use clap::{Parser, Subcommand};
+use clap::Parser;
 use indexmap::IndexMap;
 use log::LevelFilter;
-use schemars::schema_for;
 use serde::Deserialize;
 use simplelog::ColorChoice;
 use simplelog::{TermLogger, TerminalMode};
-use theme::{Appearance, AppearanceContent, ThemeFamilyContent};
+use theme::{Appearance, AppearanceContent};
 
 use crate::vscode::VsCodeTheme;
 use crate::vscode::VsCodeThemeConverter;
@@ -71,53 +70,25 @@ pub struct ThemeMetadata {
 #[derive(Parser)]
 #[command(author, version, about, long_about = None)]
 struct Args {
-    #[command(subcommand)]
-    command: Command,
-}
+    /// The path to the theme to import.
+    theme_path: PathBuf,
+
+    /// Whether to warn when values are missing from the theme.
+    #[arg(long)]
+    warn_on_missing: bool,
 
-#[derive(PartialEq, Subcommand)]
-enum Command {
-    /// Prints the JSON schema for a theme.
-    PrintSchema,
-    /// Converts a VSCode theme to Zed format [default]
-    Convert {
-        /// The path to the theme to import.
-        theme_path: PathBuf,
-
-        /// Whether to warn when values are missing from the theme.
-        #[arg(long)]
-        warn_on_missing: bool,
-
-        /// The path to write the output to.
-        #[arg(long, short)]
-        output: Option<PathBuf>,
-    },
+    /// The path to write the output to.
+    #[arg(long, short)]
+    output: Option<PathBuf>,
 }
 
 fn main() -> Result<()> {
     let args = Args::parse();
 
-    match args.command {
-        Command::PrintSchema => {
-            let theme_family_schema = schema_for!(ThemeFamilyContent);
-            println!(
-                "{}",
-                serde_json::to_string_pretty(&theme_family_schema).unwrap()
-            );
-            Ok(())
-        }
-        Command::Convert {
-            theme_path,
-            warn_on_missing,
-            output,
-        } => convert(theme_path, output, warn_on_missing),
-    }
-}
-
-fn convert(theme_file_path: PathBuf, output: Option<PathBuf>, warn_on_missing: bool) -> Result<()> {
     let log_config = {
         let mut config = simplelog::ConfigBuilder::new();
-        if !warn_on_missing {
+
+        if !args.warn_on_missing {
             config.add_filter_ignore_str("theme_printer");
         }
 
@@ -132,11 +103,13 @@ fn convert(theme_file_path: PathBuf, output: Option<PathBuf>, warn_on_missing: b
     )
     .expect("could not initialize logger");
 
+    let theme_file_path = args.theme_path;
+
     let theme_file = match File::open(&theme_file_path) {
         Ok(file) => file,
         Err(err) => {
             log::info!("Failed to open file at path: {:?}", theme_file_path);
-            return Err(err.into());
+            return Err(err)?;
         }
     };
 
@@ -159,7 +132,7 @@ fn convert(theme_file_path: PathBuf, output: Option<PathBuf>, warn_on_missing: b
     );
     let theme_json = serde_json::to_string_pretty(&theme).unwrap();
 
-    if let Some(output) = output {
+    if let Some(output) = args.output {
         let mut file = File::create(output)?;
         file.write_all(theme_json.as_bytes())?;
     } else {