use crate::types::{DartVersion, PackageInfo};
use crate::version::compare_versions;
use deps_core::{HttpCache, Result};
use serde::Deserialize;
use std::any::Any;
use std::sync::Arc;
const PUB_DEV_API_BASE: &str = "https://pub.dev/api";
pub fn package_url(name: &str) -> String {
format!("https://pub.dev/packages/{name}")
}
#[derive(Clone)]
pub struct PubDevRegistry {
cache: Arc<HttpCache>,
}
impl PubDevRegistry {
pub const fn new(cache: Arc<HttpCache>) -> Self {
Self { cache }
}
pub async fn get_versions(&self, name: &str) -> Result<Vec<DartVersion>> {
let url = format!("{PUB_DEV_API_BASE}/packages/{name}");
let data = self.cache.get_cached(&url).await?;
parse_versions_response(&data)
}
pub async fn get_latest_matching(
&self,
name: &str,
req_str: &str,
) -> Result<Option<DartVersion>> {
let versions = self.get_versions(name).await?;
Ok(versions.into_iter().find(|v| {
crate::version::version_matches_constraint(&v.version, req_str) && !v.retracted
}))
}
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<PackageInfo>> {
let url = format!("{PUB_DEV_API_BASE}/search?q={}", urlencoding::encode(query));
let data = self.cache.get_cached(&url).await?;
let search_result: SearchResponse = serde_json::from_slice(&data)?;
let mut results = Vec::new();
for entry in search_result.packages.into_iter().take(limit) {
let pkg_url = format!("{PUB_DEV_API_BASE}/packages/{}", entry.package);
if let Ok(pkg_data) = self.cache.get_cached(&pkg_url).await
&& let Ok(info) = parse_package_info(&pkg_data)
{
results.push(info);
}
}
Ok(results)
}
pub async fn get_package_info(&self, name: &str) -> Result<PackageInfo> {
let url = format!("{PUB_DEV_API_BASE}/packages/{name}");
let data = self.cache.get_cached(&url).await?;
parse_package_info(&data)
}
}
#[derive(Deserialize)]
struct PackageResponse {
name: String,
latest: VersionDetail,
versions: Vec<VersionEntry>,
}
#[derive(Deserialize)]
struct VersionEntry {
version: String,
#[serde(default)]
retracted: bool,
published: Option<String>,
}
#[derive(Deserialize)]
struct VersionDetail {
version: String,
pubspec: Option<PubspecMeta>,
}
#[derive(Deserialize)]
struct PubspecMeta {
name: Option<String>,
description: Option<String>,
homepage: Option<String>,
repository: Option<String>,
documentation: Option<String>,
}
#[derive(Deserialize)]
struct SearchResponse {
#[serde(default)]
packages: Vec<SearchEntry>,
}
#[derive(Deserialize)]
struct SearchEntry {
package: String,
}
fn parse_versions_response(data: &[u8]) -> Result<Vec<DartVersion>> {
let response: PackageResponse = serde_json::from_slice(data)?;
let mut versions: Vec<DartVersion> = response
.versions
.into_iter()
.map(|e| DartVersion {
version: e.version,
retracted: e.retracted,
published: e.published,
})
.collect();
versions.sort_by(|a, b| compare_versions(&b.version, &a.version));
Ok(versions)
}
fn parse_package_info(data: &[u8]) -> Result<PackageInfo> {
let response: PackageResponse = serde_json::from_slice(data)?;
let pubspec = response.latest.pubspec.unwrap_or(PubspecMeta {
name: Some(response.name.clone()),
description: None,
homepage: None,
repository: None,
documentation: None,
});
Ok(PackageInfo {
name: pubspec.name.unwrap_or(response.name),
description: pubspec.description,
homepage: pubspec.homepage,
repository: pubspec.repository,
documentation: pubspec.documentation,
version: response.latest.version,
license: None,
})
}
impl deps_core::Version for DartVersion {
fn version_string(&self) -> &str {
&self.version
}
fn is_yanked(&self) -> bool {
self.retracted
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl deps_core::Metadata for PackageInfo {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
fn repository(&self) -> Option<&str> {
self.repository.as_deref()
}
fn documentation(&self) -> Option<&str> {
self.documentation.as_deref()
}
fn latest_version(&self) -> &str {
&self.version
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl deps_core::Registry for PubDevRegistry {
fn get_versions<'a>(
&'a self,
name: &'a str,
) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
Box::pin(async move {
let versions = self.get_versions(name).await?;
Ok(versions
.into_iter()
.map(|v| Box::new(v) as Box<dyn deps_core::Version>)
.collect())
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a str,
req: &'a str,
) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
Box::pin(async move {
let version = self.get_latest_matching(name, req).await?;
Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
})
}
fn search<'a>(
&'a self,
query: &'a str,
limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
Box::pin(async move {
let results = self.search(query, limit).await?;
Ok(results
.into_iter()
.map(|m| Box::new(m) as Box<dyn deps_core::Metadata>)
.collect())
})
}
fn package_url(&self, name: &str) -> String {
package_url(name)
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_url() {
assert_eq!(package_url("provider"), "https://pub.dev/packages/provider");
assert_eq!(package_url("http"), "https://pub.dev/packages/http");
}
#[test]
fn test_parse_versions_response() {
let json = r#"{
"name": "http",
"latest": {"version": "1.2.0", "pubspec": {"name": "http"}},
"versions": [
{"version": "1.0.0", "retracted": false},
{"version": "1.2.0", "retracted": false},
{"version": "1.1.0", "retracted": false},
{"version": "0.9.0", "retracted": true}
]
}"#;
let versions = parse_versions_response(json.as_bytes()).unwrap();
assert_eq!(versions.len(), 4);
assert_eq!(versions[0].version, "1.2.0");
assert_eq!(versions[1].version, "1.1.0");
assert_eq!(versions[2].version, "1.0.0");
assert!(versions[3].retracted);
}
#[test]
fn test_parse_versions_response_empty() {
let json = r#"{
"name": "test",
"latest": {"version": "1.0.0"},
"versions": []
}"#;
let versions = parse_versions_response(json.as_bytes()).unwrap();
assert!(versions.is_empty());
}
#[test]
fn test_parse_package_info() {
let json = r#"{
"name": "provider",
"latest": {
"version": "6.1.2",
"pubspec": {
"name": "provider",
"description": "A wrapper around InheritedWidget",
"homepage": "https://pub.dev/packages/provider",
"repository": "https://github.com/rrousselGit/provider",
"documentation": "https://pub.dev/documentation/provider"
}
},
"versions": []
}"#;
let info = parse_package_info(json.as_bytes()).unwrap();
assert_eq!(info.name, "provider");
assert_eq!(
info.description,
Some("A wrapper around InheritedWidget".into())
);
assert_eq!(info.version, "6.1.2");
}
#[test]
fn test_parse_package_info_minimal() {
let json = r#"{
"name": "minimal",
"latest": {"version": "0.1.0"},
"versions": []
}"#;
let info = parse_package_info(json.as_bytes()).unwrap();
assert_eq!(info.name, "minimal");
assert_eq!(info.version, "0.1.0");
assert!(info.description.is_none());
}
#[test]
fn test_parse_search_response() {
let json = r#"{
"packages": [
{"package": "provider"},
{"package": "riverpod"}
]
}"#;
let response: SearchResponse = serde_json::from_slice(json.as_bytes()).unwrap();
assert_eq!(response.packages.len(), 2);
assert_eq!(response.packages[0].package, "provider");
}
#[test]
fn test_registry_creation() {
let cache = Arc::new(HttpCache::new());
let _registry = PubDevRegistry::new(cache);
}
#[test]
fn test_version_trait() {
use deps_core::Version;
let ver = DartVersion {
version: "1.0.0".into(),
retracted: true,
published: None,
};
assert_eq!(ver.version_string(), "1.0.0");
assert!(ver.is_yanked());
assert!(ver.features().is_empty());
}
#[test]
fn test_metadata_trait() {
use deps_core::Metadata;
let info = PackageInfo {
name: "test".into(),
description: Some("A test package".into()),
homepage: None,
repository: Some("https://github.com/test/test".into()),
documentation: None,
version: "1.0.0".into(),
license: None,
};
assert_eq!(info.name(), "test");
assert_eq!(info.description(), Some("A test package"));
assert_eq!(info.repository(), Some("https://github.com/test/test"));
assert!(info.documentation().is_none());
}
#[test]
fn test_registry_package_url_trait() {
use deps_core::Registry;
let cache = Arc::new(HttpCache::new());
let registry = PubDevRegistry::new(cache);
assert_eq!(
registry.package_url("http"),
"https://pub.dev/packages/http"
);
}
#[test]
fn test_registry_as_any() {
use deps_core::Registry;
let cache = Arc::new(HttpCache::new());
let registry = PubDevRegistry::new(cache);
assert!(registry.as_any().is::<PubDevRegistry>());
}
}