use std::collections::HashSet;
use std::path::Path;
use chrono::{DateTime, Utc};
use crate::catalog::sanitize_component;
use crate::error::{AxiomError, Result};
use crate::models::{IndexRecord, MetadataFilter};
use crate::uri::{AxiomUri, Scope};
pub fn default_resource_target(path_or_url: &str) -> Result<AxiomUri> {
let base = if path_or_url.starts_with("http://") || path_or_url.starts_with("https://") {
let stripped = path_or_url
.trim_start_matches("https://")
.trim_start_matches("http://");
sanitize_component(stripped)
} else {
let path = Path::new(path_or_url);
let name = path
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| AxiomError::Validation("cannot infer target name".to_string()))?;
sanitize_component(name)
};
AxiomUri::root(Scope::Resources).join(&base)
}
pub fn classify_context(uri: &AxiomUri) -> String {
let uri_str = uri.to_string();
if uri_str.starts_with("axiom://agent/skills") {
return "skill".to_string();
}
if uri_str.starts_with("axiom://user/memories") || uri_str.starts_with("axiom://agent/memories")
{
return "memory".to_string();
}
if matches!(uri.scope(), Scope::Session) {
return "session".to_string();
}
"resource".to_string()
}
pub fn infer_tags(name: &str, content: &str) -> Vec<String> {
let mut tags = HashSet::new();
if has_extension(name, "rs") {
tags.insert("rust".to_string());
}
if has_extension(name, "md") {
tags.insert("markdown".to_string());
}
if has_extension(name, "json") {
tags.insert("json".to_string());
}
let lower_content = content.to_lowercase();
for token in ["auth", "oauth", "session", "memory", "skill", "api"] {
if lower_content.contains(token) {
tags.insert(token.to_string());
}
}
let mut out: Vec<_> = tags.into_iter().collect();
out.sort();
out
}
pub fn validate_filter(filter: Option<&MetadataFilter>) -> Result<()> {
let Some(filter) = filter else {
return Ok(());
};
let allowed = ["tags", "mime"];
for key in filter.fields.keys() {
if !allowed.contains(&key.as_str()) {
return Err(AxiomError::Validation(format!(
"unknown filter field: {key}"
)));
}
}
Ok(())
}
pub struct RecordInput<'a> {
pub uri: &'a AxiomUri,
pub parent_uri: Option<&'a AxiomUri>,
pub is_leaf: bool,
pub context_type: String,
pub name: String,
pub abstract_text: String,
pub content: String,
pub tags: Vec<String>,
pub updated_at: DateTime<Utc>,
}
pub fn build_record(input: RecordInput<'_>) -> IndexRecord {
IndexRecord {
id: uuid::Uuid::new_v4().to_string(),
uri: input.uri.to_string(),
parent_uri: input.parent_uri.map(ToString::to_string),
is_leaf: input.is_leaf,
context_type: input.context_type,
name: input.name,
abstract_text: input.abstract_text,
content: input.content,
tags: input.tags,
updated_at: input.updated_at,
depth: input.uri.segments().len(),
}
}
fn has_extension(name: &str, expected: &str) -> bool {
Path::new(name)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case(expected))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn validate_filter_rejects_unknown_fields() {
let mut fields = HashMap::new();
fields.insert("unknown".to_string(), serde_json::json!("x"));
let err = validate_filter(Some(&MetadataFilter { fields })).expect_err("must fail");
assert!(matches!(err, AxiomError::Validation(_)));
}
#[test]
fn classify_context_maps_memory_and_skill_paths() {
let memory = AxiomUri::parse("axiom://user/memories/preferences/rust.md").expect("parse");
let skill = AxiomUri::parse("axiom://agent/skills/retrieval.md").expect("parse");
let session = AxiomUri::parse("axiom://session/s1/messages").expect("parse");
let resource = AxiomUri::parse("axiom://resources/api/auth.md").expect("parse");
assert_eq!(classify_context(&memory), "memory");
assert_eq!(classify_context(&skill), "skill");
assert_eq!(classify_context(&session), "session");
assert_eq!(classify_context(&resource), "resource");
}
#[test]
fn infer_tags_extracts_extension_and_keyword_tags() {
let tags = infer_tags("auth_flow.rs", "OAuth API session memory");
assert!(tags.contains(&"rust".to_string()));
assert!(tags.contains(&"oauth".to_string()));
assert!(tags.contains(&"api".to_string()));
assert!(tags.contains(&"session".to_string()));
}
#[test]
fn default_resource_target_from_http_url_uses_sanitized_host_path() {
let uri =
default_resource_target("https://example.com/Awesome Path").expect("default target");
assert_eq!(uri.to_string(), "axiom://resources/example-comawesomepath");
}
#[test]
fn build_record_sets_depth_and_parent_uri() {
let uri = AxiomUri::parse("axiom://resources/demo/node.md").expect("parse");
let parent = AxiomUri::parse("axiom://resources/demo").expect("parse");
let updated_at = Utc::now();
let record = build_record(RecordInput {
uri: &uri,
parent_uri: Some(&parent),
is_leaf: true,
context_type: "resource".to_string(),
name: "node.md".to_string(),
abstract_text: "node".to_string(),
content: "content".to_string(),
tags: vec!["markdown".to_string()],
updated_at,
});
assert_eq!(record.parent_uri.as_deref(), Some("axiom://resources/demo"));
assert_eq!(record.depth, 2);
assert_eq!(record.updated_at, updated_at);
}
}