use super::{Fixture, SnippetConfig};
use crate::core::config::e2e::{
DEFAULT_DOCS_SAMPLE_BASE_URL, DOCS_SAMPLE_URL_FIXTURE_KEY, DocsSampleBaseUrl, SAMPLE_BASE_URL_CONFIG_KEY,
SAMPLE_URL_MANIFEST_CONFIG_KEY, SAMPLE_URL_MOCK_ONLY_CONFIG_KEY, SAMPLE_URL_TEMPLATE_CONFIG_KEY, SampleUrlManifest,
SampleUrlTemplate,
};
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PlaceholderClass {
Unconfigured,
Unresolved,
}
#[derive(Debug)]
pub(super) struct SampleUrlPolicy<'a> {
base: DocsSampleBaseUrl<'a>,
template: Option<SampleUrlTemplate>,
manifest: Option<SampleUrlManifest>,
mock_only: bool,
}
impl<'a> SampleUrlPolicy<'a> {
pub(super) fn resolve(snippets: &'a SnippetConfig, project_root: &Path) -> Result<Self> {
if snippets.mock_only {
reject_conflicting_public_address(snippets)?;
}
let base = snippets
.docs_sample_base_url()
.map_err(|error| anyhow::anyhow!("invalid documentation sample base URL: {error}"))?;
let template = snippets
.sample_url_template()
.map_err(|error| anyhow::anyhow!("invalid documentation sample URL template: {error}"))?;
let manifest = snippets
.sample_url_manifest(project_root)
.map_err(|error| anyhow::anyhow!("invalid documentation sample URL manifest: {error}"))?;
Ok(Self {
base,
template,
manifest,
mock_only: snippets.mock_only,
})
}
pub(super) fn base(&self) -> DocsSampleBaseUrl<'a> {
self.base
}
pub(super) fn for_fixture<'f>(&'f self, fixture: &'f Fixture) -> Result<FixtureSampleUrl<'f>> {
let declared = fixture.docs.as_ref().and_then(|docs| docs.sample_url.as_deref());
let (base, source) = match declared {
Some(value) => {
let base =
DocsSampleBaseUrl::resolve_at(Some(value), DOCS_SAMPLE_URL_FIXTURE_KEY).map_err(|error| {
anyhow::anyhow!("fixture `{}` declares an unusable sample URL: {error}", fixture.id)
})?;
(base, SampleUrlSource::Declared)
}
None if self.mock_only => (self.base, SampleUrlSource::InheritedMockOnly),
None => (self.base, SampleUrlSource::InheritedCorpus),
};
Ok(FixtureSampleUrl {
base,
template: self.template.as_ref(),
manifest: self.manifest.as_ref(),
source,
})
}
}
fn reject_conflicting_public_address(snippets: &SnippetConfig) -> Result<()> {
let configured = [
(SAMPLE_BASE_URL_CONFIG_KEY, snippets.sample_base_url.is_some()),
(SAMPLE_URL_TEMPLATE_CONFIG_KEY, snippets.sample_url_template.is_some()),
(SAMPLE_URL_MANIFEST_CONFIG_KEY, snippets.sample_url_manifest.is_some()),
];
let conflicting: Vec<&str> = configured.iter().filter_map(|(key, set)| set.then_some(*key)).collect();
if conflicting.is_empty() {
return Ok(());
}
anyhow::bail!(
"`{SAMPLE_URL_MOCK_ONLY_CONFIG_KEY}` is set alongside `{}`; they state contradictory facts \
about the same corpus. Remove one, or drop the key(s) above and give each hosted fixture \
its own `{DOCS_SAMPLE_URL_FIXTURE_KEY}`.",
conflicting.join("`, `")
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SampleUrlSource {
Declared,
InheritedMockOnly,
InheritedCorpus,
}
#[derive(Debug)]
pub(super) struct FixtureSampleUrl<'a> {
base: DocsSampleBaseUrl<'a>,
template: Option<&'a SampleUrlTemplate>,
manifest: Option<&'a SampleUrlManifest>,
source: SampleUrlSource,
}
impl<'a> FixtureSampleUrl<'a> {
pub(super) fn base(&self) -> DocsSampleBaseUrl<'a> {
self.base
}
pub(super) fn template(&self) -> Option<&'a SampleUrlTemplate> {
self.template
}
pub(super) fn manifest(&self) -> Option<&'a SampleUrlManifest> {
self.manifest
}
pub(super) fn classify(&self, body: &str) -> Option<PlaceholderClass> {
match self.source {
SampleUrlSource::Declared => body
.contains(DEFAULT_DOCS_SAMPLE_BASE_URL)
.then_some(PlaceholderClass::Unresolved),
SampleUrlSource::InheritedMockOnly => None,
SampleUrlSource::InheritedCorpus => (self.base.is_placeholder() && body.contains(self.base.base()))
.then_some(PlaceholderClass::Unconfigured),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_declaring(sample_url: Option<&str>) -> Fixture {
let mut docs = serde_json::json!({"topic": "contract"});
if let Some(value) = sample_url {
docs["sample_url"] = serde_json::Value::String(value.to_string());
}
serde_json::from_value(serde_json::json!({
"id": "extract_uri",
"description": "Extract a document from a URI",
"input": {"url": "/pdf/report.pdf"},
"assertions": [{"type": "not_error"}],
"docs": docs,
}))
.expect("fixture must parse")
}
fn config(mock_only: bool) -> SnippetConfig {
SnippetConfig {
output: "docs/snippets".into(),
mock_only,
..SnippetConfig::default()
}
}
const PLACEHOLDER_BODY: &str = "client.extract(\"https://example.com/pdf/report.pdf\")";
#[test]
fn an_inherited_fixture_under_a_mock_only_corpus_has_no_defect_to_report() {
let config = config(true);
let policy = SampleUrlPolicy::resolve(&config, Path::new(".")).expect("policy resolves");
let fixture = fixture_declaring(None);
let sample_url = policy.for_fixture(&fixture).expect("fixture resolves");
assert_eq!(sample_url.classify(PLACEHOLDER_BODY), None);
}
#[test]
fn an_inherited_fixture_without_mock_only_is_classified_unconfigured() {
let config = config(false);
let policy = SampleUrlPolicy::resolve(&config, Path::new(".")).expect("policy resolves");
let fixture = fixture_declaring(None);
let sample_url = policy.for_fixture(&fixture).expect("fixture resolves");
assert_eq!(
sample_url.classify(PLACEHOLDER_BODY),
Some(PlaceholderClass::Unconfigured)
);
}
#[test]
fn a_declared_fixture_publishing_its_own_address_has_no_defect_to_report() {
let config = config(true);
let policy = SampleUrlPolicy::resolve(&config, Path::new(".")).expect("policy resolves");
let fixture = fixture_declaring(Some("https://samples.example.org"));
let sample_url = policy.for_fixture(&fixture).expect("fixture resolves");
assert_eq!(sample_url.base().base(), "https://samples.example.org");
assert_eq!(
sample_url.classify("client.extract(\"https://samples.example.org/pdf/report.pdf\")"),
None
);
}
#[test]
fn a_declared_fixture_still_publishing_the_reserved_domain_is_unresolved_even_under_mock_only() {
let config = config(true);
let policy = SampleUrlPolicy::resolve(&config, Path::new(".")).expect("policy resolves");
let fixture = fixture_declaring(Some("https://example.com/hosted"));
let sample_url = policy.for_fixture(&fixture).expect("fixture resolves");
assert_eq!(
sample_url.classify(PLACEHOLDER_BODY),
Some(PlaceholderClass::Unresolved)
);
assert_ne!(
PlaceholderClass::Unresolved,
PlaceholderClass::Unconfigured,
"a broken declared URL and a missing one must never collapse into one class"
);
}
#[test]
fn an_unusable_fixture_declaration_fails_naming_the_fixture_and_the_fixture_key() {
let config = config(true);
let policy = SampleUrlPolicy::resolve(&config, Path::new(".")).expect("policy resolves");
let fixture = fixture_declaring(Some("samples.example.org"));
let error = policy
.for_fixture(&fixture)
.expect_err("a scheme-less fixture address cannot form a public URL");
let message = format!("{error:#}");
assert!(
message.contains("extract_uri") && message.contains(DOCS_SAMPLE_URL_FIXTURE_KEY),
"the failure must name the fixture and the key its author wrote: {message}"
);
}
#[test]
fn mock_only_alongside_a_template_is_rejected_naming_both_keys() {
let config = SnippetConfig {
output: "docs/snippets".into(),
mock_only: true,
sample_url_template: Some("https://cdn.example.org/objects/{digest}".to_string()),
..SnippetConfig::default()
};
let error = SampleUrlPolicy::resolve(&config, Path::new("."))
.expect_err("a mock-only corpus cannot also compute every fixture's public address");
let message = format!("{error:#}");
assert!(
message.contains("mock_only") && message.contains("sample_url_template"),
"the failure must name both halves of the contradiction: {message}"
);
}
}