use std::io::Write;
use std::path::PathBuf;
use clap::{Args, Subcommand, ValueEnum};
use pointbreak::model::{InputRequestId, ObservationId, RevisionId, RevisionRefV1};
use pointbreak::session::event::{FactPortRelationV1, FactRefV1};
use pointbreak::session::{FactPortOptions, port_review_fact};
use crate::cli::common::{SignableOptions, SigningSkip};
use crate::cli::id_resolver::IdResolver;
use crate::cli::output;
#[derive(Debug, Args)]
pub(super) struct FactArgs {
#[command(subcommand)]
command: FactCommand,
}
#[derive(Debug, Subcommand)]
enum FactCommand {
Port(FactPortArgs),
}
#[derive(Debug, Args)]
struct FactPortArgs {
#[arg(long, default_value = ".")]
repo: PathBuf,
#[arg(long)]
origin_revision: String,
#[arg(long)]
origin_fact: String,
#[arg(long)]
review_cursor: String,
#[arg(long, value_enum)]
relation: FactPortRelationArg,
#[arg(long)]
target_fact: Option<String>,
#[arg(long)]
rationale_content_hash: Option<String>,
#[arg(long)]
track: String,
#[arg(long)]
sign_key: Option<String>,
#[command(flatten)]
format_args: output::FormatArgs,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
enum FactPortRelationArg {
ContextOnly,
ReanchoredAs,
CarriedOpenAs,
ResolvedBy,
}
impl From<FactPortRelationArg> for FactPortRelationV1 {
fn from(value: FactPortRelationArg) -> Self {
match value {
FactPortRelationArg::ContextOnly => Self::ContextOnly,
FactPortRelationArg::ReanchoredAs => Self::ReanchoredAs,
FactPortRelationArg::CarriedOpenAs => Self::CarriedOpenAs,
FactPortRelationArg::ResolvedBy => Self::ResolvedBy,
}
}
}
pub(super) fn run(
args: FactArgs,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
FactCommand::Port(args) => port(args, stdout, stderr),
}
}
fn port(
args: FactPortArgs,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<(), Box<dyn std::error::Error>> {
let ids = IdResolver::new(&args.repo);
let origin_revision = parse_revision_ref(&ids, &args.origin_revision)?;
let origin_fact = parse_fact_ref(&ids, &args.origin_fact)?;
let mut options = FactPortOptions::new(
&args.repo,
origin_revision,
origin_fact,
args.review_cursor,
args.relation.into(),
args.track,
);
if let Some(target_fact) = args.target_fact {
options = options.with_target_fact(parse_fact_ref(&ids, &target_fact)?);
}
if let Some(hash) = args.rationale_content_hash {
options = options.with_rationale_content_hash(hash);
}
let (options, skip) = apply_signer(options, &args.repo, args.sign_key.as_deref(), stderr);
let result = port_review_fact(options)?;
crate::cli::common::surface_best_effort_skip(&skip, stderr);
let format = output::resolve_format(args.format_args.explicit(), output::OutputFormat::Json)?;
let text = if result.created {
format!("recorded {} fact port", result.port_id.as_str())
} else {
format!("fact port {} already recorded", result.port_id.as_str())
};
output::write_document(stdout, format, &result, || text)
}
fn parse_revision_ref(
ids: &IdResolver,
value: &str,
) -> Result<RevisionRefV1, Box<dyn std::error::Error>> {
let (revision, artifact_hash) = value
.split_once('@')
.ok_or("--origin-revision must be REVISION_ID@OBJECT_ARTIFACT_SHA256")?;
Ok(RevisionRefV1::new(
RevisionId::new(ids.rev(revision)?),
artifact_hash.to_owned(),
)?)
}
fn parse_fact_ref(ids: &IdResolver, value: &str) -> Result<FactRefV1, Box<dyn std::error::Error>> {
let fact = ids.fact(value)?;
if fact.starts_with("obs:") {
Ok(FactRefV1::Observation {
observation_id: ObservationId::new(fact),
})
} else if fact.starts_with("input-request:") {
Ok(FactRefV1::InputRequest {
input_request_id: InputRequestId::new(fact),
})
} else {
Err("fact must be an observation or input-request id".into())
}
}
fn apply_signer<O: SignableOptions>(
mut options: O,
repo: &std::path::Path,
sign_key: Option<&str>,
stderr: &mut dyn Write,
) -> (O, SigningSkip) {
let mut skip = None;
if let Some(resolved) = crate::cli::common::resolve_and_surface_signer(repo, sign_key, stderr) {
let (signed, signer_skip) = crate::cli::common::apply_resolved_signer(options, resolved);
options = signed;
skip = signer_skip;
}
(options, skip)
}