code-moniker-core 0.2.0

Core symbol-graph types and per-language extractors for code-moniker (pure Rust, no pgrx). Consumed by the CLI and the PostgreSQL extension.
Documentation
use crate::core::code_graph::CodeGraph;
use crate::core::moniker::Moniker;
use crate::core::uri::{UriConfig, UriError, from_uri};

mod build;
mod parse;
mod serialize;

pub use crate::lang::Lang;
pub use build::build_graph;
pub use parse::parse_spec;
pub use serialize::{SerializeError, graph_to_spec};

pub(crate) fn parse_moniker_uri(uri: &str) -> Result<Moniker, UriError> {
	let scheme_end = uri.find("://").ok_or(UriError::MissingProject)?;
	from_uri(
		uri,
		&UriConfig {
			scheme: &uri[..scheme_end + 3],
		},
	)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeclareSpec {
	pub root: Moniker,
	pub lang: Lang,
	pub symbols: Vec<DeclSymbol>,
	pub edges: Vec<DeclEdge>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeclSymbol {
	pub moniker: Moniker,
	pub kind: String,
	pub parent: Moniker,
	pub visibility: Option<String>,
	pub signature: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeclEdge {
	pub from: Moniker,
	pub kind: EdgeKind,
	pub to: Moniker,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum EdgeKind {
	DependsOn,
	Calls,
	InjectsProvide,
	InjectsRequire,
}

impl EdgeKind {
	pub fn from_tag(s: &str) -> Option<Self> {
		match s {
			"depends_on" => Some(Self::DependsOn),
			"calls" => Some(Self::Calls),
			"injects:provide" => Some(Self::InjectsProvide),
			"injects:require" => Some(Self::InjectsRequire),
			_ => None,
		}
	}

	pub fn tag(self) -> &'static str {
		match self {
			Self::DependsOn => "depends_on",
			Self::Calls => "calls",
			Self::InjectsProvide => "injects:provide",
			Self::InjectsRequire => "injects:require",
		}
	}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DeclareError {
	JsonParse(String),
	NotAnObject(&'static str),
	MissingField {
		path: String,
		field: &'static str,
	},
	InvalidType {
		path: String,
		expected: &'static str,
	},
	UnknownLang(String),
	UnknownEdgeKind(String),
	InvalidMoniker {
		path: String,
		value: String,
		reason: String,
	},
	KindNotInProfile {
		lang: &'static str,
		kind: String,
	},
	VisibilityNotInProfile {
		lang: &'static str,
		visibility: String,
	},
	KindMismatchMoniker {
		path: String,
		declared_kind: String,
		moniker_last_kind: String,
	},
	DuplicateMoniker {
		moniker: String,
	},
	UnknownParent {
		path: String,
		parent: String,
	},
	UnknownEdgeSource {
		path: String,
		from: String,
	},
	LangMismatch {
		expected: &'static str,
		actual: String,
	},
	GraphError(String),
}

impl std::fmt::Display for DeclareError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		use DeclareError::*;
		match self {
			JsonParse(msg) => write!(f, "spec is not valid JSON: {msg}"),
			NotAnObject(what) => write!(f, "{what} must be a JSON object"),
			MissingField { path, field } => {
				write!(f, "{path}: required field `{field}` is missing")
			}
			InvalidType { path, expected } => write!(f, "{path}: expected {expected}"),
			UnknownLang(s) => write!(
				f,
				"unknown lang `{s}` (expected ts | rs | java | python | go | cs | sql)"
			),
			UnknownEdgeKind(s) => write!(
				f,
				"unknown edge kind `{s}` (expected depends_on | calls | injects:provide | injects:require)"
			),
			InvalidMoniker {
				path,
				value,
				reason,
			} => write!(f, "{path}: invalid moniker URI `{value}`: {reason}"),
			KindNotInProfile { lang, kind } => write!(
				f,
				"kind `{kind}` is not allowed for lang={lang} (see profile)"
			),
			VisibilityNotInProfile { lang, visibility } => write!(
				f,
				"visibility `{visibility}` is not allowed for lang={lang} (see profile)"
			),
			KindMismatchMoniker {
				path,
				declared_kind,
				moniker_last_kind,
			} => write!(
				f,
				"{path}: declared kind `{declared_kind}` does not match the moniker's last segment kind `{moniker_last_kind}`"
			),
			DuplicateMoniker { moniker } => {
				write!(f, "duplicate moniker in symbols: {moniker}")
			}
			UnknownParent { path, parent } => write!(
				f,
				"{path}: parent `{parent}` is neither the root nor a previously declared symbol"
			),
			UnknownEdgeSource { path, from } => {
				write!(f, "{path}: edge `from` `{from}` is not a declared symbol")
			}
			LangMismatch { expected, actual } => write!(
				f,
				"spec.lang `{actual}` does not match the typed extractor's `{expected}` (use the dynamic-dispatch entry point if you do not know the language ahead of time)"
			),
			GraphError(msg) => write!(f, "graph build error: {msg}"),
		}
	}
}

impl std::error::Error for DeclareError {}

pub fn declare_from_json_str(json: &str) -> Result<CodeGraph, DeclareError> {
	let value: serde_json::Value =
		serde_json::from_str(json).map_err(|e| DeclareError::JsonParse(e.to_string()))?;
	declare_from_json_value(&value)
}

pub fn declare_from_json_value(json: &serde_json::Value) -> Result<CodeGraph, DeclareError> {
	let spec = parse_spec(json)?;
	build_graph(&spec)
}