mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 08:05:15 +08:00
feat: prepare experiment push package
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user