Files
MetaCrate/crates/libremetaverse/src/inventory_ais.rs
Chili Palmer d5c318d280
All checks were successful
Native code generation / deterministic (push) Successful in 13m18s
Imaging and meshing gate / native (push) Successful in 4m12s
Native Rust workspace compile / compile (push) Successful in 4m12s
Implement native AIS inventory reconciliation (#63)
2026-08-10 06:41:29 +00:00

1061 lines
32 KiB
Rust

//! Native Inventory API v3 capability client and cache reconciliation.
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::too_many_arguments)]
use crate::client_core::ClientWeakHandle;
use crate::{Error, GridClient, InventoryFolder, InventoryItem};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{CancellationToken, HttpResponse, Uri};
use libremetaverse_types::{AssetType, UUID};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
const MAX_FOLDER_DEPTH_REQUEST: i32 = 50;
const MAX_AIS_OBJECTS: usize = 100_000;
const LLSD_XML: &str = "application/llsd+xml";
type InventoryResult = (
bool,
Vec<InventoryFolder>,
Vec<InventoryItem>,
Vec<InventoryItem>,
);
type ParsedInventory = (Vec<InventoryFolder>, Vec<InventoryItem>, Vec<InventoryItem>);
type CollectionSections<'a> = (Option<&'a HashMap<String, OSD>>, Option<&'a OSD>);
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AISResponseMeta {
broken_links_removed: Vec<UUID>,
items_removed: Vec<UUID>,
categories_removed: Vec<UUID>,
category_version_updates: HashMap<UUID, i32>,
}
impl AISResponseMeta {
pub fn new(
broken_links: Vec<UUID>,
items: Vec<UUID>,
categories: Vec<UUID>,
versions: HashMap<UUID, i32>,
) -> Result<Self, Error> {
Ok(Self {
broken_links_removed: broken_links,
items_removed: items,
categories_removed: categories,
category_version_updates: versions,
})
}
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn broken_links_removed(&self) -> Vec<UUID> {
self.broken_links_removed.clone()
}
#[must_use]
pub fn items_removed(&self) -> Vec<UUID> {
self.items_removed.clone()
}
#[must_use]
pub fn categories_removed(&self) -> Vec<UUID> {
self.categories_removed.clone()
}
#[must_use]
pub fn category_version_updates(&self) -> HashMap<UUID, i32> {
self.category_version_updates.clone()
}
#[must_use]
pub fn has_any_data(&self) -> bool {
!self.broken_links_removed.is_empty()
|| !self.items_removed.is_empty()
|| !self.categories_removed.is_empty()
|| !self.category_version_updates.is_empty()
}
}
#[derive(Clone)]
pub struct InventoryAISClient {
client: ClientWeakHandle,
}
impl InventoryAISClient {
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
let client = client.ok_or(Error::ArgumentNull)?;
Ok(Self {
client: client.native_weak_handle(),
})
}
fn client(&self) -> Result<GridClient, Error> {
self.client.upgrade().ok_or(Error::InvalidOperation)
}
fn capability(&self, name: &str) -> Result<Option<Uri>, Error> {
let client = self.client()?;
let Some(simulator) = client.native_network()?.current_sim() else {
return Ok(None);
};
let Some(caps) = simulator.native_caps() else {
return Ok(None);
};
caps.capability_uri(name.to_owned())
}
pub(crate) fn native_is_available(&self) -> bool {
self.capability(Self::INVENTORY_CAP_NAME)
.ok()
.flatten()
.is_some()
}
fn endpoint(capability: &Uri, path: &str) -> Uri {
Uri(format!("{}/{}", capability.0.trim_end_matches('/'), path))
}
fn valid_category(category: &str) -> bool {
!category.is_empty()
&& category
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
fn token(token: Option<CancellationToken>) -> CancellationToken {
token.unwrap_or_default()
}
fn status_ok(response: &HttpResponse) -> bool {
response.is_success_status_code() || response.status_code == 304
}
async fn request(
&self,
cap_name: &str,
method: &str,
path: String,
headers: BTreeMap<String, String>,
payload: Option<OSD>,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> {
let token = Self::token(cancellation_token);
token.throw_if_cancellation_requested()?;
let Some(capability) = self.capability(cap_name)? else {
return Ok(None);
};
let bytes = match payload {
Some(payload) => OSDParser::serialize_llsd_xml_bytes(payload)?,
None => Vec::new(),
};
let content_type = (!bytes.is_empty()).then(|| LLSD_XML.to_owned());
self.client()?
.native_http_caps_client()
.send_custom(
method,
Self::endpoint(&capability, &path),
headers,
content_type,
bytes,
token,
)
.await
.map(Some)
}
fn decode_map(bytes: Vec<u8>) -> Result<Option<HashMap<String, OSD>>, Error> {
if bytes.is_empty() {
return Ok(None);
}
match OSDParser::deserialize_with_bytes(bytes)? {
OSD::Map(map) => Ok(Some(map)),
_ => Err(Error::Argument),
}
}
fn parse_all_map(&self, map: &HashMap<String, OSD>) -> Result<ParsedInventory, Error> {
let response = OSD::Map(map.clone());
let folders = self.parse_folders_osd(&response)?;
let items = self.parse_items_osd(&response)?;
let links = self.parse_links_osd(&response)?;
if folders.len() + items.len() + links.len() > MAX_AIS_OBJECTS {
return Err(Error::InvalidOperation);
}
Ok((folders, items, links))
}
fn reconcile(
&self,
map: &HashMap<String, OSD>,
folders: &[InventoryFolder],
items: &[InventoryItem],
links: &[InventoryItem],
) -> Result<(), Error> {
let meta = Self::parse_meta_map(map)?;
if folders.is_empty() && items.is_empty() && links.is_empty() && !meta.has_any_data() {
return Ok(());
}
self.client()?.native_inventory()?.native_reconcile_ais(
folders.to_vec(),
items.iter().chain(links).cloned().collect(),
&meta,
)
}
async fn mutation(
&self,
cap_name: &str,
method: &str,
path: String,
headers: BTreeMap<String, String>,
payload: Option<OSD>,
gone_is_success: bool,
cancellation_token: Option<CancellationToken>,
) -> Result<
(
bool,
Vec<InventoryFolder>,
Vec<InventoryItem>,
Vec<InventoryItem>,
),
Error,
> {
let Some((response, bytes)) = self
.request(cap_name, method, path, headers, payload, cancellation_token)
.await?
else {
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
};
let success =
Self::status_ok(&response) || (gone_is_success && response.status_code == 410);
if !success {
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
}
let Some(map) = Self::decode_map(bytes)? else {
return Ok((true, Vec::new(), Vec::new(), Vec::new()));
};
let (folders, items, links) = self.parse_all_map(&map)?;
self.reconcile(&map, &folders, &items, &links)?;
Ok((true, folders, items, links))
}
async fn mutation_bool(
&self,
cap_name: &str,
method: &str,
path: String,
headers: BTreeMap<String, String>,
payload: Option<OSD>,
gone_is_success: bool,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
match self
.mutation(
cap_name,
method,
path,
headers,
payload,
gone_is_success,
cancellation_token,
)
.await
{
Ok((success, _, _, _)) => Ok(success),
Err(Error::Cancelled) => Err(Error::Cancelled),
Err(_error) => {
#[cfg(test)]
eprintln!("AIS mutation failed: {_error:?}");
Ok(false)
}
}
}
async fn get_parsed(
&self,
cap_name: &str,
path: String,
cancellation_token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
let result = async {
let Some((response, bytes)) = self
.request(
cap_name,
"GET",
path,
BTreeMap::new(),
None,
cancellation_token,
)
.await?
else {
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
};
if !Self::status_ok(&response) {
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
}
let Some(map) = Self::decode_map(bytes)? else {
return Ok((true, Vec::new(), Vec::new(), Vec::new()));
};
let parsed = self.parse_all_map(&map)?;
self.reconcile(&map, &parsed.0, &parsed.1, &parsed.2)?;
Ok((true, parsed.0, parsed.1, parsed.2))
}
.await;
match result {
Err(Error::Cancelled) => Err(Error::Cancelled),
Err(_) => Ok((false, Vec::new(), Vec::new(), Vec::new())),
value => value,
}
}
fn tid() -> Result<UUID, Error> {
UUID::random()
}
fn destination(uuid: UUID) -> BTreeMap<String, String> {
BTreeMap::from([("Destination".to_owned(), uuid.to_string())])
}
pub(crate) async fn native_create_inventory(
&self,
parent_uuid: UUID,
new_inventory: OSD,
create_link: bool,
cancellation_token: Option<CancellationToken>,
) -> Result<(bool, Option<InventoryItem>), Error> {
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
match self
.mutation(
Self::INVENTORY_CAP_NAME,
"POST",
path,
BTreeMap::new(),
Some(new_inventory),
false,
cancellation_token,
)
.await
{
Ok((success, _, items, links)) => Ok((
success,
if create_link { links } else { items }.into_iter().next(),
)),
Err(Error::Cancelled) => Err(Error::Cancelled),
Err(_) => Ok((false, None)),
}
}
#[allow(dead_code)] // Internal mapped helper used by InventoryManager batch links.
pub(crate) async fn native_create_inventory_links(
&self,
parent_uuid: UUID,
new_inventory: OSD,
cancellation_token: Option<CancellationToken>,
) -> Result<Vec<InventoryItem>, Error> {
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
let (_, _, _, links) = self
.mutation(
Self::INVENTORY_CAP_NAME,
"POST",
path,
BTreeMap::new(),
Some(new_inventory),
false,
cancellation_token,
)
.await?;
Ok(links)
}
pub(crate) async fn native_slam_folder(
&self,
folder: UUID,
payload: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"PUT",
format!("category/{folder}/links?tid={}", Self::tid()?),
BTreeMap::new(),
Some(payload),
false,
token,
)
.await
}
pub(crate) async fn native_remove_category(
&self,
id: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"DELETE",
format!("category/{id}"),
BTreeMap::new(),
None,
true,
token,
)
.await
}
pub(crate) async fn native_remove_item(
&self,
id: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"DELETE",
format!("item/{id}"),
BTreeMap::new(),
None,
true,
token,
)
.await
}
pub(crate) async fn native_copy_library_category(
&self,
source: UUID,
destination: UUID,
subfolders: bool,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
let suffix = if subfolders {
String::new()
} else {
",depth=0".to_owned()
};
self.mutation_bool(
Self::LIBRARY_CAP_NAME,
"COPY",
format!("category/{source}?tid={}{}", Self::tid()?, suffix),
Self::destination(destination),
None,
false,
token,
)
.await
}
pub(crate) async fn native_purge_descendents(
&self,
id: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"DELETE",
format!("category/{id}/children"),
BTreeMap::new(),
None,
false,
token,
)
.await
}
pub(crate) async fn native_update_category(
&self,
id: UUID,
payload: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"PATCH",
format!("category/{id}"),
BTreeMap::new(),
Some(payload),
false,
token,
)
.await
}
pub(crate) async fn native_update_item(
&self,
id: UUID,
payload: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"PATCH",
format!("item/{id}"),
BTreeMap::new(),
Some(payload),
false,
token,
)
.await
}
pub(crate) async fn native_copy_category(
&self,
source: UUID,
destination: UUID,
simulate: Option<bool>,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
let simulate = if simulate.unwrap_or(false) {
"&simulate=1"
} else {
""
};
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"COPY",
format!("category/{source}?tid={}{}", Self::tid()?, simulate),
Self::destination(destination),
None,
false,
token,
)
.await
}
pub(crate) async fn native_move_category(
&self,
source: UUID,
destination: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.native_update_category(
source,
OSD::Map(HashMap::from([(
"parent_id".into(),
OSD::UUID(destination),
)])),
token,
)
.await
}
pub(crate) async fn native_put_category_children(
&self,
category: String,
payload: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"PUT",
format!("category/{category}/children?tid={}", Self::tid()?),
BTreeMap::new(),
Some(payload),
false,
token,
)
.await
}
pub(crate) async fn native_copy_category_children(
&self,
category: String,
destination: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"COPY",
format!("category/{category}/children?tid={}", Self::tid()?),
Self::destination(destination),
None,
false,
token,
)
.await
}
#[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
pub(crate) async fn native_move_category_children(
&self,
_category: String,
_destination: UUID,
_token: Option<CancellationToken>,
) -> Result<bool, Error> {
Ok(false)
}
pub(crate) async fn native_delete_category_children(
&self,
category: String,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"DELETE",
format!("category/{category}/children"),
BTreeMap::new(),
None,
false,
token,
)
.await
}
pub(crate) async fn native_put_category_links(
&self,
category: String,
payload: OSD,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"PUT",
format!("category/{category}/links?tid={}", Self::tid()?),
BTreeMap::new(),
Some(payload),
false,
token,
)
.await
}
pub(crate) async fn native_copy_category_links(
&self,
category: String,
destination: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"COPY",
format!("category/{category}/links?tid={}", Self::tid()?),
Self::destination(destination),
None,
false,
token,
)
.await
}
#[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
pub(crate) async fn native_move_category_links(
&self,
_category: String,
_destination: UUID,
_token: Option<CancellationToken>,
) -> Result<bool, Error> {
Ok(false)
}
pub(crate) async fn native_delete_category_links(
&self,
category: String,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
if !Self::valid_category(&category) {
return Ok(false);
}
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"DELETE",
format!("category/{category}/links"),
BTreeMap::new(),
None,
false,
token,
)
.await
}
pub(crate) async fn native_copy_item(
&self,
item: UUID,
destination: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.mutation_bool(
Self::INVENTORY_CAP_NAME,
"COPY",
format!("item/{item}?tid={}", Self::tid()?),
Self::destination(destination),
None,
false,
token,
)
.await
}
pub(crate) async fn native_move_item(
&self,
item: UUID,
destination: UUID,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.native_update_item(
item,
OSD::Map(HashMap::from([(
"parent_id".into(),
OSD::UUID(destination),
)])),
token,
)
.await
}
pub(crate) async fn native_get_category(
&self,
category: String,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
if !Self::valid_category(&category) {
return Ok((false, vec![], vec![], vec![]));
}
self.get_parsed(
Self::INVENTORY_CAP_NAME,
format!("category/{category}"),
token,
)
.await
}
pub(crate) async fn native_get_category_children(
&self,
category: String,
depth: i32,
recursive: bool,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
if !Self::valid_category(&category) {
return Ok((false, vec![], vec![], vec![]));
}
let depth = if recursive {
MAX_FOLDER_DEPTH_REQUEST
} else {
depth.min(MAX_FOLDER_DEPTH_REQUEST)
};
self.get_parsed(
Self::INVENTORY_CAP_NAME,
format!("category/{category}/children?depth={depth}"),
token,
)
.await
}
pub(crate) async fn native_fetch_category_categories(
&self,
id: UUID,
inventory: bool,
recursive: bool,
depth: Option<i32>,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
let depth = if recursive {
MAX_FOLDER_DEPTH_REQUEST
} else {
depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
};
let cap = if inventory {
Self::INVENTORY_CAP_NAME
} else {
Self::LIBRARY_CAP_NAME
};
self.get_parsed(
cap,
format!("category/{id}/categories?depth={depth}"),
token,
)
.await
}
pub(crate) async fn native_fetch_category_subset(
&self,
id: UUID,
children: Box<dyn Iterator<Item = UUID>>,
inventory: bool,
recursive: bool,
depth: Option<i32>,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
let children: Vec<_> = children.take(MAX_AIS_OBJECTS).collect();
if children.is_empty() {
return Ok((false, vec![], vec![], vec![]));
}
let depth = if recursive {
MAX_FOLDER_DEPTH_REQUEST
} else {
depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
};
let list = children
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
let cap = if inventory {
Self::INVENTORY_CAP_NAME
} else {
Self::LIBRARY_CAP_NAME
};
self.get_parsed(
cap,
format!("category/{id}/children?depth={depth}&children={list}"),
token,
)
.await
}
pub(crate) async fn native_get_category_links(
&self,
category: String,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
if !Self::valid_category(&category) {
return Ok((false, vec![], vec![], vec![]));
}
self.get_parsed(
Self::INVENTORY_CAP_NAME,
format!("category/{category}/links"),
token,
)
.await
}
pub(crate) async fn native_fetch_item(
&self,
id: UUID,
token: Option<CancellationToken>,
) -> Result<InventoryResult, Error> {
let result = self
.get_parsed(
Self::INVENTORY_CAP_NAME,
format!("item/{id}"),
token.clone(),
)
.await?;
if result.0 && result.1.is_empty() && result.2.is_empty() && result.3.is_empty() {
// `get_parsed` handles embedded shapes; retrying is intentionally avoided.
}
Ok(result)
}
pub(crate) async fn native_fetch_cof(
&self,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.native_get_category_links("current".into(), token)
.await
.map(|v| v.0)
}
pub(crate) async fn native_fetch_orphans(
&self,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.get_parsed(Self::INVENTORY_CAP_NAME, "orphans".into(), token)
.await
.map(|v| v.0)
}
pub(crate) async fn native_empty_trash(
&self,
token: Option<CancellationToken>,
) -> Result<bool, Error> {
self.native_delete_category_children("trash".into(), token)
.await
}
pub(crate) fn native_parse_items_from_embedded(
&self,
response: Option<OSD>,
) -> Result<Vec<InventoryItem>, Error> {
response.map_or(Ok(Vec::new()), |response| self.parse_items_osd(&response))
}
pub(crate) fn native_parse_folders_from_embedded(
&self,
response: OSDMap,
) -> Result<Vec<InventoryFolder>, Error> {
self.parse_folders_osd(&OSD::Map(response.snapshot()))
}
pub(crate) fn native_parse_links_from_embedded(
&self,
response: Option<OSD>,
) -> Result<Vec<InventoryItem>, Error> {
response.map_or(Ok(Vec::new()), |response| self.parse_links_osd(&response))
}
pub(crate) fn native_parse_embedded(
&self,
response: OSD,
folders: &mut Option<Vec<InventoryFolder>>,
items: &mut Option<Vec<InventoryItem>>,
links: &mut Option<Vec<InventoryItem>>,
) -> Result<(), Error> {
let parsed = match response {
OSD::Map(map) => self.parse_all_map(&map)?,
_ => return Err(Error::Argument),
};
*folders = Some(parsed.0);
*items = Some(parsed.1);
*links = Some(parsed.2);
Ok(())
}
fn collection_map<'a>(
response: &'a OSD,
plural: &str,
singular: &str,
) -> Option<CollectionSections<'a>> {
let OSD::Map(root) = response else {
return None;
};
let container = match root.get("_embedded") {
Some(OSD::Map(map)) => map,
_ => root,
};
let collection = match container.get(plural) {
Some(OSD::Map(map)) => Some(map),
_ => None,
};
Some((collection, container.get(singular)))
}
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
fn parse_items_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
let Some((collection, singular)) = Self::collection_map(response, "items", "item") else {
return Ok(Vec::new());
};
let mut result = Vec::new();
if let Some(collection) = collection {
for value in collection.values() {
if let OSD::Map(map) = value {
result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
}
}
}
if let Some(OSD::Map(map)) = singular
&& map.contains_key("item_id")
{
result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
}
// GET /item/{id} returns the item itself at the top level.
if let OSD::Map(map) = response
&& map.contains_key("item_id")
{
result.push(InventoryItem::from_osd(response.clone())?);
}
Self::dedup_items(result)
}
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
fn parse_folders_osd(&self, response: &OSD) -> Result<Vec<InventoryFolder>, Error> {
let Some((collection, singular)) = Self::collection_map(response, "categories", "category")
else {
return Ok(Vec::new());
};
let mut result = Vec::new();
if let Some(collection) = collection {
for value in collection.values() {
if let OSD::Map(map) = value {
result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
}
}
}
if let Some(OSD::Map(map)) = singular
&& map.contains_key("category_id")
{
result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
}
Self::dedup_folders(result)
}
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
fn parse_links_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
let Some((collection, _)) = Self::collection_map(response, "links", "link") else {
return Ok(Vec::new());
};
let mut result = Vec::new();
if let Some(collection) = collection {
for value in collection.values() {
let OSD::Map(map) = value else {
return Err(Error::Argument);
};
let mut item = InventoryItem::from_osd(OSD::Map(map.clone()))?;
item.set_asset_type(AssetType::Link);
if let Some(agent) = map.get("agent_id") {
let id = agent.as_uuid()?;
item.base.set_owner_id(id);
item.set_creator_id(id);
item.set_last_owner_id(id);
}
result.push(item);
}
}
Self::dedup_items(result)
}
fn dedup_items(items: Vec<InventoryItem>) -> Result<Vec<InventoryItem>, Error> {
let mut ids = HashSet::new();
if items
.iter()
.any(|item| item.base.uuid() == UUID::zero() || !ids.insert(item.base.uuid()))
{
return Err(Error::Argument);
}
Ok(items)
}
fn dedup_folders(folders: Vec<InventoryFolder>) -> Result<Vec<InventoryFolder>, Error> {
let mut ids = HashSet::new();
if folders
.iter()
.any(|folder| folder.base.uuid() == UUID::zero() || !ids.insert(folder.base.uuid()))
{
return Err(Error::Argument);
}
Ok(folders)
}
pub(crate) fn native_parse_ais_response_meta(
response: Option<OSDMap>,
) -> Result<AISResponseMeta, Error> {
response.map_or_else(
|| Ok(AISResponseMeta::empty()),
|map| Self::parse_meta_map(&map.snapshot()),
)
}
fn parse_meta_map(map: &HashMap<String, OSD>) -> Result<AISResponseMeta, Error> {
let broken = Self::uuid_list(map.get("_broken_links_removed"))?;
let mut removed = Self::uuid_list(map.get("_removed_items"))?;
removed.extend(Self::uuid_list(map.get("_category_items_removed"))?);
let categories = Self::uuid_list(map.get("_categories_removed"))?;
let mut versions = HashMap::new();
if let Some(OSD::Map(values)) = map.get("_updated_category_versions") {
for (key, value) in values {
if let Ok(uuid) = UUID::parse(key.clone())
&& uuid != UUID::zero()
{
versions.insert(uuid, value.as_integer()?);
}
}
}
AISResponseMeta::new(broken, removed, categories, versions)
}
fn uuid_list(value: Option<&OSD>) -> Result<Vec<UUID>, Error> {
let mut values = Vec::new();
match value {
None => {}
Some(OSD::Array(array)) => {
for value in array {
let uuid = value.as_uuid()?;
if uuid != UUID::zero() {
values.push(uuid);
}
}
}
Some(OSD::Map(map)) => {
for key in map.keys() {
if let Ok(uuid) = UUID::parse(key.clone())
&& uuid != UUID::zero()
{
values.push(uuid);
}
}
}
Some(_) => return Err(Error::Argument),
}
Ok(values)
}
}