use std::collections::BTreeMap;
use reqwest::Url;
use serde::{Deserialize, Serialize};
pub(crate) const GRITPACK_MANIFEST_FILE: &str = "gritpack.toml";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseResponse {
pub(crate) uuid: String,
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: Option<String>,
pub(crate) encoding: Option<String>,
pub(crate) target: Option<String>,
pub(crate) source_root: Option<String>,
pub(crate) dependencies: Option<BTreeMap<String, ReleaseDependencySpec>>,
pub(crate) environment: Option<ReleaseEnvironmentResponse>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) stage: BTreeMap<String, StageSpec>,
#[serde(default, skip_serializing)]
pub(crate) handoff: Option<ReleaseHandoffResponse>,
pub(crate) advisory: Option<ReleaseAdvisoryResponse>,
#[serde(default)]
pub(crate) artifacts: ReleaseArtifactsResponse,
pub(crate) checksum: Option<String>,
pub(crate) file_id: Option<String>,
pub(crate) presigned_url: Option<String>,
#[serde(default)]
pub(crate) trust: Option<ReleaseTrustSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseTrustSummary {
pub(crate) artifact_profile: Option<String>,
pub(crate) trust_tier: Option<String>,
pub(crate) content_class: Option<String>,
pub(crate) publisher_signature: Option<ReleaseTrustStatus>,
pub(crate) hub_attestation: Option<ReleaseTrustStatus>,
pub(crate) artifact_digest_algo: Option<String>,
pub(crate) artifact_digest: Option<String>,
pub(crate) payload_digest_algo: Option<String>,
pub(crate) payload_digest: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseTrustStatus {
pub(crate) status: Option<String>,
pub(crate) issuer: Option<String>,
pub(crate) issuer_key_id: Option<String>,
pub(crate) policy: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct ReleaseEnvironmentResponse {
pub(crate) layout: Option<String>,
pub(crate) entry: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct StageSpec {
#[serde(default)]
pub(crate) command: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct ReleaseHandoffResponse {
#[serde(default)]
pub(crate) build: Vec<String>,
#[serde(default)]
pub(crate) test: Vec<String>,
#[serde(default)]
pub(crate) run: Vec<String>,
}
impl ReleaseHandoffResponse {
pub(crate) fn stage_named(&self, name: &str) -> Option<StageSpec> {
let command = match name {
"build" => &self.build,
"test" => &self.test,
"run" => &self.run,
_ => return None,
};
if command.is_empty() {
return None;
}
Some(StageSpec {
command: command.clone(),
env: BTreeMap::new(),
})
}
pub(crate) fn merge_into_stage_map(&self, stages: &mut BTreeMap<String, StageSpec>) {
for stage_name in ["build", "test", "run"] {
if let Some(stage) = self.stage_named(stage_name) {
stages.entry(stage_name.to_string()).or_insert(stage);
}
}
}
}
impl ReleaseResponse {
pub(crate) fn normalized_stages(&self) -> BTreeMap<String, StageSpec> {
let mut stages = self.stage.clone();
if let Some(handoff) = self.handoff.as_ref() {
handoff.merge_into_stage_map(&mut stages);
}
stages
}
pub(crate) fn content_class(&self) -> &str {
self.trust
.as_ref()
.and_then(|trust| trust.content_class.as_deref())
.unwrap_or_else(|| {
if self.artifacts.executables.is_empty() && self.artifacts.libraries.is_empty() {
"source_only"
} else {
"contains_binaries"
}
})
}
pub(crate) fn trust_tier(&self) -> &str {
if let Some(tier) = self
.trust
.as_ref()
.and_then(|trust| trust.trust_tier.as_deref())
{
return tier;
}
match self
.trust
.as_ref()
.and_then(|trust| trust.publisher_signature.as_ref())
.and_then(|status| status.status.as_deref())
{
Some("verified") => "full",
Some("invalid") => "blocked",
_ => "low",
}
}
pub(crate) fn has_invalid_signature(&self) -> bool {
matches!(
self.trust
.as_ref()
.and_then(|trust| trust.publisher_signature.as_ref())
.and_then(|status| status.status.as_deref()),
Some("invalid")
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseAdvisoryResponse {
#[serde(default)]
pub(crate) deprecated: bool,
pub(crate) message: Option<String>,
pub(crate) replacement: Option<String>,
#[serde(default)]
pub(crate) compatibility: Vec<ReleaseCompatibilityAdvisoryResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseCompatibilityAdvisoryResponse {
pub(crate) severity: ReleaseCompatibilitySeverity,
pub(crate) message: String,
pub(crate) package: Option<String>,
pub(crate) version: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ReleaseCompatibilitySeverity {
Warning,
Blocker,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct ReleaseArtifactsResponse {
#[serde(default)]
pub(crate) libraries: Vec<ReleaseLibraryArtifactResponse>,
#[serde(default)]
pub(crate) executables: Vec<ExecutableArtifactResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseLibraryArtifactResponse {
pub(crate) name: String,
pub(crate) target: String,
pub(crate) files: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExecutableArtifactResponse {
pub(crate) name: String,
pub(crate) path: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ReleaseDependencySpec {
pub(crate) version: Option<String>,
pub(crate) dialect: Option<String>,
pub(crate) target: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct PublishResponse {
pub(crate) id: String,
pub(crate) file_id: String,
pub(crate) presigned_url: String,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RegisterPublisherKeyRequest {
pub(crate) publisher_kind: String,
pub(crate) publisher_id: String,
pub(crate) key_id: String,
pub(crate) public_key: String,
pub(crate) public_key_format: String,
#[serde(default)]
pub(crate) allowed_scopes: Vec<String>,
#[serde(default)]
pub(crate) allowed_packages: Vec<String>,
pub(crate) activation_mode: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct PublisherKeyRecordResponse {
pub(crate) publisher_kind: String,
pub(crate) publisher_id: String,
pub(crate) key_id: String,
pub(crate) status: String,
pub(crate) public_key: String,
pub(crate) public_key_format: String,
#[serde(default)]
pub(crate) allowed_scopes: Vec<String>,
#[serde(default)]
pub(crate) allowed_packages: Vec<String>,
pub(crate) created_at: String,
pub(crate) activates_at: Option<String>,
pub(crate) expires_at: Option<String>,
pub(crate) revoked_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct PackageManifestFile {
pub(crate) package: LocalPackageSection,
#[serde(default)]
pub(crate) source: LocalSourceSection,
#[serde(default, rename = "lib")]
pub(crate) libraries: Vec<ReleaseLibraryArtifactResponse>,
#[serde(default, rename = "bin")]
pub(crate) executables: Vec<ExecutableArtifactResponse>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) stage: BTreeMap<String, StageSpec>,
#[serde(default, skip_serializing)]
pub(crate) handoff: Option<ReleaseHandoffResponse>,
pub(crate) dependencies: Option<BTreeMap<String, LocalDependencySpec>>,
}
impl PackageManifestFile {
pub(crate) fn normalize_stages(&mut self) {
if let Some(handoff) = self.handoff.as_ref() {
handoff.merge_into_stage_map(&mut self.stage);
}
self.handoff = None;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LocalPackageSection {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: String,
pub(crate) target: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LocalSourceSection {
#[serde(default = "default_source_root")]
pub(crate) root: String,
#[serde(default)]
pub(crate) include: Vec<String>,
#[serde(default)]
pub(crate) include_paths: Vec<String>,
#[serde(default)]
pub(crate) library_paths: Vec<String>,
#[serde(default)]
pub(crate) tool_paths: Vec<String>,
}
impl Default for LocalSourceSection {
fn default() -> Self {
Self {
root: default_source_root(),
include: Vec::new(),
include_paths: Vec::new(),
library_paths: Vec::new(),
tool_paths: Vec::new(),
}
}
}
fn default_source_root() -> String {
".".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct LocalDependencySpec {
pub(crate) version: Option<String>,
pub(crate) dialect: Option<String>,
pub(crate) target: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct PackagesState {
pub(crate) version: u32,
#[serde(default)]
pub(crate) installs: Vec<InstalledPackage>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub(crate) struct ToolState {
pub(crate) version: u32,
#[serde(default)]
pub(crate) installs: Vec<InstalledTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InstalledTool {
pub(crate) package_name: String,
pub(crate) version: String,
pub(crate) dialect: String,
pub(crate) target: Option<String>,
pub(crate) package_uuid: String,
pub(crate) install_path: String,
#[serde(default)]
pub(crate) commands: Vec<InstalledToolCommand>,
pub(crate) checksum: Option<String>,
pub(crate) file_id: Option<String>,
pub(crate) installed_at_unix: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InstalledToolCommand {
pub(crate) name: String,
pub(crate) path: String,
}
#[derive(Debug, Clone)]
pub(crate) struct ToolPackageRequest {
pub(crate) name: String,
pub(crate) version_constraint: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ToolUpgradeImpact {
pub(crate) request: String,
pub(crate) package_name: String,
pub(crate) current_version: String,
pub(crate) target_version: Option<String>,
pub(crate) target_platform: Option<String>,
pub(crate) command_names: Vec<String>,
pub(crate) package_count: usize,
pub(crate) max_depth: usize,
pub(crate) deepest_packages: Vec<String>,
pub(crate) findings: Vec<ImpactFinding>,
pub(crate) gaps: Vec<ImpactGap>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ImpactFinding {
pub(crate) severity: ImpactSeverity,
pub(crate) category: &'static str,
pub(crate) subject: String,
pub(crate) detail: String,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ImpactGap {
pub(crate) category: &'static str,
pub(crate) detail: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ImpactSeverity {
Info,
Warning,
Risk,
Blocker,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedImpactNode {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: String,
pub(crate) target: Option<String>,
pub(crate) depth: usize,
}
#[derive(Debug, Serialize)]
pub(crate) struct ToolUpgradeImpactJsonReport {
pub(crate) schema: &'static str,
pub(crate) schema_version: u32,
pub(crate) generated_at_unix: u64,
pub(crate) reports: Vec<ToolUpgradeImpactJsonEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct ToolUpgradeImpactJsonEntry {
pub(crate) request: String,
pub(crate) summary: ToolUpgradeImpactSummary,
pub(crate) graph: ToolUpgradeImpactGraph,
pub(crate) findings: Vec<ImpactFinding>,
pub(crate) gaps: Vec<ImpactGap>,
}
#[derive(Debug, Serialize)]
pub(crate) struct ToolUpgradeImpactSummary {
pub(crate) package: String,
pub(crate) status: &'static str,
pub(crate) current_version: String,
pub(crate) target_version: Option<String>,
pub(crate) target_platform: Option<String>,
pub(crate) commands: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct ToolUpgradeImpactGraph {
pub(crate) package_count: usize,
pub(crate) max_depth: usize,
pub(crate) deepest_packages: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InstalledPackage {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: String,
pub(crate) target: Option<String>,
pub(crate) encoding: String,
pub(crate) package_uuid: String,
pub(crate) install_path: String,
pub(crate) source_root: Option<String>,
#[serde(default)]
pub(crate) direct: bool,
#[serde(default)]
pub(crate) dependencies: Vec<InstalledDependency>,
#[serde(default)]
pub(crate) libraries: Vec<ReleaseLibraryArtifactResponse>,
#[serde(default)]
pub(crate) executables: Vec<ExecutableArtifactResponse>,
pub(crate) checksum: Option<String>,
pub(crate) file_id: Option<String>,
pub(crate) installed_at_unix: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InstalledDependency {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: Option<String>,
pub(crate) target: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct CachedReleaseMetadata {
pub(crate) package_uuid: String,
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: Option<String>,
pub(crate) target: Option<String>,
pub(crate) checksum: String,
pub(crate) file_id: Option<String>,
pub(crate) cached_at_unix: u64,
}
#[derive(Debug, Serialize)]
pub(crate) struct EnvironmentManifest {
pub(crate) version: u32,
pub(crate) root: EnvironmentManifestRoot,
pub(crate) paths: EnvironmentManifestPaths,
pub(crate) packages: Vec<InstalledPackage>,
pub(crate) bins: Vec<EnvironmentManifestCommand>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) stage: BTreeMap<String, StageSpec>,
#[allow(dead_code)]
#[serde(skip_serializing)]
pub(crate) handoff: ReleaseHandoffResponse,
}
#[derive(Debug, Serialize)]
pub(crate) struct EnvironmentManifestRoot {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) dialect: String,
pub(crate) target: Option<String>,
pub(crate) package_uuid: String,
pub(crate) layout: Option<String>,
pub(crate) entry: Option<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct EnvironmentManifestPaths {
pub(crate) root: String,
pub(crate) packages: String,
pub(crate) bin: String,
pub(crate) work: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct EnvironmentManifestCommand {
pub(crate) name: String,
pub(crate) path: String,
pub(crate) package: String,
pub(crate) version: String,
}
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("binary state error: {0}")]
BinaryState(#[from] bincode::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("toml error: {0}")]
Toml(#[from] toml::de::Error),
#[error("toml error: {0}")]
TomlSerialize(#[from] toml::ser::Error),
#[error("{0}")]
Message(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ReconcileMode {
Sync,
Upgrade,
}
impl ReconcileMode {
pub(crate) fn completed_label(self) -> &'static str {
match self {
Self::Sync => "synced",
Self::Upgrade => "upgraded",
}
}
pub(crate) fn keeps_satisfying_existing_versions(self) -> bool {
matches!(self, Self::Sync)
}
}
#[allow(dead_code)]
fn _keep_reqwest_url_imported_for_future_model_moves(_: &Url) {}