use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::client::TelemetryHandle;
use super::events::Event;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Identity {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub editor_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub org_id: Option<String>,
#[serde(default)]
pub aliased_pairs: Vec<AliasedPair>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AliasedPair {
pub machine_id: String,
pub editor_id: String,
}
pub fn identity_path(provider_dir: &Path) -> PathBuf {
provider_dir.join("identity.json")
}
pub fn read(path: &Path) -> Option<Identity> {
let raw = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&raw).ok()
}
pub fn write(path: &Path, identity: &Identity) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let serialized = serde_json::to_string_pretty(identity).map_err(std::io::Error::other)?;
std::fs::write(path, serialized)
}
pub fn already_aliased(identity: &Identity, machine_id: &str, editor_id: &str) -> bool {
identity
.aliased_pairs
.iter()
.any(|p| p.machine_id == machine_id && p.editor_id == editor_id)
}
pub fn record_auth_success(
handle: &TelemetryHandle,
provider_dir: &Path,
machine_id: &str,
editor_id: &str,
org_id: Option<&str>,
) {
let path = identity_path(provider_dir);
let mut identity = read(&path).unwrap_or_default();
if !already_aliased(&identity, machine_id, editor_id) {
handle.capture(Event::create_alias(machine_id, editor_id));
identity.aliased_pairs.push(AliasedPair {
machine_id: machine_id.into(),
editor_id: editor_id.into(),
});
}
identity.editor_id = Some(editor_id.into());
if let Some(o) = org_id {
identity.org_id = Some(o.into());
}
let _ = write(&path, &identity);
}
pub fn resolve_distinct_id(provider_dir: &Path, machine_id: &str) -> String {
read(&identity_path(provider_dir))
.and_then(|i| i.editor_id)
.unwrap_or_else(|| machine_id.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn already_aliased_returns_true_after_record() {
let mut id = Identity::default();
id.aliased_pairs.push(AliasedPair {
machine_id: "mach_x".into(),
editor_id: "edt_42".into(),
});
assert!(already_aliased(&id, "mach_x", "edt_42"));
assert!(!already_aliased(&id, "mach_x", "edt_99"));
assert!(!already_aliased(&id, "mach_y", "edt_42"));
}
#[test]
fn write_then_read_roundtrip() {
let tmp = TempDir::new().unwrap();
let path = identity_path(tmp.path());
let identity = Identity {
editor_id: Some("edt_42".into()),
org_id: Some("org_7".into()),
aliased_pairs: vec![AliasedPair {
machine_id: "mach_x".into(),
editor_id: "edt_42".into(),
}],
};
write(&path, &identity).unwrap();
let loaded = read(&path).unwrap();
assert_eq!(loaded, identity);
}
#[test]
fn resolve_distinct_id_prefers_editor_id_when_present() {
let tmp = TempDir::new().unwrap();
let identity = Identity {
editor_id: Some("edt_42".into()),
..Default::default()
};
write(&identity_path(tmp.path()), &identity).unwrap();
assert_eq!(resolve_distinct_id(tmp.path(), "mach_x"), "edt_42");
}
#[test]
fn resolve_distinct_id_falls_back_to_machine_id() {
let tmp = TempDir::new().unwrap();
assert_eq!(resolve_distinct_id(tmp.path(), "mach_x"), "mach_x");
}
#[test]
fn record_auth_success_aliases_once_and_persists() {
let tmp = TempDir::new().unwrap();
let handle = TelemetryHandle::disabled("mach_x".into(), false);
record_auth_success(&handle, tmp.path(), "mach_x", "edt_42", Some("org_7"));
let loaded = read(&identity_path(tmp.path())).unwrap();
assert_eq!(loaded.editor_id.as_deref(), Some("edt_42"));
assert_eq!(loaded.org_id.as_deref(), Some("org_7"));
assert_eq!(loaded.aliased_pairs.len(), 1);
record_auth_success(&handle, tmp.path(), "mach_x", "edt_42", Some("org_7"));
let loaded = read(&identity_path(tmp.path())).unwrap();
assert_eq!(loaded.aliased_pairs.len(), 1);
}
}