Skip to content
Merged
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
67 changes: 66 additions & 1 deletion src/cmd/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ impl ShowPort {
struct ShowNode {
path: String,
kind: String,
/// Codegen language carried by a node — set via `--language`, in practice on a
/// boundary. Surfaced for whichever node the server reports it on; omitted when
/// unset so a languageless node never emits a bogus or null value.
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
inputs: Vec<ShowPort>,
#[serde(skip_serializing_if = "Vec::is_empty")]
Expand Down Expand Up @@ -211,6 +216,9 @@ fn build_view(graph: &GraphResponse, filter: Option<&str>) -> Result<View, CliEr
.map(|n| ShowNode {
path: paths[&n.id].clone(),
kind: kind_str(n.kind).to_string(),
// `language` is a double option on the wire (present/null/absent);
// flatten so only a real value surfaces.
language: n.data.language.clone().flatten(),
inputs: show_ports(n.data.inputs.as_deref()),
outputs: show_ports(n.data.outputs.as_deref()),
config: show_ports(n.data.config.as_deref()),
Expand Down Expand Up @@ -300,7 +308,12 @@ fn human(view: &View, project_name: &str, branch_name: &str) -> String {
.rsplit('.')
.next()
.expect("a node path always has at least one segment");
out.push_str(&format!("\n{indent}{leaf} [{}]", node.kind));
let language = node
.language
.as_deref()
.map(|l| format!(" ({l})"))
.unwrap_or_default();
out.push_str(&format!("\n{indent}{leaf} [{}]{language}", node.kind));
let ports = " ".repeat(depth + 2);
if !node.inputs.is_empty() {
out.push_str(&format!("\n{ports}in: {}", join_ports(&node.inputs)));
Expand Down Expand Up @@ -550,6 +563,58 @@ mod tests {
assert_eq!(edges[0]["to"], "Api.Rater.raw");
}

#[test]
fn boundary_language_is_shown_in_both_modes() {
// A boundary with a codegen language surfaces it in show — otherwise the
// web UI is the only place to confirm what `--language` set. It rides on
// the boundary node line (human) and as a `language` field (JSON).
let mut g = sample_graph();
// Api is the boundary node (index 0 in sample_graph).
g.nodes[0].data.language = Some(Some("python".to_string()));

let human = render(&g, "proj", "main", None, OutputMode::Human).unwrap();
assert!(
human.contains("Api [boundary] (python)"),
"human view must annotate the boundary's language: {human}"
);

let json = render(&g, "proj", "main", None, OutputMode::Json).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let api = v["nodes"]
.as_array()
.unwrap()
.iter()
.find(|n| n["path"] == "Api")
.unwrap();
assert_eq!(api["language"], "python", "{json}");
}

#[test]
fn node_without_language_emits_no_language_value() {
// A node with no language must not emit a bogus or "null"-string value in
// either mode. The sample graph carries no language on any node.
let g = sample_graph();

let json = render(&g, "proj", "main", None, OutputMode::Json).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
for n in v["nodes"].as_array().unwrap() {
assert!(
n.get("language").is_none(),
"a languageless node must omit the field: {n}"
);
}
assert!(!json.contains("language"), "{json}");

let human = render(&g, "proj", "main", None, OutputMode::Human).unwrap();
// The language annotation is the only `] (` sequence show emits (it rides
// right after a node's `[kind]`); assert that exact signature is absent
// rather than any stray `(`, so unrelated future output can't false-trip.
assert!(
!human.contains("] ("),
"no language annotation expected: {human}"
);
}

#[test]
fn position_field_is_omitted() {
// The graph endpoint's placeholder position must never surface in show.
Expand Down