feat: prepare experiment push package

This commit is contained in:
2026-06-30 00:08:36 +08:00
parent 3c00659691
commit adc1cd5b91
45 changed files with 7858 additions and 65 deletions
+217
View File
@@ -0,0 +1,217 @@
//! 内存资源仓储实现。
use async_trait::async_trait;
use bat_core::domain::Resource;
use bat_core::repositories::resource_repository::{ResourceQuery, ResourceRepository};
use std::collections::HashMap;
use tokio::sync::RwLock;
/// 以内存 `HashMap` 保存资源索引的仓储实现。
#[derive(Debug, Default)]
pub struct InMemoryResourceRepository {
resources: RwLock<HashMap<String, Resource>>,
}
impl InMemoryResourceRepository {
/// 创建空资源仓储。
pub fn new() -> Self {
Self::default()
}
fn sorted_resources(resources: &HashMap<String, Resource>) -> Vec<Resource> {
let mut resources = resources.values().cloned().collect::<Vec<_>>();
resources.sort_by(|left, right| left.id.cmp(&right.id));
resources
}
}
#[async_trait]
impl ResourceRepository for InMemoryResourceRepository {
async fn add(&self, resource: Resource) -> bat_core::Result<String> {
let id = resource.id.clone();
self.resources.write().await.insert(id.clone(), resource);
Ok(id)
}
async fn find_by_id(&self, id: &str) -> bat_core::Result<Resource> {
self.resources
.read()
.await
.get(id)
.cloned()
.ok_or_else(|| bat_core::Error::NotFound(id.to_string()))
}
async fn find_by_hash(&self, hash: &str) -> bat_core::Result<Resource> {
let resources = self.resources.read().await;
Self::sorted_resources(&resources)
.into_iter()
.find(|resource| resource.entry.hash == hash)
.ok_or_else(|| bat_core::Error::NotFound(hash.to_string()))
}
async fn list(&self, query: ResourceQuery) -> bat_core::Result<Vec<Resource>> {
let resources = self.resources.read().await;
let resources = Self::sorted_resources(&resources)
.into_iter()
.filter(|resource| query_matches(&query, resource))
.collect();
Ok(resources)
}
async fn update(&self, resource: Resource) -> bat_core::Result<()> {
let mut resources = self.resources.write().await;
if !resources.contains_key(&resource.id) {
return Err(bat_core::Error::NotFound(resource.id));
}
resources.insert(resource.id.clone(), resource);
Ok(())
}
async fn delete(&self, id: &str) -> bat_core::Result<()> {
self.resources
.write()
.await
.remove(id)
.map(|_| ())
.ok_or_else(|| bat_core::Error::NotFound(id.to_string()))
}
async fn count(&self, query: ResourceQuery) -> bat_core::Result<u64> {
Ok(self.list(query).await?.len() as u64)
}
}
fn query_matches(query: &ResourceQuery, resource: &Resource) -> bool {
if let Some(resource_type) = query.resource_type {
if resource.entry.resource_type != resource_type {
return false;
}
}
if let Some(hash) = &query.hash {
if resource.entry.hash != *hash {
return false;
}
}
if let Some(pattern) = &query.path_pattern {
if !wildcard_matches(pattern, &resource.entry.path) {
return false;
}
}
true
}
fn wildcard_matches(pattern: &str, value: &str) -> bool {
wildcard_matches_bytes(pattern.as_bytes(), value.as_bytes())
}
fn wildcard_matches_bytes(pattern: &[u8], value: &[u8]) -> bool {
match (pattern.first(), value.first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some(b'*'), _) => {
wildcard_matches_bytes(&pattern[1..], value)
|| value
.first()
.is_some_and(|_| wildcard_matches_bytes(pattern, &value[1..]))
}
(Some(b'?'), Some(_)) => wildcard_matches_bytes(&pattern[1..], &value[1..]),
(Some(pattern_byte), Some(value_byte)) if pattern_byte == value_byte => {
wildcard_matches_bytes(&pattern[1..], &value[1..])
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bat_core::domain::{ResourceEntry, ResourceType};
use std::path::PathBuf;
fn resource(id: &str, path: &str, hash: &str, resource_type: ResourceType) -> Resource {
Resource {
id: id.to_string(),
local_path: PathBuf::from(path),
entry: ResourceEntry {
path: path.to_string(),
hash: hash.to_string(),
size: 7,
resource_type,
},
}
}
#[tokio::test]
async fn add_find_update_and_delete_resource() {
let repository = InMemoryResourceRepository::new();
let id = repository
.add(resource(
"resource/synthetic-minimal.bundle",
"synthetic-minimal.bundle",
"hash-a",
ResourceType::AssetBundle,
))
.await
.unwrap();
assert_eq!(id, "resource/synthetic-minimal.bundle");
assert_eq!(
repository.find_by_id(&id).await.unwrap().entry.path,
"synthetic-minimal.bundle"
);
let mut updated = repository.find_by_id(&id).await.unwrap();
updated.entry.size = 12;
repository.update(updated).await.unwrap();
assert_eq!(repository.find_by_id(&id).await.unwrap().entry.size, 12);
repository.delete(&id).await.unwrap();
assert!(matches!(
repository.find_by_id(&id).await,
Err(bat_core::Error::NotFound(_))
));
}
#[tokio::test]
async fn list_filters_by_type_hash_and_path() {
let repository = InMemoryResourceRepository::new();
repository
.add(resource(
"resource/a",
"synthetic-minimal.bundle",
"hash-a",
ResourceType::AssetBundle,
))
.await
.unwrap();
repository
.add(resource(
"resource/b",
"catalog.json",
"hash-b",
ResourceType::Manifest,
))
.await
.unwrap();
let query = ResourceQuery {
resource_type: Some(ResourceType::AssetBundle),
hash: Some("hash-a".to_string()),
path_pattern: Some("synthetic-*.bundle".to_string()),
};
let results = repository.list(query).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "resource/a");
assert_eq!(
repository.find_by_hash("hash-a").await.unwrap().id,
"resource/a"
);
assert_eq!(repository.count(ResourceQuery::all()).await.unwrap(), 2);
}
}