diff --git a/src/commands/search.rs b/src/commands/search.rs index 633bc0d..4ce68d2 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -63,6 +63,29 @@ fn endpoint(path: &str, authenticated: bool) -> String { } } +/// Older servers still return `description`; fold it into `snippet` when no +/// snippet exists and drop the field — the CLI never outputs `description`. +fn fold_description_into_snippet(data: &mut Value) { + let Some(results) = data["results"].as_array_mut() else { + return; + }; + for result in results { + let Some(obj) = result.as_object_mut() else { + continue; + }; + let Some(desc) = obj.remove("description") else { + continue; + }; + let has_snippet = obj + .get("snippet") + .and_then(Value::as_str) + .is_some_and(|s| !s.is_empty()); + if !has_snippet && desc.as_str().is_some_and(|d| !d.is_empty()) { + obj.insert("snippet".into(), desc); + } + } +} + fn print_yaml(data: &Value) { match serde_yaml::to_string(data) { Ok(yaml) => print!("{}", yaml), @@ -308,7 +331,8 @@ pub async fn search( let api_key = key_override(api_key); let api_key = api_key.as_deref(); match execute(&req, api_key).await { - Ok(data) => { + Ok(mut data) => { + fold_description_into_snippet(&mut data); if human { ui::header(&format!("keenable search \"{}\"", query)); if let Some(results) = data["results"].as_array() { @@ -320,16 +344,17 @@ pub async fn search( for (i, result) in results.iter().enumerate() { let title = result["title"].as_str().unwrap_or("Untitled"); let url = result["url"].as_str().unwrap_or(""); - let description = result["description"].as_str().unwrap_or(""); - let desc_truncated: String = description.chars().take(200).collect(); + let snippet = result["snippet"].as_str().unwrap_or(""); + let snippet_flat = snippet.split_whitespace().collect::>().join(" "); + let snippet_truncated: String = snippet_flat.chars().take(200).collect(); let published = result["published_at"].as_str().unwrap_or(""); let acquired = result["acquired_at"].as_str().unwrap_or(""); let num = format!("{:>2}.", i + 1).dimmed(); eprintln!(" {} {}", num, title.bold()); eprintln!(" {}", url.cyan()); - if !desc_truncated.is_empty() { - eprintln!(" {}", desc_truncated.dimmed()); + if !snippet_truncated.is_empty() { + eprintln!(" {}", snippet_truncated.dimmed()); } if !published.is_empty() || !acquired.is_empty() { let mut dates = Vec::new(); @@ -365,7 +390,10 @@ pub async fn fetch(url: &str, human: bool, api_key: Option<&str>) { let api_key = key_override(api_key); let api_key = api_key.as_deref(); match execute(&req, api_key).await { - Ok(data) => { + Ok(mut data) => { + if let Some(obj) = data.as_object_mut() { + obj.remove("description"); + } if human { ui::header("keenable fetch"); let title = data["title"].as_str().unwrap_or("Untitled"); diff --git a/tests/e2e/test_fetch.py b/tests/e2e/test_fetch.py index d743510..27cb689 100644 --- a/tests/e2e/test_fetch.py +++ b/tests/e2e/test_fetch.py @@ -8,6 +8,7 @@ def test_fetch_single_url(kn): assert data["title"] == "Example Domain" assert data["url"].startswith("https://example.com") assert "# Example Domain" in data["content"] + assert "description" not in data def test_pretty_fetch(kn): diff --git a/tests/e2e/test_search.py b/tests/e2e/test_search.py index 5058fe3..84d5fbe 100644 --- a/tests/e2e/test_search.py +++ b/tests/e2e/test_search.py @@ -7,7 +7,7 @@ from conftest import SEARCH_QUERY, host_of, host_under, parse_ts, results_of, search_results, utc # published_at is omitted (not nulled) for pages with no known publish date -RESULT_FIELDS = ("url", "title", "description", "snippet", "acquired_at") +RESULT_FIELDS = ("url", "title", "snippet", "acquired_at") # --- 2.1 core --- @@ -20,6 +20,7 @@ def test_basic_search(basic_search): for r in results: for field in RESULT_FIELDS: assert field in r, f"result missing {field}: {list(r)}" + assert "description" not in r def test_pretty_output(kn): diff --git a/tests/e2e/test_semantic.py b/tests/e2e/test_semantic.py index b03f4d8..1798b8b 100644 --- a/tests/e2e/test_semantic.py +++ b/tests/e2e/test_semantic.py @@ -18,7 +18,7 @@ def blob(result) -> str: - return " ".join(str(result.get(f) or "") for f in ("title", "description", "snippet")) + return " ".join(str(result.get(f) or "") for f in ("title", "snippet")) def test_gold_fact_mozart(kn):