use crate::snippets::types::{Language, Snippet};
use std::collections::HashMap;
use std::path::Path;
pub(super) trait SessionIdentity {
fn working_directory(&self) -> &Path;
}
impl SessionIdentity for crate::snippets::session::ValidationSession {
fn working_directory(&self) -> &Path {
&self.working_directory
}
}
impl SessionIdentity for crate::snippets::session::SessionSpec {
fn working_directory(&self) -> &Path {
&self.working_directory
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum SessionClaim<'a> {
Unclaimed,
Claimed(&'a str),
Ambiguous(Vec<&'a str>),
}
pub(super) fn resolve_session_claim<'a, T: SessionIdentity>(
snippet: &Snippet,
entries: &'a HashMap<String, T>,
language_of: impl Fn(&T) -> Language,
) -> SessionClaim<'a> {
if let Some(target) = snippet.metadata.target.as_deref() {
let normalized = Language::normalize_session_target(target);
return match entries.get_key_value(normalized.as_str()) {
Some((key, _)) => SessionClaim::Claimed(key.as_str()),
None => SessionClaim::Unclaimed,
};
}
let mut candidates: Vec<&str> = entries
.iter()
.filter(|(_, value)| language_of(value) == snippet.language)
.map(|(key, _)| key.as_str())
.collect();
candidates.sort_unstable();
match candidates.as_slice() {
[] => SessionClaim::Unclaimed,
[only] => SessionClaim::Claimed(only),
_ if distinct_working_directory_count(entries, &candidates) <= 1 => SessionClaim::Claimed(candidates[0]),
_ => match alias_default_claim(entries, &candidates, snippet.language) {
Some(key) => SessionClaim::Claimed(key),
None => SessionClaim::Ambiguous(candidates),
},
}
}
fn alias_default_claim<'a, T: SessionIdentity>(
entries: &'a HashMap<String, T>,
candidates: &[&'a str],
language: Language,
) -> Option<&'a str> {
let exact_name = language.to_string();
let exact_key = candidates.iter().copied().find(|&key| key == exact_name.as_str())?;
let exact_directory = entries.get(exact_key)?.working_directory();
let is_redundant_alias = candidates.iter().any(|&key| {
key != exact_key
&& entries
.get(key)
.is_some_and(|value| value.working_directory() == exact_directory)
});
is_redundant_alias.then_some(exact_key)
}
fn distinct_working_directory_count<'a, T: SessionIdentity>(
entries: &'a HashMap<String, T>,
candidates: &[&str],
) -> usize {
let mut directories: Vec<&'a Path> = candidates
.iter()
.filter_map(|key| entries.get(*key))
.map(SessionIdentity::working_directory)
.collect();
directories.sort_unstable();
directories.dedup();
directories.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::session::ValidationSession;
use crate::snippets::types::{SnippetMetadata, SourceOrigin};
use std::path::PathBuf;
fn snippet(language: Language, target: Option<&str>) -> Snippet {
Snippet {
id: None,
path: PathBuf::from("example.md"),
language,
title: None,
code: String::new(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata {
target: target.map(str::to_string),
..SnippetMetadata::default()
},
source_origin: SourceOrigin {
path: PathBuf::from("example.md"),
line: 1,
block_index: 0,
},
}
}
fn session(language: Language, working_directory: &str) -> ValidationSession {
ValidationSession {
language,
working_directory: PathBuf::from(working_directory),
manifest: None,
fingerprint: "fingerprint".into(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
}
}
#[test]
fn a_single_same_language_session_claims_a_target_less_snippet_regardless_of_its_name() {
let sessions = HashMap::from([("node".to_string(), session(Language::TypeScript, "packages/node"))]);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Claimed("node")
);
}
#[test]
fn two_same_language_sessions_over_different_directories_are_ambiguous() {
let sessions = HashMap::from([
(
"typescript".to_string(),
session(Language::TypeScript, "packages/typescript"),
),
("wasm".to_string(), session(Language::TypeScript, "packages/wasm")),
]);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Ambiguous(vec!["typescript", "wasm"])
);
}
#[test]
fn kotlin_and_kotlin_android_over_one_directory_collapse_to_one_session() {
let sessions = HashMap::from([
("kotlin".to_string(), session(Language::Kotlin, "packages/kotlin")),
(
"kotlin_android".to_string(),
session(Language::Kotlin, "packages/kotlin"),
),
]);
let mut candidates: Vec<&str> = sessions.keys().map(String::as_str).collect();
candidates.sort_unstable();
assert_eq!(
distinct_working_directory_count(&sessions, &candidates),
1,
"kotlin and kotlin_android must resolve to a single physical session, not two"
);
let snippet = snippet(Language::Kotlin, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Claimed("kotlin")
);
}
#[test]
fn typescript_node_and_wasm_over_one_package_collapse_to_one_session() {
let sessions = HashMap::from([
(
"typescript".to_string(),
session(Language::TypeScript, "packages/typescript"),
),
("node".to_string(), session(Language::TypeScript, "packages/typescript")),
("wasm".to_string(), session(Language::TypeScript, "packages/typescript")),
]);
let mut candidates: Vec<&str> = sessions.keys().map(String::as_str).collect();
candidates.sort_unstable();
assert_eq!(
distinct_working_directory_count(&sessions, &candidates),
1,
"typescript, node, and wasm must resolve to a single physical session, not three"
);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Claimed("node")
);
}
#[test]
fn an_alias_named_exactly_like_the_language_wins_over_a_real_second_package() {
let sessions = HashMap::from([
("node".to_string(), session(Language::TypeScript, "packages/node")),
("typescript".to_string(), session(Language::TypeScript, "packages/node")),
("wasm".to_string(), session(Language::TypeScript, "packages/wasm")),
]);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Claimed("typescript")
);
}
#[test]
fn three_same_language_sessions_with_no_exact_language_name_stay_ambiguous() {
let sessions = HashMap::from([
("node".to_string(), session(Language::TypeScript, "packages/node")),
("electron".to_string(), session(Language::TypeScript, "packages/node")),
("wasm".to_string(), session(Language::TypeScript, "packages/wasm")),
]);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Ambiguous(vec!["electron", "node", "wasm"])
);
}
#[test]
fn an_explicit_target_resolves_an_otherwise_ambiguous_language() {
let sessions = HashMap::from([
(
"typescript".to_string(),
session(Language::TypeScript, "packages/typescript"),
),
("wasm".to_string(), session(Language::TypeScript, "packages/wasm")),
]);
let snippet = snippet(Language::TypeScript, Some("wasm"));
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Claimed("wasm")
);
}
#[test]
fn an_explicit_target_naming_no_session_does_not_fall_back_to_a_language_match() {
let sessions = HashMap::from([("node".to_string(), session(Language::TypeScript, "packages/node"))]);
let snippet = snippet(Language::TypeScript, Some("wasm"));
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Unclaimed
);
}
#[test]
fn no_configured_session_for_the_language_is_unclaimed() {
let sessions = HashMap::from([("python".to_string(), session(Language::Python, "packages/python"))]);
let snippet = snippet(Language::TypeScript, None);
assert_eq!(
resolve_session_claim(&snippet, &sessions, |session| session.language),
SessionClaim::Unclaimed
);
}
}