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]),
_ => SessionClaim::Ambiguous(candidates),
}
}
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_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
);
}
}