use serde::{Deserialize, Serialize};
use super::anchor::validate_anchor_path;
use super::sealed::SealedBlobV1;
use super::{require_schema, validate_text};
use crate::ValidationError;
pub const CODE_GRAPH_SCHEMA: &str = "leanctx.hive.code-graph/v1";
pub const CODE_GRAPH_PUT_SCHEMA: &str = "leanctx.hive.code-graph-put/v1";
pub const CODE_GRAPH_ACK_SCHEMA: &str = "leanctx.hive.code-graph-ack/v1";
pub const MAX_CODE_GRAPH_NODES: usize = super::graph::MAX_GRAPH_NODES;
pub const MAX_CODE_GRAPH_EDGES: usize = super::graph::MAX_GRAPH_EDGES;
pub const MAX_CODE_NODE_ID_CHARS: usize = 700;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeGraphMode {
Anchors,
Full,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeNodeKind {
File,
Symbol,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeEdgeKind {
Contains,
Imports,
Calls,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CodeNodeV1 {
pub id: String,
pub kind: CodeNodeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_kind: Option<String>,
}
impl CodeNodeV1 {
fn validate(&self) -> Result<(), ValidationError> {
if self.id.chars().count() > MAX_CODE_NODE_ID_CHARS {
return Err(ValidationError::new(format!(
"a code node id has at most {MAX_CODE_NODE_ID_CHARS} characters"
)));
}
let rest = self
.id
.strip_prefix("code:")
.ok_or_else(|| ValidationError::new("code node ids start with code:"))?;
let (path, symbol) = match rest.split_once('#') {
Some((path, symbol)) => (path, Some(symbol)),
None => (rest, None),
};
validate_anchor_path(path)?;
match (self.kind, symbol) {
(CodeNodeKind::File, None) => {}
(CodeNodeKind::Symbol, Some(symbol)) => {
validate_text(
symbol,
super::anchor::MAX_ANCHOR_SYMBOL_CHARS,
"symbol",
false,
)?;
}
_ => {
return Err(ValidationError::new(
"a file node has no #symbol and a symbol node has one",
));
}
}
for (hint, field) in [(&self.lang, "lang"), (&self.symbol_kind, "symbol_kind")] {
if let Some(hint) = hint
&& (hint.is_empty()
|| hint.len() > 32
|| !hint
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_'))
{
return Err(ValidationError::new(format!(
"{field} is 1-32 characters of [a-z0-9_]"
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CodeEdgeV1 {
pub from: String,
pub to: String,
#[serde(rename = "type")]
pub kind: CodeEdgeKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CodeGraphV1 {
pub schema: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision_hint: Option<String>,
pub nodes: Vec<CodeNodeV1>,
pub edges: Vec<CodeEdgeV1>,
}
impl CodeGraphV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, CODE_GRAPH_SCHEMA)?;
if let Some(hint) = &self.revision_hint
&& !(matches!(hint.len(), 40 | 64)
&& hint
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)))
{
return Err(ValidationError::new("revision_hint is a git commit id"));
}
if self.nodes.len() > MAX_CODE_GRAPH_NODES || self.edges.len() > MAX_CODE_GRAPH_EDGES {
return Err(ValidationError::new(format!(
"a code graph has at most {MAX_CODE_GRAPH_NODES} nodes and {MAX_CODE_GRAPH_EDGES} edges"
)));
}
for (i, node) in self.nodes.iter().enumerate() {
node.validate()?;
if i > 0 && self.nodes[i - 1].id >= node.id {
return Err(ValidationError::new(
"nodes must be sorted by id without repeats",
));
}
}
let known = |id: &str| {
self.nodes
.binary_search_by(|node| node.id.as_str().cmp(id))
.is_ok()
};
for (i, edge) in self.edges.iter().enumerate() {
if edge.kind == CodeEdgeKind::Unknown {
return Err(ValidationError::new("unknown edge type"));
}
if !known(&edge.from) || !known(&edge.to) {
return Err(ValidationError::new("an edge names a missing node"));
}
if i > 0 {
let prev = &self.edges[i - 1];
if (&prev.from, &prev.to, prev.kind) >= (&edge.from, &edge.to, edge.kind) {
return Err(ValidationError::new("edges must be sorted without repeats"));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CodeGraphPutV1 {
pub schema: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph: Option<CodeGraphV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sealed: Option<SealedBlobV1>,
}
impl CodeGraphPutV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, CODE_GRAPH_PUT_SCHEMA)?;
match (&self.graph, &self.sealed) {
(Some(graph), None) => graph.validate(),
(None, Some(sealed)) => sealed.validate(),
_ => Err(ValidationError::new(
"a code graph upload carries exactly one of graph and sealed",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeGraphAckV1 {
pub schema: String,
pub nodes: u64,
pub edges: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision_hint: Option<String>,
}