use crate::core::backend::GeneratedFile;
use crate::core::config::e2e::{E2eConfig, SnippetConfig};
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::{EnumDef, TypeDef};
use crate::e2e::codegen::{E2eCodegen, all_generators};
use crate::e2e::fixture::{Fixture, FixtureDocs, SideEffectClass};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Component, Path, PathBuf};
pub mod coverage;
pub(crate) mod ledger_paths;
pub mod migration;
mod mock_harness_guard;
mod mock_url_defaults;
pub mod ownership;
mod recipe_policy;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnippetInclusion {
Include,
Exclude { missing_requirements: Vec<String> },
}
#[derive(Debug, Clone)]
pub struct GeneratedSnippet {
pub file: GeneratedFile,
pub fixture_id: String,
pub fixture_source: String,
pub language: String,
pub requirements: Vec<String>,
pub side_effects: SideEffectClass,
}
pub const COVERAGE_MANIFEST: &str = ".alef-snippet-coverage.json";
pub const COVERAGE_MANIFEST_VERSION: u32 = 2;
pub fn is_snippet_coverage_manifest_path(path: &Path) -> bool {
path.file_name().and_then(|name| name.to_str()) == Some(COVERAGE_MANIFEST)
}
pub const CRATE_REQUIREMENT_PREFIX: &str = "crate:";
const SERDE_JSON_REQUIREMENT: &str = "crate:serde_json";
const TOKIO_REQUIREMENT: &str = "crate:tokio";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SnippetCoverageKey {
pub fixture_id: String,
pub language: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedSnippetMetadata {
pub key: SnippetCoverageKey,
pub path: PathBuf,
pub language: String,
pub target: String,
pub session: String,
pub requires: Vec<String>,
pub side_effect: SideEffectClass,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MissingSnippet {
pub key: SnippetCoverageKey,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentedSnippetException {
pub key: SnippetCoverageKey,
pub reason: String,
pub reference: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnippetCoverageLedger {
#[serde(default)]
pub format_version: u32,
#[serde(default)]
pub generated_paths: Vec<PathBuf>,
#[serde(default)]
pub generated_metadata: Vec<GeneratedSnippetMetadata>,
pub expected: Vec<SnippetCoverageKey>,
pub generated: Vec<SnippetCoverageKey>,
pub missing: Vec<MissingSnippet>,
pub documented_exceptions: Vec<DocumentedSnippetException>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnippetGuardRejection {
pub key: SnippetCoverageKey,
pub marker: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MockHarnessLeak {
pub marker: String,
pub fixture_id: String,
pub language: String,
}
impl std::fmt::Display for MockHarnessLeak {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"`{}` snippet for fixture `{}` leaks e2e mock-server scaffolding (`{}`); \
a documentation snippet must construct its client the way a reader would",
self.language, self.fixture_id, self.marker
)
}
}
impl std::error::Error for MockHarnessLeak {}
#[derive(Debug, Clone, Default)]
pub struct SnippetGenerationReport {
pub snippets: Vec<GeneratedSnippet>,
pub coverage: SnippetCoverageLedger,
pub guard_rejections: Vec<SnippetGuardRejection>,
}
struct SnippetRenderContext<'a> {
e2e: &'a E2eConfig,
crate_config: &'a ResolvedCrateConfig,
type_defs: &'a [TypeDef],
enums: &'a [EnumDef],
functions: &'a [crate::core::ir::FunctionDef],
errors: &'a [crate::core::ir::ErrorDef],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DocumentationLanguage {
Binding(Language),
Shell,
}
impl DocumentationLanguage {
fn code_fence(self) -> &'static str {
match self {
Self::Binding(language) => crate::docs::naming::lang_code_fence(language),
Self::Shell => "bash",
}
}
fn canonical_name(self) -> &'static str {
self.code_fence()
}
fn display_name(self) -> &'static str {
match self {
Self::Binding(language) => crate::docs::naming::lang_display_name(language),
Self::Shell => "Shell",
}
}
}
#[expect(clippy::too_many_arguments, reason = "preserves the public snippet generation API")]
pub fn generate_snippets(
fixtures: &[Fixture],
languages: &[String],
e2e: &E2eConfig,
snippets: &SnippetConfig,
crate_config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[crate::core::ir::FunctionDef],
) -> Result<Vec<GeneratedFile>> {
Ok(generate_snippet_report(
fixtures,
languages,
e2e,
snippets,
crate_config,
type_defs,
enums,
functions,
&[],
)?
.snippets
.into_iter()
.map(|snippet| snippet.file)
.collect())
}
#[expect(clippy::too_many_arguments, reason = "preserves the public snippet generation API")]
pub fn generate_snippet_report(
fixtures: &[Fixture],
languages: &[String],
e2e: &E2eConfig,
snippets: &SnippetConfig,
crate_config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<SnippetGenerationReport> {
crate::with_extensions(|extensions| {
let context = SnippetRenderContext {
e2e,
crate_config,
type_defs,
enums,
functions,
errors,
};
generate_snippet_report_with_extensions(fixtures, languages, snippets, &context, extensions)
})
}
fn generate_snippet_report_with_extensions(
fixtures: &[Fixture],
languages: &[String],
snippets: &SnippetConfig,
context: &SnippetRenderContext<'_>,
extensions: &[Box<dyn crate::Extension>],
) -> Result<SnippetGenerationReport> {
validate_relative_path(Path::new(&snippets.output), "snippet output")?;
ownership::snapshot_pre_run_ledger(Path::new(&snippets.output));
let generators = snippet_generators(languages)?;
let mut generated = BTreeMap::<PathBuf, GeneratedSnippet>::new();
let mut guard_rejections = Vec::<SnippetGuardRejection>::new();
let mut coverage = SnippetCoverageLedger {
format_version: COVERAGE_MANIFEST_VERSION,
..SnippetCoverageLedger::default()
};
for fixture in fixtures {
validate_requirements(fixture)?;
validate_coverage_exceptions(fixture)?;
validate_docs_paths(fixture, languages)?;
for (language, generator) in &generators {
if crate::e2e::codegen::call_skip_reason(fixture, language, context.e2e).is_some()
|| function_excluded_for_language(fixture, language, generator.language_name(), context)
|| visitor_excluded_for_language(fixture, generator.language_name(), context)
{
continue;
}
let key = SnippetCoverageKey {
fixture_id: fixture.id.clone(),
language: language.to_string(),
};
coverage.expected.push(key.clone());
let Some(docs) = fixture.docs.as_ref() else {
coverage.missing.push(MissingSnippet {
key,
reason: "fixture has no documentation metadata".to_string(),
});
continue;
};
let capabilities = capabilities(language, snippets, context.crate_config);
let capability_decision = snippet_inclusion(fixture, &capabilities);
let exclusion_reason = match &capability_decision {
SnippetInclusion::Exclude { missing_requirements } => {
Some(format!("missing requirements: {}", missing_requirements.join(", ")))
}
SnippetInclusion::Include => None,
};
if let Some(reason) = exclusion_reason {
if let Some(exception) = coverage_exception(docs, language) {
coverage.documented_exceptions.push(DocumentedSnippetException {
key,
reason: exception.reason.clone(),
reference: exception.documentation.clone(),
});
} else {
coverage.missing.push(MissingSnippet { key, reason });
}
continue;
}
let lang = parse_language(generator.language_name()).ok_or_else(|| {
anyhow::anyhow!(
"e2e code generator `{}` has no documentation language mapping",
generator.language_name()
)
})?;
let path = snippet_path(&snippets.output, docs, &fixture.id, language, lang)?;
let body = match render_snippet_body(extensions, generator.as_ref(), fixture, language, context) {
Ok(body) => body,
Err(error) => {
if let Some(leak) = error.downcast_ref::<MockHarnessLeak>() {
guard_rejections.push(SnippetGuardRejection {
key: key.clone(),
marker: leak.marker.clone(),
});
coverage.missing.push(MissingSnippet {
key,
reason: format!("{error:#}"),
});
continue;
}
if let Some(exception) = coverage_exception(docs, language) {
coverage.documented_exceptions.push(DocumentedSnippetException {
key,
reason: exception.reason.clone(),
reference: exception.documentation.clone(),
});
} else {
coverage.missing.push(MissingSnippet {
key,
reason: format!("{error:#}"),
});
}
continue;
}
};
let content = render_snippet_markdown(&body, fixture, docs, language, lang);
let requirements = snippet_requirements(fixture, language, &body);
let file = GeneratedFile {
path: path.clone(),
content,
generated_header: false,
};
let snippet = GeneratedSnippet {
file,
fixture_id: fixture.id.clone(),
fixture_source: fixture.source.clone(),
language: language.to_string(),
requirements: requirements.clone(),
side_effects: docs.side_effects,
};
if generated.insert(path.clone(), snippet).is_some() {
bail!("snippet output collision at {}", path.display());
}
let relative_path = path
.strip_prefix(&snippets.output)
.context("generated snippet path escaped the configured output root")?
.to_path_buf();
coverage.generated_paths.push(relative_path.clone());
coverage.generated_metadata.push(GeneratedSnippetMetadata {
key: key.clone(),
path: relative_path,
language: lang.canonical_name().to_string(),
target: language.to_string(),
session: language.to_string(),
requires: requirements,
side_effect: docs.side_effects,
});
coverage.generated.push(key);
}
}
guard_rejections.sort_by(|left, right| left.key.cmp(&right.key));
ensure_no_guard_rejections(&guard_rejections)?;
coverage = coverage::normalize(coverage);
coverage::validate(&coverage)?;
Ok(SnippetGenerationReport {
snippets: generated.into_values().collect(),
coverage,
guard_rejections,
})
}
fn coverage_exception<'a>(
docs: &'a FixtureDocs,
language: &str,
) -> Option<&'a crate::e2e::fixture::SnippetCoverageException> {
let canonical = crate::e2e::fixture::canonical_language(language);
docs.coverage_exceptions
.iter()
.find(|(key, _)| crate::e2e::fixture::canonical_language(key) == canonical)
.map(|(_, exception)| exception)
}
fn known_language_names() -> BTreeSet<String> {
crate::e2e::known_e2e_target_names().into_iter().collect()
}
fn describe_known_languages(known: &BTreeSet<String>) -> String {
known
.iter()
.map(|canonical| {
let aliases = crate::e2e::fixture::language_alias_groups()
.iter()
.find(|(name, _)| *name == canonical.as_str())
.map(|(_, aliases)| *aliases)
.unwrap_or_default();
if aliases.is_empty() {
canonical.clone()
} else {
format!("{canonical} (also accepted: {})", aliases.join(", "))
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn validate_coverage_exceptions(fixture: &Fixture) -> Result<()> {
let Some(docs) = &fixture.docs else {
return Ok(());
};
let known = known_language_names();
for (language, exception) in &docs.coverage_exceptions {
if language.trim().is_empty() || exception.reason.trim().is_empty() {
bail!(
"fixture `{}` has invalid coverage exception for language `{language}`: language and reason must be non-empty",
fixture.id
);
}
if !known.contains(crate::e2e::fixture::canonical_language(language)) {
bail!(
"fixture `{}` has a coverage exception for unknown language `{language}`; accepted language spellings: {}",
fixture.id,
describe_known_languages(&known)
);
}
validate_documentation_reference(&exception.documentation).map_err(|error| {
anyhow::anyhow!(
"fixture `{}` has invalid coverage exception documentation for language `{language}`: {error}",
fixture.id
)
})?;
}
Ok(())
}
fn validate_documentation_reference(reference: &str) -> Result<()> {
if reference.trim() != reference || reference.is_empty() {
bail!("reference must be non-empty and have no surrounding whitespace");
}
if reference.starts_with("https://") || reference.starts_with("http://") {
if reference.chars().any(char::is_whitespace) {
bail!("URL reference must not contain whitespace");
}
return Ok(());
}
validate_relative_path(Path::new(reference), "documentation reference")
}
fn render_snippet_body(
extensions: &[Box<dyn crate::Extension>],
generator: &dyn E2eCodegen,
fixture: &Fixture,
language: &str,
context: &SnippetRenderContext<'_>,
) -> Result<String> {
let docs_fixture = fixture.docs_call_fixture();
for extension in extensions {
if let Some(body) = extension
.render_e2e_snippet(
&docs_fixture,
context.e2e,
context.crate_config,
language,
context.type_defs,
context.enums,
)
.map_err(|error| anyhow::anyhow!("extension `{}` could not render snippet: {error:#}", extension.name()))?
{
if body.trim().is_empty() {
bail!("extension `{}` returned an empty snippet body", extension.name());
}
mock_harness_guard::reject_mock_harness_scaffolding(&body, &docs_fixture, language)?;
return Ok(body);
}
}
let call = context.e2e.resolve_call_for_fixture(
docs_fixture.call.as_deref(),
&docs_fixture.id,
&docs_fixture.resolved_category(),
&docs_fixture.tags,
&docs_fixture.input,
);
let docs_fixture = mock_url_defaults::with_default_mock_url_literals(docs_fixture, call);
let fixture = &docs_fixture;
if let Some(kind) = recipe_policy::extension_owned_recipe_kind(fixture, fixture.resolved_args(call)) {
bail!("{kind} fixture requires an extension-owned documentation recipe");
}
let effective_function = call
.effective_function(language)
.or_else(|| {
let skipped_for_language = fixture.skip.as_ref().is_some_and(|skip| skip.should_skip(language));
(!skipped_for_language && crate::e2e::fixture::canonical_language(language) == "c")
.then(|| crate::e2e::codegen::recipe::trait_bridge_function_identity(context.crate_config, fixture))
.flatten()
})
.unwrap_or_default();
if effective_function.trim().is_empty() {
bail!(
"built-in `{language}` snippet recipe has no function identity; configure a call function or provide an extension-owned documentation recipe"
);
}
let body = generator
.render_snippet_body_with_functions(
fixture,
context.e2e,
context.crate_config,
context.type_defs,
context.enums,
context.functions,
context.errors,
)
.map_err(|error| anyhow::anyhow!("built-in `{language}` snippet recipe is incompatible: {error:#}"))?;
if body.trim().is_empty() {
bail!("built-in `{language}` snippet recipe returned an empty body");
}
mock_harness_guard::reject_mock_harness_scaffolding(&body, fixture, language)?;
Ok(body)
}
fn ensure_no_guard_rejections(rejections: &[SnippetGuardRejection]) -> Result<()> {
if rejections.is_empty() {
return Ok(());
}
let mut by_language: BTreeMap<&str, BTreeMap<&str, Vec<&str>>> = BTreeMap::new();
for rejection in rejections {
by_language
.entry(rejection.key.language.as_str())
.or_default()
.entry(rejection.marker.as_str())
.or_default()
.push(rejection.key.fixture_id.as_str());
}
let mut detail = String::new();
for (language, markers) in &by_language {
let language_total: usize = markers.values().map(Vec::len).sum();
detail.push_str(&format!("\n {language} ({language_total}):"));
for (marker, fixtures) in markers {
detail.push_str(&format!(
"\n `{marker}` ({}): {}",
fixtures.len(),
fixtures.join(", ")
));
}
}
bail!(
"{} documentation snippet(s) were rejected by the mock-harness guard; each would otherwise \
disappear from the docs tree with no report. Fix the generator so the snippet constructs \
its client the way a reader would — a `docs.coverage_exceptions` entry cannot retire a \
guard rejection.{detail}",
rejections.len()
)
}
fn snippet_generators(languages: &[String]) -> Result<Vec<(&str, Box<dyn E2eCodegen>)>> {
let mut available = BTreeMap::new();
for generator in all_generators() {
let name = generator.language_name();
if available.insert(name, generator).is_some() {
bail!("duplicate e2e code generator registered for snippet language `{name}`");
}
}
let mut requested = BTreeSet::new();
languages
.iter()
.map(|language| {
let generator_name = generator_name(language);
if !requested.insert(generator_name) {
bail!("duplicate snippet language resolves to e2e code generator `{generator_name}`");
}
available
.remove(generator_name)
.map(|generator| (language.as_str(), generator))
.ok_or_else(|| anyhow::anyhow!("no e2e code generator registered for snippet language `{language}`"))
})
.collect()
}
fn generator_name(language: &str) -> &str {
crate::e2e::fixture::canonical_language(language)
}
const SNIPPET_REGENERATE_COMMAND: &str = "alef e2e generate";
fn render_snippet_markdown(
body: &str,
fixture: &Fixture,
docs: &FixtureDocs,
target: &str,
language: DocumentationLanguage,
) -> String {
let snippet_id = format!("fixture_{target}_{}", fixture.id);
let requirements = snippet_requirements(fixture, target, body);
let requires = serde_json::to_string(&requirements).unwrap_or_else(|_| "[]".to_string());
let rendered = crate::e2e::template_env::render(
"snippets/file.md.jinja",
minijinja::context! {
description => docs.description.as_deref().unwrap_or(&fixture.description),
fence => language.code_fence(),
id => snippet_id,
language => language.canonical_name(),
level => level_stamp(docs.side_effects),
requires => requires,
side_effect => side_effect_name(docs.side_effects),
target => target,
title => language.display_name(),
body => body.trim_end(),
},
);
crate::docs::with_html_header(rendered, SNIPPET_REGENERATE_COMMAND)
}
fn snippet_requirements(fixture: &Fixture, target: &str, body: &str) -> Vec<String> {
let mut requirements = fixture.requirements.clone();
if target == "rust" && fixture.visitor.is_some() && !requirements.iter().any(|value| value == "feature:visitor") {
requirements.push("feature:visitor".to_string());
}
if generator_name(target) == "rust"
&& body.contains("serde_json::")
&& !requirements.iter().any(|value| value == SERDE_JSON_REQUIREMENT)
{
requirements.push(SERDE_JSON_REQUIREMENT.to_string());
}
if generator_name(target) == "rust"
&& body.contains("#[tokio::main]")
&& !requirements.iter().any(|value| value == TOKIO_REQUIREMENT)
{
requirements.push(TOKIO_REQUIREMENT.to_string());
}
requirements
}
fn level_stamp(side_effects: SideEffectClass) -> Option<&'static str> {
if side_effects == SideEffectClass::Safe {
None
} else {
Some("typecheck")
}
}
fn side_effect_name(side_effect: SideEffectClass) -> &'static str {
match side_effect {
SideEffectClass::Safe => "safe",
SideEffectClass::Network => "network",
SideEffectClass::Process => "process",
SideEffectClass::Install => "install",
SideEffectClass::Server => "server",
}
}
fn validate_requirements(fixture: &Fixture) -> Result<()> {
for requirement in &fixture.requirements {
let valid = requirement.split_once(':').is_some_and(|(kind, value)| {
matches!(kind, "feature" | "model" | "service" | "credential") && !value.is_empty() && !value.contains(':')
});
if !valid {
bail!("fixture `{}` has invalid requirement token `{requirement}`", fixture.id);
}
}
Ok(())
}
pub fn snippet_inclusion(fixture: &Fixture, capabilities: &BTreeSet<String>) -> SnippetInclusion {
let missing_requirements: Vec<_> = fixture
.requirements
.iter()
.filter(|requirement| !capabilities.contains(*requirement))
.cloned()
.collect();
if missing_requirements.is_empty() {
SnippetInclusion::Include
} else {
SnippetInclusion::Exclude { missing_requirements }
}
}
fn capabilities(language: &str, snippets: &SnippetConfig, crate_config: &ResolvedCrateConfig) -> BTreeSet<String> {
let mut values = snippets.capabilities.for_language(language);
values.extend(crate_config.features.iter().map(|feature| format!("feature:{feature}")));
values
}
fn function_excluded_for_language(
fixture: &Fixture,
language: &str,
generator_language_name: &str,
context: &SnippetRenderContext<'_>,
) -> bool {
let Some(DocumentationLanguage::Binding(lang)) = parse_language(generator_language_name) else {
return false;
};
let docs_fixture = fixture.docs_call_fixture();
let call = context.e2e.resolve_call_for_fixture(
docs_fixture.call.as_deref(),
&docs_fixture.id,
&docs_fixture.resolved_category(),
&docs_fixture.tags,
&docs_fixture.input,
);
let Some(function_name) = call.core_lookup_name(language) else {
return false;
};
let (excluded_functions, _) = crate::docs::language_pages::excludes::language_excludes(context.crate_config, lang);
excluded_functions.contains(function_name.as_ref())
}
fn visitor_excluded_for_language(
fixture: &Fixture,
generator_language_name: &str,
context: &SnippetRenderContext<'_>,
) -> bool {
if fixture.visitor.is_none() {
return false;
}
let Some(DocumentationLanguage::Binding(lang)) = parse_language(generator_language_name) else {
return false;
};
let (excluded_functions, _) = crate::docs::language_pages::excludes::language_excludes(context.crate_config, lang);
excluded_functions.contains(crate::e2e::fixture::VISITOR_EXCLUDE_FUNCTION_NAME)
}
fn snippet_path(
output: &str,
docs: &FixtureDocs,
fixture_id: &str,
target_language: &str,
language: DocumentationLanguage,
) -> Result<PathBuf> {
if let Some(relative) = docs.paths.get(target_language) {
let relative = Path::new(relative);
validate_relative_path(relative, "fixture docs target path")?;
if relative.extension().and_then(|value| value.to_str()) != Some("md") {
bail!("fixture docs target path must end in .md: {}", relative.display());
}
return Ok(Path::new(output)
.join(snippet_output_slug(target_language, language))
.join(relative));
}
validate_component(&docs.topic, "snippet topic")?;
let stem = docs.stem.as_deref().unwrap_or(fixture_id);
validate_component(stem, "snippet stem")?;
Ok(Path::new(output)
.join(snippet_output_slug(target_language, language))
.join(&docs.topic)
.join(format!("{stem}.md")))
}
fn snippet_output_slug(target_language: &str, language: DocumentationLanguage) -> &'static str {
match target_language {
"node" => "typescript",
"wasm" => "wasm",
"kotlin_android" => "kotlin-android",
"brew" => "brew",
"homebrew" => "homebrew",
_ => language.canonical_name(),
}
}
fn validate_component(value: &str, label: &str) -> Result<()> {
if value.is_empty() || Path::new(value).components().count() != 1 || matches!(value, "." | "..") {
bail!("unsafe {label} `{value}`");
}
Ok(())
}
fn validate_relative_path(path: &Path, label: &str) -> Result<()> {
if path.as_os_str().is_empty()
|| path.is_absolute()
|| path.components().any(|part| !matches!(part, Component::Normal(_)))
{
bail!("{label} must be a safe relative path: {}", path.display());
}
Ok(())
}
fn validate_docs_paths(fixture: &Fixture, languages: &[String]) -> Result<()> {
let Some(docs) = &fixture.docs else {
return Ok(());
};
for target in docs.paths.keys() {
if !languages.iter().any(|language| language == target) {
bail!(
"fixture `{}` docs path targets unconfigured language `{target}`",
fixture.id
);
}
}
Ok(())
}
fn parse_language(value: &str) -> Option<DocumentationLanguage> {
let language = match crate::e2e::fixture::canonical_language(value) {
"python" => Language::Python,
"node" => Language::Node,
"wasm" => Language::Wasm,
"ruby" => Language::Ruby,
"php" | "php_ext" => Language::Php,
"elixir" => Language::Elixir,
"go" => Language::Go,
"java" => Language::Java,
"csharp" => Language::Csharp,
"r" => Language::R,
"rust" => Language::Rust,
"kotlin" => Language::Kotlin,
"kotlin_android" => Language::KotlinAndroid,
"swift" => Language::Swift,
"dart" => Language::Dart,
"gleam" => Language::Gleam,
"zig" => Language::Zig,
"c" => Language::C,
"brew" | "homebrew" => return Some(DocumentationLanguage::Shell),
_ => return None,
};
Some(DocumentationLanguage::Binding(language))
}
#[cfg(test)]
mod tests;