use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EntityId(pub String);
impl EntityId {
pub fn new(manager: &str, package: &str, version: &str, path: &str, symbol: &str) -> Self {
Self(format!(
"scip:{manager}/{package}/{version}/{path}#{symbol}"
))
}
pub fn local(path: &str, symbol: &str) -> Self {
Self(format!("scip:local/project/0.0.0/{path}#{symbol}"))
}
pub fn file_path(&self) -> Option<&str> {
let after_version = self.0.split('/').skip(3).collect::<Vec<_>>().join("/");
let path = after_version.split('#').next()?;
let start = self.0.find(path)?;
let end = self.0.find('#').unwrap_or(self.0.len());
Some(&self.0[start..end])
}
pub fn symbol_name(&self) -> Option<&str> {
self.0.split('#').nth(1)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EntityKind {
Function {
is_async: bool,
parameter_count: usize,
return_type: Option<String>,
},
ApiEndpoint {
method: String, route: String, handler: String, },
DataModel { fields: Vec<String> },
Feature {
description: String,
source: String, },
TestCase {
test_type: TestType,
targets: Vec<EntityId>, },
Requirement {
ticket_id: Option<String>,
acceptance_criteria: Vec<String>,
},
Module {
language: String,
exports: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TestType {
Unit,
Integration,
E2E,
Property,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UcmEntity {
pub id: EntityId,
pub kind: EntityKind,
pub name: String,
pub file_path: String,
pub line_range: Option<(usize, usize)>,
pub language: String,
pub discovered_at: chrono::DateTime<chrono::Utc>,
pub discovery_source: DiscoverySource,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DiscoverySource {
StaticAnalysis,
GitDiff,
TicketSystem,
ApiTraffic,
HistoricalContext,
Manual,
}
impl UcmEntity {
pub fn new(
id: EntityId,
kind: EntityKind,
name: impl Into<String>,
file_path: impl Into<String>,
language: impl Into<String>,
source: DiscoverySource,
) -> Self {
Self {
id,
kind,
name: name.into(),
file_path: file_path.into(),
line_range: None,
language: language.into(),
discovered_at: chrono::Utc::now(),
discovery_source: source,
}
}
pub fn with_line_range(mut self, start: usize, end: usize) -> Self {
self.line_range = Some((start, end));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scip_id_construction() {
let id = EntityId::new(
"npm",
"my-app",
"1.0.0",
"src/auth/service.ts",
"validateToken",
);
assert_eq!(
id.as_str(),
"scip:npm/my-app/1.0.0/src/auth/service.ts#validateToken"
);
}
#[test]
fn test_scip_id_local() {
let id = EntityId::local("src/main.rs", "main");
assert!(id.as_str().contains("local/project"));
}
#[test]
fn test_entity_symbol_name() {
let id = EntityId::new(
"npm",
"my-app",
"1.0.0",
"src/auth/service.ts",
"validateToken",
);
assert_eq!(id.symbol_name(), Some("validateToken"));
}
}