mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-27 02:39:38 +02:00
feat(cli): omnigraph config view (merged config / origins / locator)
Add config view [--resolved] [--show-origin] [--json] [<graph>]: prints the merged layered config (null/empty values pruned for readability), or with --show-origin the layer each field came from (sorted, deterministic), or with --resolved <graph> the typed GraphLocator (embedded vs remote). Suppress the always-empty legacy project:/server: blocks on serialize.
This commit is contained in:
parent
059fbe4c4a
commit
2bf3e45d08
3 changed files with 284 additions and 4 deletions
|
|
@ -26,8 +26,8 @@ use omnigraph_compiler::{
|
||||||
json_params_to_param_map, lint_query_file,
|
json_params_to_param_map, lint_query_file,
|
||||||
};
|
};
|
||||||
use omnigraph_config::{
|
use omnigraph_config::{
|
||||||
AliasCommand, GraphLocator, OmnigraphConfig, ReadOutputFormat, graph_resource_id_for_selection,
|
AliasCommand, GraphLocator, OmnigraphConfig, Provenance, ReadOutputFormat,
|
||||||
load_layered_config,
|
graph_resource_id_for_selection, load_layered_config,
|
||||||
};
|
};
|
||||||
use omnigraph_policy::{
|
use omnigraph_policy::{
|
||||||
PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest, PolicyTestConfig,
|
PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest, PolicyTestConfig,
|
||||||
|
|
@ -313,6 +313,11 @@ enum Command {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: GraphsCommand,
|
command: GraphsCommand,
|
||||||
},
|
},
|
||||||
|
/// Inspect the merged configuration (global + project layers).
|
||||||
|
Config {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ConfigCommand,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Operations on the graph registry of a multi-graph server (MR-668).
|
/// Operations on the graph registry of a multi-graph server (MR-668).
|
||||||
|
|
@ -512,6 +517,27 @@ enum PolicyCommand {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum ConfigCommand {
|
||||||
|
/// Print the merged configuration and, optionally, where each value came from.
|
||||||
|
View {
|
||||||
|
/// Project config file (defaults to ./omnigraph.yaml).
|
||||||
|
#[arg(long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
/// Resolve the named graph (or `defaults.graph`) to its typed locator.
|
||||||
|
#[arg(long)]
|
||||||
|
resolved: bool,
|
||||||
|
/// Annotate each value with the layer (global/project) it came from.
|
||||||
|
#[arg(long = "show-origin")]
|
||||||
|
show_origin: bool,
|
||||||
|
/// Emit JSON instead of YAML.
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
/// Graph to resolve with `--resolved` (defaults to `defaults.graph`).
|
||||||
|
graph: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum QueriesCommand {
|
enum QueriesCommand {
|
||||||
/// Type-check the stored-query registry against the live schema.
|
/// Type-check the stored-query registry against the live schema.
|
||||||
|
|
@ -787,6 +813,147 @@ fn load_cli_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
||||||
Ok(loaded.config)
|
Ok(loaded.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `config view`: print the merged config, its per-field origin, or a resolved
|
||||||
|
/// graph locator. Reads the layered config directly (it needs the provenance the
|
||||||
|
/// CLI's normal path drops).
|
||||||
|
fn config_view(
|
||||||
|
config_path: Option<&PathBuf>,
|
||||||
|
resolved: bool,
|
||||||
|
show_origin: bool,
|
||||||
|
json: bool,
|
||||||
|
graph: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let loaded = load_layered_config(config_path)?;
|
||||||
|
for warning in &loaded.warnings {
|
||||||
|
eprintln!("warning: {warning}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolved {
|
||||||
|
let locator = loaded.config.resolve_graph(None, graph)?;
|
||||||
|
return print_resolved_locator(&locator, json);
|
||||||
|
}
|
||||||
|
if show_origin {
|
||||||
|
return print_config_origins(&loaded.provenance, json);
|
||||||
|
}
|
||||||
|
// Prune null/empty values so the dump shows only what is actually set.
|
||||||
|
let mut value = serde_yaml::to_value(&loaded.config)?;
|
||||||
|
prune_empty(&mut value);
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||||
|
} else {
|
||||||
|
print!("{}", serde_yaml::to_string(&value)?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively drop `null` values and empty maps/sequences from a config dump so
|
||||||
|
/// `config view` shows only fields that are actually set.
|
||||||
|
fn prune_empty(value: &mut serde_yaml::Value) {
|
||||||
|
match value {
|
||||||
|
serde_yaml::Value::Mapping(map) => {
|
||||||
|
for (_, child) in map.iter_mut() {
|
||||||
|
prune_empty(child);
|
||||||
|
}
|
||||||
|
map.retain(|_, child| !is_empty_value(child));
|
||||||
|
}
|
||||||
|
serde_yaml::Value::Sequence(seq) => {
|
||||||
|
for child in seq.iter_mut() {
|
||||||
|
prune_empty(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty_value(value: &serde_yaml::Value) -> bool {
|
||||||
|
match value {
|
||||||
|
serde_yaml::Value::Null => true,
|
||||||
|
serde_yaml::Value::Mapping(map) => map.is_empty(),
|
||||||
|
serde_yaml::Value::Sequence(seq) => seq.is_empty(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print each honored field's origin layer, sorted (deterministic).
|
||||||
|
fn print_config_origins(provenance: &Provenance, json: bool) -> Result<()> {
|
||||||
|
if json {
|
||||||
|
let origins: std::collections::BTreeMap<&str, &str> = provenance
|
||||||
|
.iter()
|
||||||
|
.map(|(field, layer)| (field.as_str(), layer.label()))
|
||||||
|
.collect();
|
||||||
|
print_json(&origins)
|
||||||
|
} else {
|
||||||
|
for (field, layer) in provenance.iter() {
|
||||||
|
println!("{field} ({})", layer.label());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a resolved [`GraphLocator`] (embedded vs remote) in human or JSON form.
|
||||||
|
fn print_resolved_locator(locator: &GraphLocator, json: bool) -> Result<()> {
|
||||||
|
match locator {
|
||||||
|
GraphLocator::Embedded {
|
||||||
|
uri,
|
||||||
|
branch,
|
||||||
|
snapshot,
|
||||||
|
graph_id,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if json {
|
||||||
|
print_json(&serde_json::json!({
|
||||||
|
"kind": "embedded",
|
||||||
|
"uri": uri,
|
||||||
|
"graph_id": graph_id,
|
||||||
|
"branch": branch,
|
||||||
|
"snapshot": snapshot,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
println!("embedded");
|
||||||
|
println!(" uri: {uri}");
|
||||||
|
println!(" graph_id: {graph_id}");
|
||||||
|
if let Some(branch) = branch {
|
||||||
|
println!(" branch: {branch}");
|
||||||
|
}
|
||||||
|
if let Some(snapshot) = snapshot {
|
||||||
|
println!(" snapshot: {snapshot}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphLocator::Remote {
|
||||||
|
endpoint,
|
||||||
|
server,
|
||||||
|
graph_id,
|
||||||
|
branch,
|
||||||
|
snapshot,
|
||||||
|
} => {
|
||||||
|
if json {
|
||||||
|
print_json(&serde_json::json!({
|
||||||
|
"kind": "remote",
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"server": server,
|
||||||
|
"graph_id": graph_id,
|
||||||
|
"branch": branch,
|
||||||
|
"snapshot": snapshot,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
println!("remote");
|
||||||
|
println!(" endpoint: {endpoint}");
|
||||||
|
println!(" server: {server}");
|
||||||
|
println!(" graph_id: {graph_id}");
|
||||||
|
if let Some(branch) = branch {
|
||||||
|
println!(" branch: {branch}");
|
||||||
|
}
|
||||||
|
if let Some(snapshot) = snapshot {
|
||||||
|
println!(" snapshot: {snapshot}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ResolvedCliGraph {
|
struct ResolvedCliGraph {
|
||||||
uri: String,
|
uri: String,
|
||||||
|
|
@ -3016,6 +3183,23 @@ async fn main() -> Result<()> {
|
||||||
print_policy_explain(&decision, &actor, &request);
|
print_policy_explain(&decision, &actor, &request);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Command::Config { command } => match command {
|
||||||
|
ConfigCommand::View {
|
||||||
|
config,
|
||||||
|
resolved,
|
||||||
|
show_origin,
|
||||||
|
json,
|
||||||
|
graph,
|
||||||
|
} => {
|
||||||
|
config_view(
|
||||||
|
config.as_ref(),
|
||||||
|
resolved,
|
||||||
|
show_origin,
|
||||||
|
json,
|
||||||
|
graph.as_deref(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
Command::Optimize {
|
Command::Optimize {
|
||||||
uri,
|
uri,
|
||||||
target,
|
target,
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,99 @@ fn cli_resolves_graph_from_global_config_with_no_project_file() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_view_prints_merged_config_as_yaml() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let config = temp.path().join("omnigraph.yaml");
|
||||||
|
write_config(
|
||||||
|
&config,
|
||||||
|
"version: 1\ngraphs:\n local:\n storage: ./l.omni\n\
|
||||||
|
defaults:\n graph: local\n output_format: kv\n",
|
||||||
|
);
|
||||||
|
let output = output_success(cli().arg("config").arg("view").arg("--config").arg(&config));
|
||||||
|
let stdout = stdout_string(&output);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("defaults:") && stdout.contains("local"),
|
||||||
|
"merged config should serialize defaults + graph: {stdout}"
|
||||||
|
);
|
||||||
|
// The always-empty legacy `project:`/`server:` blocks are suppressed.
|
||||||
|
assert!(
|
||||||
|
!stdout.contains("project:") && !stdout.contains("server:"),
|
||||||
|
"legacy noise blocks must not appear: {stdout}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_view_show_origin_labels_each_layer() {
|
||||||
|
let global_dir = tempdir().unwrap();
|
||||||
|
let global_file = global_dir.path().join("config.yaml");
|
||||||
|
write_config(
|
||||||
|
&global_file,
|
||||||
|
"version: 1\nservers:\n prod:\n endpoint: https://prod\ndefaults:\n output_format: kv\n",
|
||||||
|
);
|
||||||
|
let project_dir = tempdir().unwrap();
|
||||||
|
let project = project_dir.path().join("omnigraph.yaml");
|
||||||
|
write_config(
|
||||||
|
&project,
|
||||||
|
"version: 1\ndefaults:\n graph: local\ngraphs:\n local:\n storage: ./l.omni\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = output_success(
|
||||||
|
cli()
|
||||||
|
.env("OMNIGRAPH_CONFIG", &global_file)
|
||||||
|
.arg("config")
|
||||||
|
.arg("view")
|
||||||
|
.arg("--show-origin")
|
||||||
|
.arg("--config")
|
||||||
|
.arg(&project),
|
||||||
|
);
|
||||||
|
let stdout = stdout_string(&output);
|
||||||
|
assert!(stdout.contains("servers.prod (global)"), "{stdout}");
|
||||||
|
assert!(
|
||||||
|
stdout.contains("defaults.output_format (global)"),
|
||||||
|
"{stdout}"
|
||||||
|
);
|
||||||
|
assert!(stdout.contains("defaults.graph (project)"), "{stdout}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_view_resolved_prints_embedded_and_remote_locators() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let config = temp.path().join("omnigraph.yaml");
|
||||||
|
write_config(
|
||||||
|
&config,
|
||||||
|
"version: 1\nservers:\n prod:\n endpoint: https://prod.example\n\
|
||||||
|
graphs:\n local:\n storage: ./l.omni\n staging:\n server: prod\n graph_id: prod\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let embedded = parse_stdout_json(&output_success(
|
||||||
|
cli()
|
||||||
|
.arg("config")
|
||||||
|
.arg("view")
|
||||||
|
.arg("--resolved")
|
||||||
|
.arg("--json")
|
||||||
|
.arg("--config")
|
||||||
|
.arg(&config)
|
||||||
|
.arg("local"),
|
||||||
|
));
|
||||||
|
assert_eq!(embedded["kind"], "embedded");
|
||||||
|
assert!(embedded["uri"].as_str().unwrap().ends_with("l.omni"));
|
||||||
|
|
||||||
|
let remote = parse_stdout_json(&output_success(
|
||||||
|
cli()
|
||||||
|
.arg("config")
|
||||||
|
.arg("view")
|
||||||
|
.arg("--resolved")
|
||||||
|
.arg("--json")
|
||||||
|
.arg("--config")
|
||||||
|
.arg(&config)
|
||||||
|
.arg("staging"),
|
||||||
|
));
|
||||||
|
assert_eq!(remote["kind"], "remote");
|
||||||
|
assert_eq!(remote["endpoint"], "https://prod.example");
|
||||||
|
assert_eq!(remote["graph_id"], "prod");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_plan_json_reports_supported_additive_change() {
|
fn schema_plan_json_reports_supported_additive_change() {
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,9 @@ pub struct OmnigraphConfig {
|
||||||
/// `load_config_in`, which also rejects unsupported versions.
|
/// `load_config_in`, which also rejects unsupported versions.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub version: Option<u32>,
|
pub version: Option<u32>,
|
||||||
#[serde(default)]
|
// Legacy, no consumer and empty after load — parsed for back-compat but never
|
||||||
|
// emitted by `config view` (`skip_serializing`).
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
pub project: ProjectConfig,
|
pub project: ProjectConfig,
|
||||||
/// Named remote servers (endpoints) referenced by remote graph entries
|
/// Named remote servers (endpoints) referenced by remote graph entries
|
||||||
/// (`graphs.<name>.server`) and `server/graph_id` addressing — RFC-002 §1.
|
/// (`graphs.<name>.server`) and `server/graph_id` addressing — RFC-002 §1.
|
||||||
|
|
@ -406,7 +408,8 @@ pub struct OmnigraphConfig {
|
||||||
pub serve: Serve,
|
pub serve: Serve,
|
||||||
/// Legacy spelling of `serve:` (no `version:`); rejected under v1, folded
|
/// Legacy spelling of `serve:` (no `version:`); rejected under v1, folded
|
||||||
/// into `serve` at load. Do not read directly — use the `serve_*` accessors.
|
/// into `serve` at load. Do not read directly — use the `serve_*` accessors.
|
||||||
#[serde(default)]
|
/// Empty after load, so never emitted by `config view` (`skip_serializing`).
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
pub server: ServerDefaults,
|
pub server: ServerDefaults,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auth: AuthDefaults,
|
pub auth: AuthDefaults,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue