use patent::model::{Match, Query, Source as SourceId};
use patent::sources::artifacthub::ArtifactHub;
use patent::sources::aur::Aur;
use patent::sources::crates_io::CratesIo;
use patent::sources::docker_hub::DockerHub;
use patent::sources::github::GitHub;
use patent::sources::go::GoPkgDev;
use patent::sources::hackage::Hackage;
use patent::sources::hacker_news::HackerNews;
use patent::sources::hex::Hex;
use patent::sources::homebrew::Homebrew;
use patent::sources::jetbrains::JetBrains;
use patent::sources::maven::Maven;
use patent::sources::nixpkgs::Nixpkgs;
use patent::sources::npm::Npm;
use patent::sources::nuget::NuGet;
use patent::sources::packagist::Packagist;
use patent::sources::pypi::PyPI;
use patent::sources::rubygems::RubyGems;
use patent::sources::vscode::VsCodeMarketplace;
use patent::sources::SourceAdapter;
fn client() -> reqwest::Client {
reqwest::Client::builder()
.user_agent(concat!(
"patent/",
env!("CARGO_PKG_VERSION"),
" (prior-art search; https://github.com/r14dd/patent)"
))
.build()
.unwrap()
}
fn query(idea: &str, keywords: &[&str]) -> Query {
Query {
idea: idea.to_string(),
keywords: keywords.iter().map(|k| k.to_string()).collect(),
}
}
fn assert_live(matches: &[Match], expected: SourceId) {
assert!(
!matches.is_empty(),
"{expected} returned zero matches for a popular query — the live API \
shape has likely drifted and the adapter can no longer parse it"
);
for m in matches {
assert!(
!m.name.trim().is_empty(),
"{expected}: a match has an empty name: {m:?}"
);
assert!(
!m.url.trim().is_empty(),
"{expected}: match {:?} has an empty url",
m.name
);
assert_eq!(
m.source, expected,
"{expected}: a match is attributed to the wrong source"
);
}
eprintln!("✓ {expected}: {} matches", matches.len());
}
fn assert_dated(matches: &[Match], expected: SourceId) {
let dated = matches.iter().filter(|m| m.last_updated.is_some()).count();
assert!(
dated > 0,
"{expected}: not one of {} live matches carried a last-updated date — \
the upstream date field has likely been renamed, retyped, or dropped",
matches.len()
);
let this_year = patent::freshness::now().year();
for ts in matches.iter().filter_map(|m| m.last_updated.as_deref()) {
let parsed = patent::freshness::parse(ts)
.unwrap_or_else(|| panic!("{expected}: stored an unparseable date: {ts:?}"));
assert!(
(2000..=this_year + 1).contains(&parsed.year()),
"{expected}: implausible last-updated date {ts:?} — the upstream \
timestamp units have likely changed"
);
}
eprintln!("✓ {expected}: {dated}/{} matches dated", matches.len());
}
macro_rules! live {
($name:ident, $adapter:expr, $source:expr, $idea:expr, $keywords:expr) => {
live!($name, $adapter, $source, $idea, $keywords, dated: false);
};
($name:ident, $adapter:expr, $source:expr, $idea:expr, $keywords:expr, dated: $dated:expr) => {
#[tokio::test]
#[ignore = "hits the live network; run with --ignored"]
async fn $name() {
let adapter = $adapter;
let matches = adapter
.search(&query($idea, $keywords))
.await
.unwrap_or_else(|e| panic!("{} live search errored: {e}", $source));
assert_live(&matches, $source);
if $dated {
assert_dated(&matches, $source);
}
}
};
}
live!(
live_crates_io,
CratesIo::new(client()),
SourceId::CratesIo,
"a fast async runtime for rust",
&["async", "runtime"],
dated: true
);
live!(
live_github,
GitHub::new(client()),
SourceId::GitHub,
"a kubernetes command line tool",
&["kubernetes", "cli"],
dated: true
);
live!(
live_npm,
Npm::new(client()),
SourceId::Npm,
"react component library",
&["react"],
dated: true
);
#[tokio::test]
#[ignore = "live network; also an ACCEPTED DEGRADATION — PyPI is bot-walled with no keyless search, see comment"]
async fn live_pypi() {
let adapter = PyPI::new(client());
let matches = adapter
.search(&query("http requests library for python", &["requests"]))
.await
.unwrap_or_else(|e| panic!("PyPI live search errored: {e}"));
assert_live(&matches, SourceId::PyPI);
}
live!(
live_hacker_news,
HackerNews::new(client()),
SourceId::HackerNews,
"database",
&["database"]
);
live!(
live_go,
GoPkgDev::new(client()),
SourceId::Go,
"web framework for go",
&["gin"],
dated: true
);
live!(
live_maven,
Maven::new(client()),
SourceId::Maven,
"json serialization library",
&["jackson"],
dated: true
);
live!(
live_rubygems,
RubyGems::new(client()),
SourceId::RubyGems,
"a ruby web framework",
&["rails"]
);
live!(
live_docker_hub,
DockerHub::new(client()),
SourceId::DockerHub,
"a web server container image",
&["nginx"]
);
live!(
live_vscode,
VsCodeMarketplace::new(client()),
SourceId::VsCodeMarketplace,
"python language support",
&["python"]
);
live!(
live_nuget,
NuGet::new(client()),
SourceId::NuGet,
"a json library",
&["json"]
);
live!(
live_homebrew,
Homebrew::new(client()),
SourceId::Homebrew,
"download files from the web",
&["wget"]
);
live!(
live_packagist,
Packagist::new(client()),
SourceId::Packagist,
"a php web framework",
&["laravel"]
);
live!(
live_hex,
Hex::new(client()),
SourceId::Hex,
"an elixir web framework",
&["phoenix"],
dated: true
);
live!(
live_artifacthub,
ArtifactHub::new(client()),
SourceId::ArtifactHub,
"prometheus monitoring helm chart",
&["prometheus"],
dated: true
);
live!(
live_aur,
Aur::new(client()),
SourceId::Aur,
"an aur helper",
&["yay"],
dated: true
);
live!(
live_hackage,
Hackage::new(client()),
SourceId::Hackage,
"a haskell json parsing library",
&["json"]
);
live!(
live_jetbrains,
JetBrains::new(client()),
SourceId::JetBrains,
"a jetbrains plugin for kubernetes yaml files",
&["plugin", "kubernetes", "yaml"],
dated: true
);
live!(
live_nixpkgs,
Nixpkgs::new(client()),
SourceId::Nixpkgs,
"a fast recursive grep replacement",
&["ripgrep"]
);
live!(
live_nixpkgs_multi_keyword,
Nixpkgs::new(client()),
SourceId::Nixpkgs,
"a nix flake tool for managing developer shells",
&["nix", "flake", "developer", "shell", "manager"]
);