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
10 changes: 5 additions & 5 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ eyre = "0.6.8"
# needs real I/O (git fetch, filesystem), so it's off by default for
# `comline-core`'s wasm32 consumers (the playground editor, the language
# server); the CLI is the one consumer that actually runs `comline build`.
comline-core = { git = "https://github.com/ComlineProject/core", rev = "2ae998634bad418e4a03ce7494e673275dafcc01", features = ["deps"] }
comline-core = { git = "https://github.com/ComlineProject/core", rev = "47066583002dbca27975827106f9e34761d0dc64", features = ["deps"] }
# All three pinned to revs that agree on one `comline-codegen`, and bumped
# together: comline-rust and comline-typescript's codegen crates consume
# `comline-codegen`'s types, so the rev below must be byte-identical to the
# one *they* pin - Cargo only unifies a git dependency across identical rev
# strings, and two copies of `GenRequest` are two incompatible types.
comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6" }
comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "5346678b82fc275fca741bf5ca0cfad1e8edf075", optional = true }
comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "c2096cd500f7cd3a83d495fbbbf5df72bc12ecc7", optional = true }
comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "0601f004302ebb93e6804732419ccf459902e84a" }
comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "6e8715ac126ab78316dd6a2712165d716351fab1", optional = true }
comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "470dc5bad2f7e36e1cc2f44e3e57fde864a42315", optional = true }
# `comline conformance`: the conformance runner (corpus loading, judging,
# profile proofs) and its case format, a public spec. Pinned by git rev.
comline-runtime-conformance = { git = "https://github.com/ComlineProject/runtime", rev = "6f8f73a247f0aa4ac8b7041b2fdc4b02bce0e0d6" }
Expand Down
57 changes: 57 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This is the long-form reference. For a two-minute tour see the
- [`new`](#comline-new)
- [`add`](#comline-add)
- [`check`](#comline-check)
- [`inspect`](#comline-inspect)
- [`build`](#comline-build)
- [`generate`](#comline-generate)
- [`diff`](#comline-diff)
Expand Down Expand Up @@ -195,6 +196,62 @@ comline --path ./services/users check
Exit `0` if valid, `1` if a schema or the config is invalid, `2` if the directory
is not a Comline project.

### `comline inspect`

```
comline inspect [--json]
```

What each protocol speaks on the wire, and which guarantees it keeps. Like
`check`, it compiles the package and writes nothing to `.comline/`.

For each protocol, a short header names the format, framing and handshake it
resolves to, with their parameters and where each came from:

| Source | Meaning |
|---|---|
| `from @framing` / `from @handshake` | the protocol's own annotation |
| `from config.idp ...` | the package's `wire.format`, `wire.framing` or `handshake` |
| `core's default` | nothing named one: `packed@1`, `datagram@2`, `comline@1` |
| `JSON-RPC's format` | the framing is JSON-RPC and the package names no format |

Then one row per guarantee (schema agreement, encoding agreement, encoding
conformance, version honesty), held or given up. A given-up row says why, and
whether it has been accepted. A dependency's protocol is accepted by this
package's `accept_unverified`. Who accepts this package's own protocols isn't
known here: each consumer does that in its own `config.idp`. A protocol's
`@unverified` reason is in its header.

```
$ comline --plain inspect
protocol api::Legacy (shop)
format json@1 JSON-RPC's format
framing jsonrpc (method_names = qualified) from @framing
handshake none from @handshake
unverified "the 2009 gateway can't run a handshake"

schema agreement given up
because handshake `none`
acceptance: up to each consumer's config.idp
encoding agreement given up
because handshake `none`
acceptance: up to each consumer's config.idp
encoding conformance held
version honesty held
```

The report goes to stdout. `--json` prints the same as JSON, as
`{ "protocols": [...] }`, with each choice's `source` given as `annotation`,
`package_default`, `core_default` or `framing`. Each guarantee's `acceptance`
is `{ "state": "not_needed" | "unknown" | "not_accepted" }`, or
`{ "state": "accepted", "by": "<package>" }`.

A protocol's compatibility facts (its line, version and shape hash) aren't
shown, because they aren't built yet (core#119).

Exit `0` on success, `1` if a schema or the config is invalid, `2` if the
directory is not a Comline project.

### `comline build`

```
Expand Down
23 changes: 23 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,29 @@ first error found. Unlike `build`, this never writes to `.comline/` and never \
bumps the version, so it is safe to run in editors and pre-commit hooks.")]
Check,

/// Show what each protocol speaks on the wire and which guarantees it keeps
#[command(long_about = "\
For each protocol, the format, framing and handshake it resolves to, their \
parameters, and where each came from: the protocol's own `@framing` or \
`@handshake`, the package's `wire` or `handshake` in `config.idp`, or core's \
default. Then every guarantee (schema agreement, encoding agreement, encoding \
conformance, version honesty), held or given up, with the cause, the \
`@unverified` reason, and whether it has been accepted. A dependency's \
protocol is accepted by this package's `accept_unverified`; who accepts this \
package's own protocols isn't known here.

Like `check`, this never writes to `.comline/`. `--json` prints the same as \
JSON.

Examples:
comline inspect
comline inspect --json")]
Inspect {
/// Print the report as JSON
#[arg(long)]
json: bool,
},

/// Validate the project and write generated code for each configured target
#[command(long_about = "\
Validate the project (no version bump, no `.comline/` write), then write \
Expand Down
152 changes: 152 additions & 0 deletions src/commands/inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! `comline inspect` — what each protocol resolves to on the wire, where
//! each choice came from, and which guarantees it keeps. Read-only, like
//! `check`: nothing is written to `.comline/`.
//!
//! The model is core's (`comline_core::inspect`), shared with the language
//! server's hover. The report goes to stdout; `--json` prints the same as
//! JSON.

use std::io::Write as _;
use std::path::Path;

use anstyle::{AnsiColor, Color, Style};
use comline_core::inspect::{self, Acceptance, Choice, ProtocolSummary, Source};
use miette::{IntoDiagnostic, Result};

use crate::commands::{core_err, ensure_project};
use crate::ui;

const HELD: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
const GIVEN_UP: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
const DIM: Style = Style::new().dimmed();
const BOLD: Style = Style::new().bold();

pub fn run(work_dir: &Path, json: bool) -> Result<()> {
ensure_project(work_dir)?;

let spinner = ui::spinner("compiling schemas");
let outcome = comline_core::package::build::compile_package(work_dir);
ui::finish_spinner(spinner);
let context = outcome.map_err(|e| core_err("validation failed", e))?;

let protocols = inspect::protocols(&context);
let mut out = anstream::stdout().lock();
if json {
let report = serde_json::json!({ "protocols": protocols });
writeln!(
out,
"{}",
serde_json::to_string_pretty(&report).into_diagnostic()?
)
.into_diagnostic()?;
return Ok(());
}

if protocols.is_empty() {
ui::note("No protocols to inspect");
return Ok(());
}
let text = protocols
.iter()
.map(|p| render(p, ui::plain()))
.collect::<Vec<_>>()
.join("\n");
write!(out, "{text}").into_diagnostic()?;
Ok(())
}

/// One protocol: a header of choices, then a row per guarantee. Fits 80
/// columns: anything long goes on its own indented line.
pub fn render(p: &ProtocolSummary, plain: bool) -> String {
let style = |s: Style, text: &str| {
if plain {
text.to_string()
} else {
format!("{s}{text}{s:#}")
}
};
let mut out = String::new();

let whose = if p.dependency {
format!("dependency {}", p.package)
} else {
p.package.clone()
};
out += &format!(
"{} {}\n",
style(BOLD, &format!("protocol {}::{}", p.schema, p.name)),
style(DIM, &format!("({whose})"))
);
for (key, choice) in [
("format", &p.format),
("framing", &p.framing),
("handshake", &p.handshake),
] {
out += &format!(
" {key:<10} {:<24} {}\n",
choice_value(choice),
style(DIM, &source(choice.source, key))
);
}
let reason = p.guarantees.iter().find_map(|g| g.reason.as_deref());
if p.unverified {
match reason {
Some(reason) => out += &format!(" {:<10} \"{reason}\"\n", "unverified"),
None => out += &format!(" {:<10} (no reason given)\n", "unverified"),
}
}

out += "\n";
for row in &p.guarantees {
let (mark, state) = match (row.held, plain) {
(true, true) => ("", style(HELD, "held")),
(true, false) => ("✓ ", style(HELD, "held")),
(false, true) => ("", style(GIVEN_UP, "given up")),
(false, false) => ("✗ ", style(GIVEN_UP, "given up")),
};
out += &format!(" {mark}{:<22} {state}\n", row.guarantee.describe());
for cause in &row.causes {
out += &format!(" because {cause}\n");
}
match &row.acceptance {
Acceptance::NotNeeded => {}
Acceptance::Unknown => {
out += &format!(
" {}\n",
style(DIM, "acceptance: up to each consumer's config.idp")
);
}
Acceptance::Accepted(by) => out += &format!(" accepted by {by}\n"),
Acceptance::NotAccepted => {
out += &format!(" {}\n", style(GIVEN_UP, "not accepted"))
}
}
}
out
}

/// `jsonrpc (method_names = qualified)`.
fn choice_value(choice: &Choice) -> String {
if choice.params.is_empty() {
return choice.value.clone();
}
let params: Vec<String> = choice
.params
.iter()
.map(|(k, v)| format!("{k} = {v}"))
.collect();
format!("{} ({})", choice.value, params.join(", "))
}

/// Where a choice came from, for the header.
fn source(source: Source, key: &str) -> String {
match source {
Source::Annotation => format!("from @{key}"),
Source::PackageDefault => match key {
"handshake" => "from config.idp `handshake`".to_string(),
_ => format!("from config.idp `wire.{key}`"),
},
Source::CoreDefault => "core's default".to_string(),
Source::Framing => "JSON-RPC's format".to_string(),
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod completions;
pub mod conformance;
pub mod diff;
pub mod generate;
pub mod inspect;
pub mod new;
pub mod release;
pub mod reset;
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ fn run(cli: Cli) -> Result<()> {
match cli.command {
Commands::Build { watch } => commands::build::run(&work_dir, watch),
Commands::Check => commands::check::run(&work_dir),
Commands::Inspect { json } => commands::inspect::run(&work_dir, json),
Commands::Generate {
target,
out,
Expand Down
Loading
Loading