Skip to content
Open
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
14 changes: 14 additions & 0 deletions crates/cli/src/spacetime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,20 @@ impl Default for CommandSchemaBuilder {
}

impl CommandSchema {
/// Return only values accepted by this command schema.
///
/// `SpacetimeConfig` stores entity-level fields like `database`, `server`, and
/// `module-path` together, while each command accepts only a subset of them.
/// This keeps command fallbacks from reimplementing schema-key filtering.
pub fn filter_config_values(&self, values: &HashMap<String, Value>) -> HashMap<String, Value> {
let valid_config_keys: HashSet<String> = self.keys.iter().map(|k| k.config_name().to_string()).collect();
values
.iter()
.filter(|(key, _)| valid_config_keys.contains(&key.replace('-', "_")))
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
}

/// Get a value from clap arguments only (not from config).
/// Useful for filtering or checking if a value was provided via CLI.
pub fn get_clap_arg<T: Clone + Send + Sync + 'static>(
Expand Down
190 changes: 148 additions & 42 deletions crates/cli/src/subcommands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use crate::spacetime_config::{
find_and_load_with_env, CommandConfig, CommandSchema, CommandSchemaBuilder, Key, LoadedConfig, SpacetimeConfig,
find_and_load_with_env, CommandConfig, CommandSchema, CommandSchemaBuilder, FlatTarget, Key, LoadedConfig,
SpacetimeConfig,
};
use crate::tasks::csharp::dotnet_format;
use crate::tasks::rust::rustfmt;
Expand Down Expand Up @@ -71,46 +72,7 @@ fn get_filtered_generate_configs<'a>(
schema: &'a CommandSchema,
args: &'a clap::ArgMatches,
) -> Result<Vec<CommandConfig<'a>>, anyhow::Error> {
// Get all database targets from config with parent→child inheritance
let all_targets = spacetime_config.collect_all_targets_with_inheritance();

if all_targets.is_empty() {
return Ok(vec![]);
}

// Filter by database name pattern (glob) if provided via CLI
let filtered_targets = if let Some(cli_database) = args.get_one::<String>("database") {
let pattern =
glob::Pattern::new(cli_database).with_context(|| format!("Invalid glob pattern: {cli_database}"))?;

let matched: Vec<_> = all_targets
.into_iter()
.filter(|target| {
target
.fields
.get("database")
.and_then(|v| v.as_str())
.is_some_and(|db| pattern.matches(db))
})
.collect();

if matched.is_empty() {
anyhow::bail!(
"No database target matches '{}'. Available databases: {}",
cli_database,
spacetime_config
.collect_all_targets_with_inheritance()
.iter()
.filter_map(|t| t.fields.get("database").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join(", ")
);
}

matched
} else {
all_targets
};
let filtered_targets = get_filtered_generate_targets(spacetime_config, args)?;

// Collect generate entries from matched targets, inheriting entity fields
// Deduplicate by (module_path, serialized_generate_entry)
Expand Down Expand Up @@ -165,6 +127,80 @@ fn get_filtered_generate_configs<'a>(
Ok(generate_configs)
}

fn get_filtered_generate_targets(
spacetime_config: &SpacetimeConfig,
args: &clap::ArgMatches,
) -> Result<Vec<FlatTarget>, anyhow::Error> {
// Get all database targets from config with parent→child inheritance
let all_targets = spacetime_config.collect_all_targets_with_inheritance();

if all_targets.is_empty() {
return Ok(vec![]);
}

// Filter by database name pattern (glob) if provided via CLI
if let Some(cli_database) = args.get_one::<String>("database") {
let pattern =
glob::Pattern::new(cli_database).with_context(|| format!("Invalid glob pattern: {cli_database}"))?;

let matched: Vec<_> = all_targets
.into_iter()
.filter(|target| {
target
.fields
.get("database")
.and_then(|v| v.as_str())
.is_some_and(|db| pattern.matches(db))
})
.collect();

if matched.is_empty() {
anyhow::bail!(
"No database target matches '{}'. Available databases: {}",
cli_database,
spacetime_config
.collect_all_targets_with_inheritance()
.iter()
.filter_map(|t| t.fields.get("database").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join(", ")
);
}

Ok(matched)
} else {
Ok(all_targets)
}
}

fn fallback_generate_configs_from_targets<'a>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document what this function's for

spacetime_config: &SpacetimeConfig,
command: &clap::Command,
schema: &'a CommandSchema,
args: &'a clap::ArgMatches,
) -> anyhow::Result<Vec<CommandConfig<'a>>> {
let targets = get_filtered_generate_targets(spacetime_config, args)?;
let mut configs = Vec::with_capacity(targets.len());
for target in targets {
configs.push(CommandConfig::new(
schema,
schema.filter_config_values(&target.fields),
args,
)?);
}

