use crate::config::CliConfig;
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::collections::BTreeSet;
use std::process::Command;
const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
const SDK: &str = "cufflink-fn";
#[derive(Debug, Default, PartialEq, Eq, Deserialize)]
pub struct PlatformVersion {
#[serde(default, deserialize_with = "number")]
pub api_version: u32,
#[serde(default, deserialize_with = "text")]
pub platform_version: Option<String>,
#[serde(default, deserialize_with = "names")]
pub supports: Vec<String>,
}
fn number<'de, D: Deserializer<'de>>(field: D) -> Result<u32, D::Error> {
Ok(Value::deserialize(field)?
.as_u64()
.and_then(|n| u32::try_from(n).ok())
.unwrap_or(0))
}
fn text<'de, D: Deserializer<'de>>(field: D) -> Result<Option<String>, D::Error> {
Ok(Value::deserialize(field)?.as_str().map(str::to_string))
}
fn names<'de, D: Deserializer<'de>>(field: D) -> Result<Vec<String>, D::Error> {
Ok(Value::deserialize(field)?
.as_array()
.into_iter()
.flatten()
.filter_map(|entry| entry.as_str().map(str::to_string))
.collect())
}
fn labelled(version: &str) -> String {
if version.starts_with(|c: char| c.is_ascii_digit()) {
format!("v{version}")
} else {
version.to_string()
}
}
fn listed(names: &[String]) -> String {
if names.is_empty() {
"none".to_string()
} else {
names.join(", ")
}
}
impl PlatformVersion {
fn label(&self) -> String {
self.platform_version
.as_deref()
.map_or_else(|| "of an unknown version".to_string(), labelled)
}
}
fn sdk_versions(metadata: &Value) -> BTreeSet<String> {
metadata["packages"]
.as_array()
.into_iter()
.flatten()
.filter(|package| package["name"] == SDK)
.filter_map(|package| package["version"].as_str())
.map(labelled)
.collect()
}
pub fn sdk_version() -> Option<String> {
Command::new("cargo")
.args(["metadata", "--format-version", "1"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| serde_json::from_slice::<Value>(&output.stdout).ok())
.map(|metadata| sdk_versions(&metadata))
.filter(|versions| !versions.is_empty())
.map(|versions| versions.into_iter().collect::<Vec<_>>().join(", "))
}
pub fn negotiated(platform: &PlatformVersion, sdk: Option<&str>, manifest: &Value) -> String {
let needs: Vec<String> = manifest["required_platform_features"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_string)
.collect();
format!(
"platform {} (features {}) ← sdk {} (needs {})",
platform.label(),
listed(&platform.supports),
sdk.unwrap_or("of an unknown version"),
listed(&needs)
)
}
fn described(api_url: &str, platform: &PlatformVersion) -> String {
format!(
"platform {} at {api_url} (deploy API {})\nfeatures {}",
platform.label(),
platform.api_version,
listed(&platform.supports)
)
}
pub async fn print(env: Option<&str>) -> eyre::Result<()> {
println!("cufflink {CLI_VERSION}");
if env.is_some() {
let config = CliConfig::load_with_env(env)?;
let url = format!("{}/api/version", config.api_url);
let asked = config
.http_client()
.get(&url)
.send()
.await
.map_err(|e| eyre::eyre!("Could not reach the platform at {url}: {e}"))?;
let status = asked.status();
if !status.is_success() {
eyre::bail!("{url} answered {status}");
}
println!("{}", described(&config.api_url, &asked.json().await?));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn platform(version: Option<&str>, supports: &[&str]) -> PlatformVersion {
PlatformVersion {
api_version: 1,
platform_version: version.map(str::to_string),
supports: supports.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn a_deploy_says_what_the_platform_offers_and_the_service_needs() {
let offered = platform(
Some("sha-4f2a"),
&["bundled_wasm", "db_v2", "route_posture"],
);
assert_eq!(
negotiated(
&offered,
Some("v0.27.0"),
&json!({"required_platform_features": ["db_v2", "route_posture"]})
),
"platform sha-4f2a (features bundled_wasm, db_v2, route_posture) ← sdk v0.27.0 \
(needs db_v2, route_posture)"
);
assert_eq!(
negotiated(
&platform(Some("0.27.0"), &[]),
None,
&json!({"name": "notes"})
),
"platform v0.27.0 (features none) ← sdk of an unknown version (needs none)"
);
assert_eq!(
negotiated(&platform(None, &["streams"]), Some("v0.26.1"), &json!({})),
"platform of an unknown version (features streams) ← sdk v0.26.1 (needs none)"
);
}
#[test]
fn the_sdk_is_every_version_of_it_cargo_resolved() {
let metadata = json!({"packages": [
{"name": "cufflink-fn", "version": "0.27.0"},
{"name": "cufflink", "version": "0.11.0"},
{"name": "cufflink-fn", "version": "0.26.1"},
{"name": "cufflink-fn", "version": "0.27.0"},
]});
assert_eq!(
sdk_versions(&metadata).into_iter().collect::<Vec<_>>(),
["v0.26.1", "v0.27.0"]
);
assert!(sdk_versions(&json!({})).is_empty());
}
#[test]
fn a_platform_is_read_from_what_it_answers() {
let answered: PlatformVersion = serde_json::from_value(json!({
"api_version": 1,
"platform_version": "sha-4f2a",
"supports": ["bundled_wasm", "streams"],
}))
.expect("a platform");
assert_eq!(
answered,
platform(Some("sha-4f2a"), &["bundled_wasm", "streams"])
);
assert_eq!(
described("http://localhost:8080", &answered),
"platform sha-4f2a at http://localhost:8080 (deploy API 1)\n\
features bundled_wasm, streams"
);
let silent: PlatformVersion = serde_json::from_value(json!({})).expect("a platform");
assert_eq!(silent, PlatformVersion::default());
let odd: PlatformVersion = serde_json::from_value(json!({
"api_version": "1",
"platform_version": 7,
"supports": ["bundled_wasm", 7, {"streams": true}, "streams"],
}))
.expect("a platform that answers what is not of its type");
assert_eq!(
odd,
PlatformVersion {
api_version: 0,
..platform(None, &["bundled_wasm", "streams"])
}
);
let unlisted: PlatformVersion =
serde_json::from_value(json!({"supports": "all"})).expect("a platform");
assert!(unlisted.supports.is_empty());
}
}