Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion comet-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ path = "src/main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"
syn = { version = "2", features = ["full"] }
syn = { version = "2", features = ["full", "visit"] }
quote = "1"
proc-macro2 = "1"
toml = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
comet = { path = "..", default-features = false, features = ["nebula-schema"] }
69 changes: 68 additions & 1 deletion comet-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};
use clap::{Args, Parser, Subcommand, ValueEnum};

#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -29,6 +29,9 @@ pub enum Command {
/// Inspect Nebula RLS coverage.
#[command(subcommand)]
Rls(RlsCommand),
/// Inspect routes and generate typed RPC contracts/clients.
#[command(subcommand)]
Rpc(RpcCommand),
/// Run the project's test/release gate.
#[command(subcommand)]
Test(TestCommand),
Expand All @@ -46,6 +49,14 @@ pub enum RlsCommand {
Status(RlsStatusArgs),
}

#[derive(Subcommand)]
pub enum RpcCommand {
/// Emit the discovered RPC manifest as JSON.
Manifest(RpcManifestArgs),
/// Generate a typed RPC client from discovered routes.
Generate(RpcGenerateArgs),
}

#[derive(Args)]
pub struct NewArgs {
/// Project name; also used as the crate name and D1 database name.
Expand Down Expand Up @@ -178,6 +189,39 @@ pub struct RlsStatusArgs {
pub custom_predicate_rules: Vec<String>,
}

#[derive(Args)]
pub struct RpcManifestArgs {
/// Project directory to inspect. Defaults to the current directory.
#[arg(long)]
pub path: Option<PathBuf>,

/// File to write. Defaults to stdout.
#[arg(long)]
pub out: Option<PathBuf>,
}

#[derive(Args)]
pub struct RpcGenerateArgs {
/// Project directory to inspect. Defaults to the current directory.
#[arg(long)]
pub path: Option<PathBuf>,

/// Client language to generate.
#[arg(long)]
pub lang: RpcLanguage,

/// File to write. Defaults to stdout.
#[arg(long)]
pub out: Option<PathBuf>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum RpcLanguage {
Ts,
Dart,
Rust,
}

#[derive(Subcommand)]
pub enum TestCommand {
/// `cargo fmt --check` + `cargo test --lib`.
Expand All @@ -196,3 +240,26 @@ pub struct TestArgs {
#[arg(long)]
pub path: Option<PathBuf>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn rpc_generate_requires_supported_language() {
let result = Cli::try_parse_from([
"comet",
"rpc",
"generate",
"--lang",
"go",
"--out",
"client.go",
]);
let Err(error) = result else {
panic!("expected invalid rpc language to fail");
};

assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
}
}
1 change: 1 addition & 0 deletions comet-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod generate;
pub mod migrate;
pub mod new;
pub mod rls;
pub mod rpc;
pub mod test;
133 changes: 133 additions & 0 deletions comet-cli/src/commands/rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::fs;
use std::path::PathBuf;

use anyhow::{Context, Result, bail};

use crate::cli::{RpcGenerateArgs, RpcLanguage, RpcManifestArgs};
use crate::rpc;

pub fn manifest(args: RpcManifestArgs) -> Result<()> {
let project_dir = args.path.unwrap_or_else(|| PathBuf::from("."));
let manifest = rpc::discover_manifest(&project_dir)?;
if manifest.routes.is_empty() {
bail!(
"no Rocket route attributes found under {}; RPC manifest generation needs at least one #[get]/#[post]/#[put]/#[delete]/#[patch] route",
project_dir.join("src").display()
);
}
let json = serde_json::to_string_pretty(&manifest).context("serializing RPC manifest")?;

match args.out {
Some(path) => {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
fs::write(&path, json).with_context(|| format!("writing {}", path.display()))?;
}
None => println!("{json}"),
}

Ok(())
}

pub fn generate(args: RpcGenerateArgs) -> Result<()> {
let project_dir = args.path.unwrap_or_else(|| PathBuf::from("."));
let manifest = rpc::discover_manifest(&project_dir)?;
if !manifest
.routes
.iter()
.any(|route| route.support == rpc::RpcRouteSupport::Json)
{
bail!(
"no JSON RPC routes found under {}; client generation only supports routes with Json<T> inputs or outputs",
project_dir.join("src").display()
);
}
let output = match args.lang {
RpcLanguage::Ts => {
let types = rpc::discover_typescript_types(&project_dir, &manifest)?;
rpc::generate_typescript_client_with_types(&manifest, &types)
}
RpcLanguage::Dart => {
let types = rpc::discover_typescript_types(&project_dir, &manifest)?;
rpc::generate_dart_client_with_types(&manifest, &types)
}
RpcLanguage::Rust => {
let types = rpc::discover_typescript_types(&project_dir, &manifest)?;
rpc::generate_rust_client_with_types(&manifest, &types)
}
};

match args.out {
Some(path) => write_output(&path, output),
None => {
println!("{output}");
Ok(())
}
}
}

fn write_output(path: &PathBuf, output: String) -> Result<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}

fs::write(path, output).with_context(|| format!("writing {}", path.display()))
}

#[cfg(test)]
mod tests {
use std::fs;

use super::*;
use crate::cli::{RpcGenerateArgs, RpcLanguage};

#[test]
fn manifest_errors_when_no_routes_are_found() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("src")).unwrap();

let error = manifest(RpcManifestArgs {
path: Some(dir.path().to_path_buf()),
out: Some(dir.path().join("rpc.json")),
})
.unwrap_err();

assert!(
error
.to_string()
.contains("no Rocket route attributes found")
);
}

#[test]
fn generate_errors_when_no_json_routes_are_found() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("src")).unwrap();
fs::write(
dir.path().join("src/routes.rs"),
r#"
#[get("/")]
pub fn index() -> &'static str {
"ok"
}
"#,
)
.unwrap();

let error = generate(RpcGenerateArgs {
path: Some(dir.path().to_path_buf()),
lang: RpcLanguage::Ts,
out: Some(dir.path().join("client.ts")),
})
.unwrap_err();

assert!(error.to_string().contains("no JSON RPC routes found"));
}
}
7 changes: 6 additions & 1 deletion comet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ mod commands;
mod discover;
mod entity_introspect;
mod fieldspec;
mod rpc;
mod rustfile;
mod schema_dump;
mod snapshot;

use clap::Parser;
use cli::{AuthCommand, Command, GenerateCommand, MigrateCommand, RlsCommand, TestCommand};
use cli::{
AuthCommand, Command, GenerateCommand, MigrateCommand, RlsCommand, RpcCommand, TestCommand,
};

fn main() -> anyhow::Result<()> {
let cli = cli::Cli::parse();
Expand All @@ -23,6 +26,8 @@ fn main() -> anyhow::Result<()> {
Command::Migrate(MigrateCommand::Status(args)) => commands::migrate::status(args),
Command::Auth(AuthCommand::Init(args)) => commands::auth::init(args),
Command::Rls(RlsCommand::Status(args)) => commands::rls::status(args),
Command::Rpc(RpcCommand::Manifest(args)) => commands::rpc::manifest(args),
Command::Rpc(RpcCommand::Generate(args)) => commands::rpc::generate(args),
Command::Test(TestCommand::Unit(args)) => commands::test::unit(args),
Command::Test(TestCommand::Integration(args)) => commands::test::integration(args),
Command::Test(TestCommand::Perf(args)) => commands::test::perf(args),
Expand Down
Loading
Loading