use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use toml::Value;
use rototo::diagnostics::{Diagnostic, DiagnosticCatalogEntry, DiagnosticSource, Severity};
use rototo::error::{Result, RototoError};
use rototo::model::{
DiagnosticCatalog, QualifierInspection, QualifierLint, VariableInspection, VariableLint,
VariableResolution, WorkspaceInspection, WorkspaceLint,
};
use rototo::workspace::{qualifier_for_id, read_toml, read_variable_toml, variable_for_id};
use crate::terminal::{Role, stdout};
#[derive(Debug, Serialize)]
struct WorkspaceInspectionJson<'a> {
workspace: String,
environments: &'a [String],
qualifiers: Vec<WorkspaceFileJson<'a>>,
variables: Vec<WorkspaceFileJson<'a>>,
}
#[derive(Debug, Serialize)]
struct WorkspaceFileJson<'a> {
id: &'a str,
uri: &'a str,
path: String,
}
#[derive(Debug, Serialize)]
struct WorkspaceLintJson<'a> {
workspace: String,
diagnostics: &'a [Diagnostic],
}
#[derive(Debug, Serialize)]
struct QualifierLintJson<'a> {
workspace: String,
id: &'a str,
diagnostics: &'a [Diagnostic],
}
#[derive(Debug, Serialize)]
struct VariableLintJson<'a> {
workspace: String,
id: &'a str,
diagnostics: &'a [Diagnostic],
}
#[derive(Debug, Serialize)]
struct WorkspaceEntityListJson<'a> {
workspace: String,
variables: Vec<WorkspaceFileJson<'a>>,
qualifiers: Vec<WorkspaceFileJson<'a>>,
}
#[derive(Debug, Serialize)]
struct WorkspaceEntityGetJson {
id: String,
uri: String,
path: String,
value: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct WorkspaceEntityGetsJson {
workspace: String,
variables: Vec<WorkspaceEntityGetJson>,
qualifiers: Vec<WorkspaceEntityGetJson>,
}
#[derive(Debug, Serialize)]
struct ResolutionsJson<'a, T: Serialize> {
workspace: String,
values: &'a [T],
}
#[derive(Debug, Serialize)]
struct DiagnosticCatalogJson<'a> {
scope: &'a rototo::model::DiagnosticCatalogScope,
subject: &'a str,
diagnostics: &'a [DiagnosticCatalogEntry],
}
pub(crate) async fn print_inspection(inspection: &WorkspaceInspection, json: bool) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&inspection_json(inspection))
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
let relationships = inspect_relationships(inspection).await?;
println!(
"{}: {}",
stdout(Role::Fg, "workspace"),
stdout(Role::Dim, inspection.root.display().to_string())
);
println!(
"{}: {}",
stdout(Role::Fg, "environments"),
inspection
.environments
.iter()
.map(environment_text)
.collect::<Vec<_>>()
.join(", ")
);
match relationships.context_schema.as_deref() {
Some(schema) => println!("{}: {}", stdout(Role::Fg, "context"), context_text(schema)),
None => println!("{}: none", stdout(Role::Fg, "context")),
}
println!();
print_inspection_legend();
println!();
println!("{}:", stdout(Role::Fg, "variables"));
print_variable_relationships(&relationships.variables);
println!();
println!("{}:", stdout(Role::Fg, "qualifier relationships"));
print_qualifier_relationships(&relationships.qualifiers);
println!();
println!("{}:", stdout(Role::Fg, "qualifier usage"));
print_qualifier_usage(&relationships.qualifier_usage);
println!();
println!("{}:", stdout(Role::Fg, "next"));
println!(
" {}",
stdout(
Role::Dim,
format!("rototo lint {}", inspection.root.display())
)
);
println!(
" {}",
stdout(
Role::Dim,
format!(
"rototo resolve {} -v <id> --env <env> -c '{{}}'",
inspection.root.display(),
)
)
);
println!(
" {}",
stdout(
Role::Dim,
format!(
"rototo resolve {} -q <id> -c '{{}}'",
inspection.root.display(),
)
)
);
Ok(())
}
pub(crate) async fn print_entity_inspections(
inspection: &WorkspaceInspection,
variable_ids: &[String],
qualifier_ids: &[String],
json: bool,
) -> Result<()> {
if json {
return print_entity_gets(inspection, variable_ids, qualifier_ids, true).await;
}
let total = variable_ids.len() + qualifier_ids.len();
let mut first = true;
for id in variable_ids {
print_variable_detail(inspection, id, total, &mut first).await?;
}
let relationships = if qualifier_ids.is_empty() {
None
} else {
Some(inspect_relationships(inspection).await?)
};
for id in qualifier_ids {
print_qualifier_detail(
inspection,
id,
total,
&mut first,
relationships
.as_ref()
.expect("qualifier relationships are loaded"),
)
.await?;
}
Ok(())
}
async fn print_variable_detail(
inspection: &WorkspaceInspection,
id: &str,
total: usize,
first: &mut bool,
) -> Result<()> {
let variable = variable_for_id(inspection, id)?;
let toml = read_variable_toml(&inspection.root, variable).await?;
let variable_table = toml.get("variable").and_then(Value::as_table);
print_detail_header(first, total, "variable", &variable.id);
println!(
" {}: {}",
stdout(Role::Fg, "source"),
stdout(Role::Dim, variable.path.display().to_string())
);
if let Some(description) = variable_table
.and_then(|table| table.get("description"))
.and_then(Value::as_str)
{
println!(" {}: {}", stdout(Role::Fg, "description"), description);
}
if let Some(contract) = variable_contract(variable_table) {
println!(
" {}: {}",
stdout(Role::Fg, "contract"),
stdout(Role::Dim, contract)
);
}
if let Some(custom_lint) = variable_table
.and_then(|table| table.get("lint"))
.and_then(|lint| lint.get("path"))
.and_then(Value::as_str)
{
println!(
" {}: {}",
stdout(Role::Fg, "custom lint"),
stdout(Role::Dim, custom_lint)
);
}
println!(" {}:", stdout(Role::Fg, "values"));
match variable_table
.and_then(|table| table.get("values"))
.and_then(Value::as_table)
{
Some(values) if !values.is_empty() => {
for (key, value) in values {
print_detail_value(" ", key, value)?;
}
}
_ => println!(" {}", stdout(Role::Dim, "none loaded")),
}
println!(" {}:", stdout(Role::Fg, "environments"));
match variable_table
.and_then(|table| table.get("env"))
.and_then(Value::as_table)
{
Some(environments) if !environments.is_empty() => {
for (environment, value) in environments {
print_variable_environment_detail(environment, value);
}
}
_ => println!(" {}", stdout(Role::Dim, "none declared")),
}
Ok(())
}
async fn print_qualifier_detail(
inspection: &WorkspaceInspection,
id: &str,
total: usize,
first: &mut bool,
relationships: &WorkspaceRelationships,
) -> Result<()> {
let qualifier = qualifier_for_id(inspection, id)?;
let toml = read_toml(&inspection.root.join(&qualifier.path)).await?;
let qualifier_table = toml.get("qualifier").and_then(Value::as_table);
print_detail_header(first, total, "qualifier", &qualifier.id);
println!(
" {}: {}",
stdout(Role::Fg, "source"),
stdout(Role::Dim, qualifier.path.display().to_string())
);
if let Some(description) = qualifier_table
.and_then(|table| table.get("description"))
.and_then(Value::as_str)
{
println!(" {}: {}", stdout(Role::Fg, "description"), description);
}
println!(" {}:", stdout(Role::Fg, "predicates"));
match qualifier_table
.and_then(|table| table.get("predicate"))
.and_then(Value::as_array)
{
Some(predicates) if !predicates.is_empty() => {
for predicate in predicates {
print_predicate_detail(predicate)?;
}
}
_ => println!(" {}", stdout(Role::Dim, "none declared")),
}
println!(" {}:", stdout(Role::Fg, "used by"));
print_qualifier_detail_usage(&qualifier.id, relationships);
Ok(())
}
fn print_detail_header(first: &mut bool, _total: usize, kind: &str, id: &str) {
if !*first {
println!();
}
*first = false;
let styled_id = match kind {
"variable" => variable_text(id),
"qualifier" => qualifier_text(id),
_ => stdout(Role::Fg, id),
};
println!("{} {}", stdout(Role::Fg, kind), styled_id);
}
fn variable_contract(variable_table: Option<&toml::map::Map<String, Value>>) -> Option<String> {
variable_table
.and_then(|table| table.get("schema"))
.and_then(Value::as_str)
.map(|schema| format!("schema {schema}"))
.or_else(|| {
variable_table
.and_then(|table| table.get("type"))
.and_then(Value::as_str)
.map(|value_type| format!("type {value_type}"))
})
}
fn print_detail_value(prefix: &str, key: &str, value: &Value) -> Result<()> {
let json = serde_json::to_value(value).map_err(|err| RototoError::new(err.to_string()))?;
match json {
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
println!("{prefix}{}:", value_text(key));
print_pretty_json_lines(&json, &format!("{prefix} "))?;
}
_ => println!(
"{prefix}{} = {}",
value_text(key),
stdout(
Role::Dim,
serde_json::to_string(&json).map_err(|err| RototoError::new(err.to_string()))?
)
),
}
Ok(())
}
fn print_pretty_json_lines(value: &serde_json::Value, prefix: &str) -> Result<()> {
let text =
serde_json::to_string_pretty(value).map_err(|err| RototoError::new(err.to_string()))?;
for line in text.lines() {
println!("{}", stdout(Role::Dim, format!("{prefix}{line}")));
}
Ok(())
}
fn print_variable_environment_detail(environment: &str, value: &Value) {
println!(" {}", environment_text(environment));
if let Some(default) = env_default_value(value) {
println!(
" {}: {}",
stdout(Role::Fg, "default"),
value_text(default)
);
}
if let Some(rules) = value.get("rule").and_then(Value::as_array)
&& !rules.is_empty()
{
println!(" {}:", stdout(Role::Fg, "rules"));
for rule in rules {
print_variable_rule_detail(rule);
}
}
}
fn print_variable_rule_detail(rule: &Value) {
let qualifier = rule
.get("qualifier")
.and_then(Value::as_str)
.unwrap_or("<missing qualifier>");
let value = rule
.get("value")
.and_then(Value::as_str)
.unwrap_or("<missing value>");
println!(
" {} -> {}",
qualifier_text(qualifier),
value_text(value)
);
if let Some(description) = rule.get("description").and_then(Value::as_str) {
println!(
" {}: {}",
stdout(Role::Fg, "description"),
description
);
}
}
fn print_qualifier_detail_usage(id: &str, relationships: &WorkspaceRelationships) {
let Some(usage) = relationships.qualifier_usage.get(id) else {
println!(" {}", stdout(Role::Dim, "none"));
return;
};
if usage.variables.is_empty() && usage.qualifiers.is_empty() {
println!(" {}", stdout(Role::Dim, "none"));
return;
}
if !usage.variables.is_empty() {
println!(
" variables: {}",
usage
.variables
.iter()
.map(variable_text)
.collect::<Vec<_>>()
.join(", ")
);
}
if !usage.qualifiers.is_empty() {
println!(
" qualifiers: {}",
usage
.qualifiers
.iter()
.map(qualifier_text)
.collect::<Vec<_>>()
.join(", ")
);
}
}
fn print_predicate_detail(predicate: &Value) -> Result<()> {
let attribute = predicate
.get("attribute")
.and_then(Value::as_str)
.unwrap_or("<missing attribute>");
let op = predicate
.get("op")
.and_then(Value::as_str)
.unwrap_or("<missing op>");
let mut parts = vec![format_predicate_attribute(attribute), stdout(Role::Fg, op)];
if op == "bucket" {
if let Some(salt) = predicate.get("salt").and_then(Value::as_str) {
parts.push(format!("salt {}", stdout(Role::Dim, salt)));
}
if let Some(range) = predicate.get("range") {
parts.push(format!("range {}", inline_json(range)?));
}
} else if let Some(value) = predicate.get("value") {
parts.push(inline_json(value)?);
}
println!(" {}", parts.join(" "));
if let Some(description) = predicate.get("description").and_then(Value::as_str) {
println!(" {}: {}", stdout(Role::Fg, "description"), description);
}
Ok(())
}
fn format_predicate_attribute(attribute: &str) -> String {
if let Some(qualifier) = attribute.strip_prefix("qualifier.") {
format!("qualifier.{}", qualifier_text(qualifier))
} else {
context_text(attribute)
}
}
fn inline_json(value: &Value) -> Result<String> {
let json = serde_json::to_value(value).map_err(|err| RototoError::new(err.to_string()))?;
serde_json::to_string(&json).map_err(|err| RototoError::new(err.to_string()))
}
async fn inspect_relationships(inspection: &WorkspaceInspection) -> Result<WorkspaceRelationships> {
let manifest = read_toml(&inspection.root.join("rototo-workspace.toml")).await?;
let context_schema = manifest
.get("context")
.and_then(|context| context.get("schema"))
.and_then(Value::as_str)
.map(str::to_owned);
let mut variables = Vec::new();
let mut qualifier_usage = inspection
.qualifiers
.iter()
.map(|qualifier| (qualifier.id.clone(), QualifierUsage::default()))
.collect::<BTreeMap<_, _>>();
for variable in &inspection.variables {
let toml = read_variable_toml(&inspection.root, variable).await?;
let relationship = variable_relationship(variable, &toml, &inspection.environments);
for env in &relationship.environments {
for rule in &env.rules {
qualifier_usage
.entry(rule.qualifier.clone())
.or_default()
.variables
.insert(variable.id.clone());
}
}
variables.push(relationship);
}
let mut qualifiers = Vec::new();
for qualifier in &inspection.qualifiers {
let toml = read_toml(&inspection.root.join(&qualifier.path)).await?;
let relationship = qualifier_relationship(qualifier, &toml);
for referenced in &relationship.qualifiers {
qualifier_usage
.entry(referenced.clone())
.or_default()
.qualifiers
.insert(qualifier.id.clone());
}
qualifiers.push(relationship);
}
Ok(WorkspaceRelationships {
context_schema,
variables,
qualifiers,
qualifier_usage,
})
}
fn variable_relationship(
variable: &VariableInspection,
toml: &Value,
workspace_environments: &[String],
) -> VariableRelationship {
let variable_table = toml.get("variable");
let contract = variable_table
.and_then(|table| table.get("schema"))
.and_then(Value::as_str)
.map(|schema| format!("schema: {schema}"))
.or_else(|| {
variable_table
.and_then(|table| table.get("type"))
.and_then(Value::as_str)
.map(|value_type| format!("type: {value_type}"))
})
.unwrap_or_else(|| "contract: missing".to_owned());
let custom_lint = variable_table
.and_then(|table| table.get("lint"))
.and_then(|lint| lint.get("path"))
.and_then(Value::as_str)
.map(str::to_owned);
let env_table = variable_table
.and_then(|table| table.get("env"))
.and_then(Value::as_table);
let fallback = env_table
.and_then(|env| env.get("_"))
.and_then(env_default_value);
let mut environments = Vec::new();
environments.push(env_relationship(
"_",
env_table.and_then(|env| env.get("_")),
None,
));
for environment in workspace_environments {
let env_value = env_table.and_then(|env| env.get(environment));
environments.push(env_relationship(
environment,
env_value,
fallback.as_deref(),
));
}
VariableRelationship {
id: variable.id.clone(),
contract,
custom_lint,
environments,
}
}
fn env_relationship(
environment: &str,
env_value: Option<&Value>,
fallback: Option<&str>,
) -> VariableEnvironmentRelationship {
let default = env_value
.and_then(env_default_value)
.map(DefaultSource::Explicit)
.or_else(|| fallback.map(|value| DefaultSource::Fallback(value.to_owned())));
let rules = env_value
.and_then(|env| env.get("rule"))
.and_then(Value::as_array)
.map(|rules| {
rules
.iter()
.filter_map(variable_rule_relationship)
.collect::<Vec<_>>()
})
.unwrap_or_default();
VariableEnvironmentRelationship {
environment: environment.to_owned(),
default,
rules,
}
}
fn env_default_value(env_value: &Value) -> Option<String> {
env_value
.get("value")
.and_then(Value::as_str)
.map(str::to_owned)
}
fn variable_rule_relationship(rule: &Value) -> Option<VariableRuleRelationship> {
Some(VariableRuleRelationship {
qualifier: rule.get("qualifier")?.as_str()?.to_owned(),
value: rule.get("value")?.as_str()?.to_owned(),
})
}
fn qualifier_relationship(qualifier: &QualifierInspection, toml: &Value) -> QualifierRelationship {
let mut contexts = BTreeSet::new();
let mut qualifiers = BTreeSet::new();
if let Some(predicates) = toml
.get("qualifier")
.and_then(|qualifier| qualifier.get("predicate"))
.and_then(Value::as_array)
{
for predicate in predicates {
let Some(attribute) = predicate.get("attribute").and_then(Value::as_str) else {
continue;
};
if let Some(id) = attribute.strip_prefix("qualifier.") {
qualifiers.insert(id.to_owned());
} else {
contexts.insert(attribute.to_owned());
}
}
}
QualifierRelationship {
id: qualifier.id.clone(),
contexts: contexts.into_iter().collect(),
qualifiers: qualifiers.into_iter().collect(),
}
}
fn print_variable_relationships(variables: &[VariableRelationship]) {
for variable in variables {
println!(
" {} [{}]",
variable_text(&variable.id),
stdout(Role::Dim, variable.metadata().join(", "))
);
for env in &variable.environments {
println!(" {}", variable_env_line(env));
}
}
}
fn variable_env_line(env: &VariableEnvironmentRelationship) -> String {
let mut parts = env
.rules
.iter()
.map(|rule| {
format!(
"{} => {}",
qualifier_text(&rule.qualifier),
value_text(&rule.value)
)
})
.collect::<Vec<_>>();
match &env.default {
Some(DefaultSource::Explicit(value)) => {
parts.push(format!("default => {}", value_text(value)));
}
Some(DefaultSource::Fallback(value)) => {
parts.push(format!("fallback _ => {}", value_text(value)));
}
None => {
parts.push(stdout(Role::Dim, "no default"));
}
}
format!(
"{} -> {}",
environment_text(&env.environment),
parts.join("; ")
)
}
fn print_qualifier_relationships(qualifiers: &[QualifierRelationship]) {
for qualifier in qualifiers {
println!(" {}", qualifier_text(&qualifier.id));
if qualifier.contexts.is_empty() && qualifier.qualifiers.is_empty() {
println!(" {}", stdout(Role::Dim, "uses no declared predicates"));
continue;
}
if !qualifier.contexts.is_empty() {
println!(
" uses context: {}",
qualifier
.contexts
.iter()
.map(context_text)
.collect::<Vec<_>>()
.join(", ")
);
}
if !qualifier.qualifiers.is_empty() {
println!(
" uses qualifiers: {}",
qualifier
.qualifiers
.iter()
.map(qualifier_text)
.collect::<Vec<_>>()
.join(", ")
);
}
}
}
fn print_qualifier_usage(usage: &BTreeMap<String, QualifierUsage>) {
let mut unused = Vec::new();
for (qualifier, usage) in usage {
if usage.variables.is_empty() && usage.qualifiers.is_empty() {
unused.push(qualifier.clone());
continue;
}
let mut parts = Vec::new();
if !usage.variables.is_empty() {
parts.push(format!(
"variables: {}",
usage
.variables
.iter()
.map(variable_text)
.collect::<Vec<_>>()
.join(", ")
));
}
if !usage.qualifiers.is_empty() {
parts.push(format!(
"qualifiers: {}",
usage
.qualifiers
.iter()
.map(qualifier_text)
.collect::<Vec<_>>()
.join(", ")
));
}
println!(" {} -> {}", qualifier_text(qualifier), parts.join("; "));
}
if !unused.is_empty() {
println!(
" {} -> {}",
stdout(Role::Dim, "unused"),
unused
.iter()
.map(qualifier_text)
.collect::<Vec<_>>()
.join(", ")
);
}
}
fn print_inspection_legend() {
println!("{}:", stdout(Role::Fg, "legend"));
println!(
" {} {} {} {} {}",
environment_text("environment"),
variable_text("variable"),
qualifier_text("qualifier"),
value_text("value"),
context_text("context")
);
}
fn environment_text(text: impl AsRef<str>) -> String {
stdout(Role::Ok, text)
}
fn variable_text(text: impl AsRef<str>) -> String {
stdout(Role::Info, text)
}
fn qualifier_text(text: impl AsRef<str>) -> String {
stdout(Role::Clay, text)
}
fn value_text(text: impl AsRef<str>) -> String {
stdout(Role::Fg, text)
}
fn context_text(text: impl AsRef<str>) -> String {
stdout(Role::Dim, text)
}
struct WorkspaceRelationships {
context_schema: Option<String>,
variables: Vec<VariableRelationship>,
qualifiers: Vec<QualifierRelationship>,
qualifier_usage: BTreeMap<String, QualifierUsage>,
}
struct VariableRelationship {
id: String,
contract: String,
custom_lint: Option<String>,
environments: Vec<VariableEnvironmentRelationship>,
}
impl VariableRelationship {
fn metadata(&self) -> Vec<String> {
let mut metadata = vec![self.contract.clone()];
if let Some(custom_lint) = &self.custom_lint {
metadata.push(format!("custom lint: {custom_lint}"));
}
metadata
}
}
struct VariableEnvironmentRelationship {
environment: String,
default: Option<DefaultSource>,
rules: Vec<VariableRuleRelationship>,
}
struct VariableRuleRelationship {
qualifier: String,
value: String,
}
enum DefaultSource {
Explicit(String),
Fallback(String),
}
struct QualifierRelationship {
id: String,
contexts: Vec<String>,
qualifiers: Vec<String>,
}
#[derive(Default)]
struct QualifierUsage {
variables: BTreeSet<String>,
qualifiers: BTreeSet<String>,
}
pub(crate) fn print_workspace_lint(lint: &WorkspaceLint, json: bool, quiet: bool) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&WorkspaceLintJson {
workspace: lint.root.display().to_string(),
diagnostics: &lint.diagnostics,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
if lint.diagnostics.is_empty() {
if quiet {
return Ok(());
}
println!(
"{}: {}",
stdout(Role::Ok, "ok"),
stdout(Role::Dim, lint.root.display().to_string())
);
return Ok(());
}
print_diagnostics(&lint.diagnostics);
Ok(())
}
pub(crate) fn print_qualifier_lint(lint: &QualifierLint, json: bool, quiet: bool) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&QualifierLintJson {
workspace: lint.root.display().to_string(),
id: &lint.id,
diagnostics: &lint.diagnostics,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
if lint.diagnostics.is_empty() {
if quiet {
return Ok(());
}
println!(
"{}: {}",
stdout(Role::Ok, "ok"),
stdout(Role::Clay, format!("qualifier://{}", lint.id))
);
return Ok(());
}
print_diagnostics(&lint.diagnostics);
Ok(())
}
pub(crate) fn print_variable_lint(lint: &VariableLint, json: bool, quiet: bool) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&VariableLintJson {
workspace: lint.root.display().to_string(),
id: &lint.id,
diagnostics: &lint.diagnostics,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
if lint.diagnostics.is_empty() {
if quiet {
return Ok(());
}
println!(
"{}: {}",
stdout(Role::Ok, "ok"),
stdout(Role::Clay, format!("variable://{}", lint.id))
);
return Ok(());
}
print_diagnostics(&lint.diagnostics);
Ok(())
}
pub(crate) fn print_workspace_entity_list(
inspection: &WorkspaceInspection,
show_variables: bool,
show_qualifiers: bool,
json: bool,
) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&WorkspaceEntityListJson {
workspace: inspection.root.display().to_string(),
variables: if show_variables {
inspection.variables.iter().map(variable_json).collect()
} else {
Vec::new()
},
qualifiers: if show_qualifiers {
inspection.qualifiers.iter().map(qualifier_json).collect()
} else {
Vec::new()
},
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
match (show_variables, show_qualifiers) {
(true, true) => {
println!("{}:", stdout(Role::Fg, "variables"));
for variable in &inspection.variables {
println!(" {}", variable_text(&variable.id));
}
println!();
println!("{}:", stdout(Role::Fg, "qualifiers"));
for qualifier in &inspection.qualifiers {
println!(" {}", qualifier_text(&qualifier.id));
}
}
(true, false) => {
for variable in &inspection.variables {
println!("{}", variable_text(&variable.id));
}
}
(false, true) => {
for qualifier in &inspection.qualifiers {
println!("{}", qualifier_text(&qualifier.id));
}
}
(false, false) => {}
}
Ok(())
}
pub(crate) async fn print_entity_gets(
inspection: &WorkspaceInspection,
variable_ids: &[String],
qualifier_ids: &[String],
json: bool,
) -> Result<()> {
if json {
let mut variables = Vec::new();
for id in variable_ids {
variables.push(variable_get_json(inspection, id).await?);
}
let mut qualifiers = Vec::new();
for id in qualifier_ids {
qualifiers.push(qualifier_get_json(inspection, id).await?);
}
println!(
"{}",
serde_json::to_string_pretty(&WorkspaceEntityGetsJson {
workspace: inspection.root.display().to_string(),
variables,
qualifiers,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
let total = variable_ids.len() + qualifier_ids.len();
let mut first = true;
for id in variable_ids {
let variable = variable_for_id(inspection, id)?;
let path = inspection.root.join(&variable.path);
print_entity_get_header(&mut first, total, "variable", &variable.id);
print_workspace_file(&path).await?;
}
for id in qualifier_ids {
let qualifier = qualifier_for_id(inspection, id)?;
let path = inspection.root.join(&qualifier.path);
print_entity_get_header(&mut first, total, "qualifier", &qualifier.id);
print_workspace_file(&path).await?;
}
Ok(())
}
fn print_entity_get_header(first: &mut bool, total: usize, kind: &str, id: &str) {
if total <= 1 {
return;
}
if !*first {
println!();
}
*first = false;
let styled_id = match kind {
"variable" => variable_text(id),
"qualifier" => qualifier_text(id),
_ => stdout(Role::Fg, id),
};
println!("{} {}", stdout(Role::Fg, kind), styled_id);
}
async fn variable_get_json(
inspection: &WorkspaceInspection,
id: &str,
) -> Result<WorkspaceEntityGetJson> {
let variable = variable_for_id(inspection, id)?;
let value = serde_json::to_value(read_variable_toml(&inspection.root, variable).await?)
.map_err(|err| RototoError::new(err.to_string()))?;
Ok(WorkspaceEntityGetJson {
id: variable.id.clone(),
uri: variable.uri.clone(),
path: variable.path.display().to_string(),
value,
})
}
async fn qualifier_get_json(
inspection: &WorkspaceInspection,
id: &str,
) -> Result<WorkspaceEntityGetJson> {
let qualifier = qualifier_for_id(inspection, id)?;
let value = serde_json::to_value(read_toml(&inspection.root.join(&qualifier.path)).await?)
.map_err(|err| RototoError::new(err.to_string()))?;
Ok(WorkspaceEntityGetJson {
id: qualifier.id.clone(),
uri: qualifier.uri.clone(),
path: qualifier.path.display().to_string(),
value,
})
}
pub(crate) fn print_diagnostic_catalog(catalog: &DiagnosticCatalog, json: bool) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&DiagnosticCatalogJson {
scope: &catalog.scope,
subject: &catalog.subject,
diagnostics: &catalog.diagnostics,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
println!(
"{:<44} {:<6} title",
stdout(Role::Fg, "code"),
stdout(Role::Fg, "source"),
);
for diagnostic in &catalog.diagnostics {
println!(
"{:<44} {:<6} {}",
stdout(Role::Info, &diagnostic.code),
stdout(Role::Dim, diagnostic_source_label(&diagnostic.source)),
diagnostic.title
);
}
Ok(())
}
pub(crate) fn print_qualifier_resolutions(
workspace: &std::path::Path,
resolutions: &[rototo::model::QualifierResolution],
json: bool,
) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&ResolutionsJson {
workspace: workspace.display().to_string(),
values: resolutions,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
for resolution in resolutions {
println!(
"{}={}",
stdout(Role::Info, &resolution.id),
resolution.value
);
}
Ok(())
}
pub(crate) fn print_variable_resolutions(
workspace: &std::path::Path,
resolutions: &[VariableResolution],
json: bool,
) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(&ResolutionsJson {
workspace: workspace.display().to_string(),
values: resolutions,
})
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
for resolution in resolutions {
println!(
"{}={} ({})",
stdout(Role::Info, &resolution.id),
serde_json::to_string(&resolution.value)
.map_err(|err| RototoError::new(err.to_string()))?,
stdout(Role::Dim, &resolution.value_key)
);
}
Ok(())
}
pub(crate) fn print_diagnostic_catalog_entry(
diagnostic: &DiagnosticCatalogEntry,
json: bool,
) -> Result<()> {
if json {
println!(
"{}",
serde_json::to_string_pretty(diagnostic)
.map_err(|err| RototoError::new(err.to_string()))?
);
return Ok(());
}
println!("{}", stdout(Role::Info, &diagnostic.code));
println!(
" {}: {}",
stdout(Role::Fg, "source"),
diagnostic_source_label(&diagnostic.source)
);
if let Some(kind) = &diagnostic.kind {
println!(" {}: {}", stdout(Role::Fg, "kind"), kind);
}
println!(" {}: {}", stdout(Role::Fg, "title"), diagnostic.title);
println!(" {}: {}", stdout(Role::Fg, "help"), diagnostic.help);
Ok(())
}
fn inspection_json(inspection: &WorkspaceInspection) -> WorkspaceInspectionJson<'_> {
WorkspaceInspectionJson {
workspace: inspection.root.display().to_string(),
environments: &inspection.environments,
qualifiers: inspection.qualifiers.iter().map(qualifier_json).collect(),
variables: inspection.variables.iter().map(variable_json).collect(),
}
}
fn qualifier_json(qualifier: &QualifierInspection) -> WorkspaceFileJson<'_> {
WorkspaceFileJson {
id: &qualifier.id,
uri: &qualifier.uri,
path: qualifier.path.display().to_string(),
}
}
fn variable_json(variable: &VariableInspection) -> WorkspaceFileJson<'_> {
WorkspaceFileJson {
id: &variable.id,
uri: &variable.uri,
path: variable.path.display().to_string(),
}
}
async fn print_workspace_file(path: &std::path::Path) -> Result<()> {
print!(
"{}",
tokio::fs::read_to_string(path)
.await
.map_err(|err| RototoError::new(format!("failed to read {}: {err}", path.display())))?
);
Ok(())
}
fn print_diagnostics(diagnostics: &[Diagnostic]) {
for diagnostic in diagnostics {
println!(
"{}[{}]: {}: {}",
stdout(
severity_role(&diagnostic.severity),
severity_label(&diagnostic.severity)
),
stdout(Role::Info, &diagnostic.code),
stdout(Role::Dim, &diagnostic.path),
diagnostic.message
);
if let Some(rule) = &diagnostic.rule {
println!(" {}: {}", stdout(Role::Fg, "rule"), rule);
}
println!(" {}: {}", stdout(Role::Fg, "help"), diagnostic.help);
}
}
fn severity_label(severity: &Severity) -> &'static str {
match severity {
Severity::Error => "error",
}
}
fn severity_role(severity: &Severity) -> Role {
match severity {
Severity::Error => Role::Error,
}
}
fn diagnostic_source_label(source: &DiagnosticSource) -> &'static str {
match source {
DiagnosticSource::Kernel => "kernel",
DiagnosticSource::Schema => "schema",
DiagnosticSource::Custom => "custom",
}
}