use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CapabilityId(String);
impl CapabilityId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for CapabilityId {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for CapabilityId {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Display for CapabilityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for CapabilityId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProviderId(String);
impl ProviderId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for ProviderId {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for ProviderId {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Display for ProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for ProviderId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityVerb {
Ready,
Set,
Go,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityState {
Ready,
Missing,
Incomplete,
Blocked,
Stale,
Optional,
NotNeeded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityRelevance {
Required,
Optional,
NotNeeded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityDescriptor {
pub id: CapabilityId,
pub title: String,
pub provider: ProviderId,
pub verbs: Vec<CapabilityVerb>,
pub default_relevance: CapabilityRelevance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NextAction {
pub command: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityReport {
pub id: CapabilityId,
pub title: String,
pub provider: ProviderId,
pub state: CapabilityState,
pub relevance: CapabilityRelevance,
pub summary: String,
pub next_action: Option<NextAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RunStatus {
Ok,
Changed,
Noop,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityActionKind {
Create,
Modify,
Delete,
Run,
Check,
Skip,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityAction {
pub kind: CapabilityActionKind,
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityRunReport {
pub id: CapabilityId,
#[serde(
serialize_with = "run_verb::serialize",
deserialize_with = "run_verb::deserialize"
)]
pub verb: CapabilityVerb,
pub status: RunStatus,
pub actions: Vec<CapabilityAction>,
}
mod run_verb {
use serde::{Deserialize, Deserializer, Serializer};
use super::CapabilityVerb;
#[allow(clippy::trivially_copy_pass_by_ref)] pub(super) fn serialize<S>(verb: &CapabilityVerb, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error as _;
match verb {
CapabilityVerb::Set => serializer.serialize_str("set"),
CapabilityVerb::Go => serializer.serialize_str("go"),
CapabilityVerb::Ready => Err(S::Error::custom("ready is not valid in a run report")),
}
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<CapabilityVerb, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error as _;
let raw = String::deserialize(deserializer)?;
match raw.as_str() {
"set" => Ok(CapabilityVerb::Set),
"go" => Ok(CapabilityVerb::Go),
other => Err(D::Error::unknown_variant(other, &["set", "go"])),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_round_trips_json() {
let descriptor = CapabilityDescriptor {
id: "linting".into(),
title: "Linting".into(),
provider: "rust".into(),
verbs: vec![
CapabilityVerb::Ready,
CapabilityVerb::Set,
CapabilityVerb::Go,
],
default_relevance: CapabilityRelevance::Required,
};
let json = serde_json::to_string(&descriptor).unwrap();
let back: CapabilityDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(descriptor, back);
assert!(json.contains("\"default_relevance\":\"required\""));
}
#[test]
fn report_round_trips_json_with_null_next_action() {
let report = CapabilityReport {
id: "deploy".into(),
title: "Deploy".into(),
provider: "deploy".into(),
state: CapabilityState::NotNeeded,
relevance: CapabilityRelevance::NotNeeded,
summary: "deployment is not needed".into(),
next_action: None,
};
let json = serde_json::to_string(&report).unwrap();
let back: CapabilityReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, back);
assert!(json.contains("\"state\":\"not-needed\""));
assert!(json.contains("\"next_action\":null"));
}
#[test]
fn run_report_round_trips_json() {
let report = CapabilityRunReport {
id: "linting".into(),
verb: CapabilityVerb::Set,
status: RunStatus::Changed,
actions: vec![
CapabilityAction {
kind: CapabilityActionKind::Create,
summary: "created clippy config".into(),
path: Some("clippy.toml".into()),
},
CapabilityAction {
kind: CapabilityActionKind::Run,
summary: "ran clippy".into(),
path: None,
},
CapabilityAction {
kind: CapabilityActionKind::Error,
summary: "clippy failed".into(),
path: None,
},
],
};
let json = serde_json::to_string(&report).unwrap();
let back: CapabilityRunReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, back);
}
#[test]
fn run_report_rejects_ready_verb_on_wire() {
let report = CapabilityRunReport {
id: "linting".into(),
verb: CapabilityVerb::Ready,
status: RunStatus::Ok,
actions: Vec::new(),
};
assert!(serde_json::to_string(&report).is_err());
let err = serde_json::from_str::<CapabilityRunReport>(
r#"{"id":"linting","verb":"ready","status":"ok","actions":[]}"#,
)
.unwrap_err();
assert!(err.to_string().contains("unknown variant"));
}
}