schema.validate_no_generate_entry_specific_cli_args(command, args, configs.len())?;
schema.validate_no_module_specific_cli_args_for_multiple_targets(
command,
args,
configs.len(),
"generating for multiple targets",
"Please specify the database name to select a single target, or remove these arguments.",
)?;

Ok(configs)
}

pub fn cli() -> clap::Command {
clap::Command::new("generate")
.about("Generate client files for a spacetime module.")
Expand Down Expand Up @@ -629,7 +665,12 @@ pub async fn exec_ex(
let (using_config, generate_configs) = if let Some(loaded) = loaded_config_ref {
let filtered = get_filtered_generate_configs(&loaded.config, &cmd, &schema, args)?;
if filtered.is_empty() {
(false, vec![CommandConfig::new(&schema, HashMap::new(), args)?])
let fallback = fallback_generate_configs_from_targets(&loaded.config, &cmd, &schema, args)?;
if fallback.is_empty() {
(false, vec![CommandConfig::new(&schema, HashMap::new(), args)?])
} else {
(true, fallback)
}
} else {
(true, filtered)
}
Expand Down Expand Up @@ -1357,6 +1398,71 @@ mod tests {
);
}

#[test]
fn test_fallback_generate_config_uses_selected_child_target_fields() {
let cmd = cli();
let schema = build_generate_config_schema(&cmd).unwrap();

let mut root_fields = HashMap::new();
root_fields.insert("module-path".to_string(), serde_json::json!("./root-module"));

let mut child_fields = HashMap::new();
child_fields.insert("database".to_string(), serde_json::json!("child-db"));
child_fields.insert("module-path".to_string(), serde_json::json!("./child-module"));

let spacetime_config = SpacetimeConfig {
additional_fields: root_fields,
children: Some(vec![SpacetimeConfig {
additional_fields: child_fields,
..Default::default()
}]),
..Default::default()
};

let matches = cmd.clone().get_matches_from(vec![
"generate",
"child-db",
"--lang",
"rust",
"--bin-path",
"dummy.wasm",
]);
let fallback = fallback_generate_configs_from_targets(&spacetime_config, &cmd, &schema, &matches).unwrap();

assert_eq!(fallback.len(), 1);
assert_eq!(
fallback[0].get_one::<PathBuf>("module_path").unwrap(),
Some(PathBuf::from("./child-module"))
);
}

#[test]
fn test_fallback_generate_config_filters_unsupported_entity_fields() {
let cmd = cli();
let schema = build_generate_config_schema(&cmd).unwrap();

let mut root_fields = HashMap::new();
root_fields.insert("database".to_string(), serde_json::json!("my-db"));
root_fields.insert("server".to_string(), serde_json::json!("local"));
root_fields.insert("module-path".to_string(), serde_json::json!("./server"));

let spacetime_config = SpacetimeConfig {
additional_fields: root_fields,
..Default::default()
};

let matches = cmd
.clone()
.get_matches_from(vec!["generate", "--lang", "rust", "--bin-path", "dummy.wasm"]);
let fallback = fallback_generate_configs_from_targets(&spacetime_config, &cmd, &schema, &matches).unwrap();

assert_eq!(fallback.len(), 1);
assert_eq!(
fallback[0].get_one::<PathBuf>("module_path").unwrap(),
Some(PathBuf::from("./server"))
);
}

#[test]
fn test_language_serde_deserialize_all_variants() {
// Verify all Language variants deserialize correctly from config JSON strings.
Expand Down
Loading
Loading