mod branches;
mod claims;
mod comparison;
mod decisions;
mod interchange;
mod proposals;
mod slices;
mod upstream;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use clap::{Args, Subcommand};
use graphforge_api::{
DiscoverResearchProjectsRequest, GraphForge, OperationId, ResearchProjectDiscoveryLimits,
ResearchProjectDiscoveryQuery, UpdateResearchMetadataRequest, WorkspaceResearchMetadata,
WriteContext,
};
use serde_json::json;
use crate::{canonical_uuid, write_execution_result};
#[derive(Subcommand)]
pub(crate) enum ResearchCommand {
Decision {
#[command(subcommand)]
command: decisions::DecisionCommand,
},
Interchange {
#[command(subcommand)]
command: interchange::InterchangeCommand,
},
Proposal {
#[command(subcommand)]
command: proposals::ProposalCommand,
},
Upstream {
#[command(subcommand)]
command: upstream::UpstreamCommand,
},
Compare(comparison::Request),
Claim {
#[command(subcommand)]
command: claims::ClaimCommand,
},
Branch {
#[command(subcommand)]
command: branches::BranchCommand,
},
Slice {
#[command(subcommand)]
command: slices::SliceCommand,
},
Version {
#[command(subcommand)]
command: crate::research_versions_cli::ResearchVersionCommand,
},
Metadata {
#[command(subcommand)]
command: ResearchMetadataCommand,
},
Discover(ResearchDiscoverArgs),
}
#[derive(Subcommand)]
pub(crate) enum ResearchMetadataCommand {
Show,
Update(ResearchMetadataUpdateArgs),
}
#[derive(Args)]
pub(crate) struct ResearchMetadataUpdateArgs {
#[arg(long)]
file: PathBuf,
#[arg(long)]
operation_uuid: String,
#[arg(long)]
actor_uuid: Option<String>,
}
#[derive(Args)]
pub(crate) struct ResearchDiscoverArgs {
#[arg(long = "root", required = true)]
roots: Vec<PathBuf>,
#[arg(long)]
free_text: Option<String>,
#[arg(long)]
language: Vec<String>,
#[arg(long)]
subject: Vec<String>,
#[arg(long)]
ontology: Vec<String>,
#[arg(long = "source-type")]
source_types: Vec<String>,
#[arg(long)]
temporal_label: Option<String>,
#[arg(long, default_value_t = 1_024)]
max_projects: usize,
#[arg(long, default_value_t = 1_024)]
max_candidates: usize,
}
pub(crate) fn run_research(
graph: &mut GraphForge,
command: ResearchCommand,
json: bool,
output: &mut dyn Write,
) -> Result<(), crate::CliRuntimeError> {
match command {
ResearchCommand::Decision { command } => decisions::run(graph, command, output)?,
ResearchCommand::Interchange { command } => interchange::run(graph, command, output)?,
ResearchCommand::Proposal { command } => proposals::run(graph, command, json, output)?,
ResearchCommand::Upstream { command } => upstream::run(graph, command, json, output)?,
ResearchCommand::Compare(args) => comparison::run(graph, args, json, output)?,
ResearchCommand::Claim { command } => claims::run(graph, command, json, output)?,
ResearchCommand::Branch { command } => branches::run(graph, command, json, output)?,
ResearchCommand::Slice { command } => slices::run(graph, command, json, output)?,
ResearchCommand::Version { command } => {
crate::research_versions_cli::run(graph, command, json, output)?;
}
ResearchCommand::Metadata { command } => match command {
ResearchMetadataCommand::Show => {
let summary = graph.research_project_summary()?;
let payload = json!({
"project_path": summary.project_path.display().to_string(),
"identity": {
"volume_serial": summary.identity.volume_serial,
"file_id_hex": summary.identity.file_id_hex,
"generation_uuid": summary.identity.generation_uuid.hyphenated().to_string(),
},
"metadata": summary.metadata,
});
let text = if json {
serde_json::to_string_pretty(&payload)
.map_err(|error| graphforge_api::GfError::Validation(error.to_string()))?
} else {
serde_json::to_string(&payload)
.map_err(|error| graphforge_api::GfError::Validation(error.to_string()))?
};
writeln!(output, "{text}")
.map_err(|error| graphforge_api::GfError::Execution(error.to_string()))?;
}
ResearchMetadataCommand::Update(args) => {
let bytes = fs::read(&args.file).map_err(|error| {
graphforge_api::GfError::Validation(format!(
"metadata file is unreadable: {error}"
))
})?;
let metadata = WorkspaceResearchMetadata::from_canonical_json(&bytes)?;
graph.update_research_metadata(UpdateResearchMetadataRequest {
context: WriteContext {
operation_uuid: OperationId(canonical_uuid(&args.operation_uuid)?),
actor_uuid: args.actor_uuid.as_deref().map(canonical_uuid).transpose()?,
},
metadata,
})?;
let text = if json {
r#"{"status":"updated"}"#
} else {
"updated"
};
writeln!(output, "{text}")
.map_err(|error| graphforge_api::GfError::Execution(error.to_string()))?;
}
},
ResearchCommand::Discover(args) => {
let result =
GraphForge::discover_research_projects(&DiscoverResearchProjectsRequest {
project_roots: args.roots,
query: ResearchProjectDiscoveryQuery {
free_text: args.free_text,
languages: args.language,
subjects: args.subject,
ontologies: args.ontology,
source_types: args.source_types,
temporal_label: args.temporal_label,
},
limits: ResearchProjectDiscoveryLimits {
max_projects: args.max_projects,
max_candidates: args.max_candidates,
},
})?;
write_execution_result(&result, json, output)?;
}
}
Ok(())
}