diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-02-27 20:56:20 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-02-27 20:56:20 +0100 |
| commit | 7930d543a2aa68d4ad2958605827d7eb1baa91f8 (patch) | |
| tree | fe59d1f549e303a96b78d3e925d75abb70b73af0 | |
| parent | c05bfcc2775f0e11db6e856bfcf06d0419c35d54 (diff) | |
| download | jellything-7930d543a2aa68d4ad2958605827d7eb1baa91f8.tar jellything-7930d543a2aa68d4ad2958605827d7eb1baa91f8.tar.bz2 jellything-7930d543a2aa68d4ad2958605827d7eb1baa91f8.tar.zst | |
reimplement Object as slice type
54 files changed, 881 insertions, 995 deletions
diff --git a/common/object/src/buffer.rs b/common/object/src/buffer.rs index 1f8cec6..4d3af34 100644 --- a/common/object/src/buffer.rs +++ b/common/object/src/buffer.rs @@ -4,56 +4,12 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{Object, Tag, TypedTag, ValueStore}; +use crate::{Object, Tag, TypedTag, ValueMarker, value::ValueSer}; use bytemuck::{try_cast_slice, try_cast_vec}; +use core::marker::Sized; use log::trace; use std::borrow::Cow; -#[derive(PartialEq, Eq, Hash)] -pub struct ObjectBuffer(pub Vec<u32>); - -impl ObjectBuffer { - pub fn empty() -> Self { - Self(vec![0]) - } - pub fn as_object<'a>(&'a self) -> Object<'a> { - Object::load(&self.0).unwrap() - } - pub fn to_bytes(self) -> Vec<u8> { - self.0.into_iter().flat_map(u32::to_le_bytes).collect() - } - pub fn new(fields: &mut [(Tag, &dyn ValueStore)]) -> ObjectBuffer { - let mut tags = Vec::new(); - let mut offsets = Vec::new(); - let mut values = Vec::new(); - fields.sort_by_key(|(t, _)| t.0); - let mut temp = Vec::new(); - for (tag, val) in fields { - tags.push(tag.0); - let ty = val.get_type(); - let off = (values.len() as u32) << 5 | (ty as u32) << 2; - if ty.is_aligned() { - offsets.push(off); - val.store_aligned(&mut values); - } else { - temp.clear(); - val.store_unaligned(&mut temp); - let pad = pad_vec(&mut temp); - offsets.push(off | pad); - values.extend(&*slice_u8_to_u32(&temp)); - } - } - ObjectBuffer( - [tags.len() as u32] - .into_iter() - .chain(tags) - .chain(offsets) - .chain(values) - .collect(), - ) - } -} - pub type OBB = ObjectBufferBuilder; #[derive(Default)] @@ -62,10 +18,9 @@ impl ObjectBufferBuilder { pub fn new() -> Self { Self::default() } - pub fn push<T: ValueStore>(&mut self, tag: TypedTag<T>, value: T) { - let ty = value.get_type(); - let tyb = (ty as u32) << 2; - if ty.is_aligned() { + pub fn push<'a, T: ValueMarker<'a> + ?Sized>(&mut self, tag: TypedTag<T>, value: T::Inner) { + let tyb = (T::Inner::TYPE as u32) << 2; + if T::Inner::TYPE.is_aligned() { let mut buf = Vec::new(); value.store_aligned(&mut buf); self.0.push((tag.0, tyb, buf)); @@ -76,11 +31,15 @@ impl ObjectBufferBuilder { self.0.push((tag.0, tyb | pad, vec_u8_to_u32(buf))); } } - pub fn with<T: ValueStore>(mut self, tag: TypedTag<T>, value: T) -> Self { + pub fn with<'a, T: ValueMarker<'a> + ?Sized>( + mut self, + tag: TypedTag<T>, + value: T::Inner, + ) -> Self { self.push(tag, value); self } - pub fn finish(mut self) -> ObjectBuffer { + pub fn finish(mut self) -> Box<Object> { let mut tags = Vec::new(); let mut offsets = Vec::new(); let mut values = Vec::new(); @@ -91,7 +50,7 @@ impl ObjectBufferBuilder { offsets.push(off); values.extend(buf); } - ObjectBuffer( + vec_to_ob( [tags.len() as u32] .into_iter() .chain(tags) @@ -102,10 +61,13 @@ impl ObjectBufferBuilder { } } -impl From<Vec<u8>> for ObjectBuffer { - fn from(value: Vec<u8>) -> Self { - ObjectBuffer(vec_u8_to_u32(value)) - } +pub fn vec_to_ob(v: Vec<u32>) -> Box<Object> { + //? safe way to do this? + unsafe { std::mem::transmute(v.into_boxed_slice()) } +} +pub const fn slice_to_ob<'a>(v: &'a [u32]) -> &'a Object { + //? safe way to do this? + unsafe { std::mem::transmute(v) } } #[inline] diff --git a/common/object/src/debug.rs b/common/object/src/debug.rs index b45d5bf..196da20 100644 --- a/common/object/src/debug.rs +++ b/common/object/src/debug.rs @@ -4,16 +4,10 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{Object, ObjectBuffer, Tag, TypedTag, ValueType}; +use crate::{Object, Tag, TypedTag, ValueType}; use std::{any::type_name, fmt::Debug}; -impl Debug for ObjectBuffer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.as_object().fmt(f) - } -} - -impl Debug for Object<'_> { +impl Debug for Object { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("Object"); let mut nonexhaustive = false; @@ -23,8 +17,8 @@ impl Debug for Object<'_> { let ty = self.offset_type(i); let sz = self.size(i); match (ty, sz) { - (ValueType::String, _) => s.field(k, &self.get_typed::<&str>(i).unwrap()), - (ValueType::Binary, _) => s.field(k, &self.get_typed::<&[u8]>(i).unwrap()), + (ValueType::String, _) => s.field(k, &self.get_typed::<str>(i).unwrap()), + (ValueType::Binary, _) => s.field(k, &self.get_typed::<[u8]>(i).unwrap()), (ValueType::Object, _) => s.field(k, &self.get_typed::<Object>(i).unwrap()), (ValueType::UInt, 4) => s.field(k, &self.get_typed::<u32>(i).unwrap()), (ValueType::UInt, 8) => s.field(k, &self.get_typed::<u64>(i).unwrap()), diff --git a/common/object/src/json.rs b/common/object/src/json.rs index a18ecf5..32b7009 100644 --- a/common/object/src/json.rs +++ b/common/object/src/json.rs @@ -7,7 +7,7 @@ use crate::{Object, Tag, ValueType}; use serde_json::{Map, Value}; -pub fn object_to_json(ob: Object<'_>) -> Value { +pub fn object_to_json(ob: &Object) -> Value { let mut o = Map::new(); let mut nonexhaustive = false; for (i, tag) in ob.keys().enumerate() { @@ -16,7 +16,7 @@ pub fn object_to_json(ob: Object<'_>) -> Value { let ty = ob.offset_type(i); let sz = ob.size(i); let val = match (ty, sz) { - (ValueType::String, _) => ob.get_typed::<&str>(i).unwrap().to_string().into(), + (ValueType::String, _) => ob.get_typed::<str>(i).unwrap().to_string().into(), (ValueType::Object, _) => object_to_json(ob.get_typed::<Object>(i).unwrap()), (ValueType::Tag, 4) => ob.get_typed::<Tag>(i).unwrap().to_string().into(), (ValueType::UInt, 4) => ob.get_typed::<u32>(i).unwrap().into(), diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs index ae8d1cd..783871b 100644 --- a/common/object/src/lib.rs +++ b/common/object/src/lib.rs @@ -3,102 +3,66 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -#![feature(iter_array_chunks, strip_circumfix)] +#![feature(iter_array_chunks, strip_circumfix, phantom_variance_markers)] mod buffer; pub mod debug; #[cfg(feature = "json")] pub mod json; mod path; -mod registry; +pub mod tag; #[cfg(test)] mod tests; mod value; pub use buffer::*; pub use path::*; -pub use registry::*; +pub use tag::*; pub use value::*; -use std::{collections::BTreeSet, fmt::Display, hash::Hash, marker::PhantomData}; +use core::{marker::Sized, todo}; +use std::{collections::BTreeSet, hash::Hash, marker::PhantomData}; +#[derive(Hash, PartialEq, Eq)] #[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Tag(pub u32); +pub struct Object([u32]); -impl Default for Tag { - fn default() -> Self { - Self::new(b"1111") - } -} -impl Tag { - pub const fn new(fourcc: &[u8; 4]) -> Self { - Self(u32::from_le_bytes(*fourcc)) - } -} -impl Display for Tag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(str::from_utf8(&self.0.to_le_bytes()).unwrap()) +impl ToOwned for Object { + type Owned = Box<Object>; + fn to_owned(&self) -> Self::Owned { + vec_to_ob(self.0.to_vec()) } } -#[derive(PartialEq, Eq, PartialOrd, Ord)] -pub struct TypedTag<T>(pub Tag, pub PhantomData<T>); -impl<T> Display for TypedTag<T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} +pub const EMPTY: &'static Object = slice_to_ob(&[0]); -impl<T> Clone for TypedTag<T> { - fn clone(&self) -> Self { - Self(self.0, PhantomData) +impl Object { + #[inline] + fn len(&self) -> usize { + self.0[0] as usize } -} -impl<T> Copy for TypedTag<T> {} - -#[derive(Clone, Copy, Hash, PartialEq, Eq)] -pub struct Object<'a> { - tags: &'a [u32], - offsets: &'a [u32], - values: &'a [u32], -} - -impl<'a> Default for Object<'a> { - fn default() -> Self { - Self::EMPTY + #[inline] + fn tags(&self) -> &[u32] { + &self.0[1..1 + self.len()] } -} -impl<'a> Object<'a> { - pub const EMPTY: Object<'static> = Object { - offsets: &[], - tags: &[], - values: &[], - }; - - pub fn load(buf: &'a [u32]) -> Option<Self> { - let nf = *buf.get(0)? as usize; - if buf.len() < 1 + nf * 2 { - return None; - } - Some(Self { - tags: &buf[1..1 + nf], - offsets: &buf[1 + nf..1 + nf + nf], - values: &buf[1 + nf + nf..], - }) + #[inline] + fn offsets(&self) -> &[u32] { + let nf = self.len(); + &self.0[1 + nf..1 + nf + nf] } - pub fn dump(&self) -> ObjectBuffer { - let mut out = Vec::new(); - out.push(self.tags.len() as u32); - out.extend(self.tags); - out.extend(self.offsets); - out.extend(self.values); - ObjectBuffer(out) + #[inline] + fn values(&self) -> &[u32] { + let nf = self.len(); + &self.0[1 + nf + nf..] } + pub fn export(&self) -> &[u32] { + &self.0 + } + pub fn find_field(&self, tag: Tag) -> Option<usize> { // using partition as binary search for the first field (instead of regular binary_search that returns any) - let first = self.tags.partition_point(|&x| x < tag.0); - self.tags + let first = self.tags().partition_point(|&x| x < tag.0); + self.tags() .get(first) .is_some_and(|&x| x == tag.0) .then_some(first) @@ -108,94 +72,97 @@ impl<'a> Object<'a> { } #[inline] fn offset(&self, i: usize) -> usize { - self.offsets + self.offsets() .get(i) .map(|&v| v >> 5) - .unwrap_or(self.values.len() as u32) as usize + .unwrap_or(self.values().len() as u32) as usize } fn offset_type(&self, i: usize) -> ValueType { - let raw = (self.offsets[i] >> 2) & 0b111; + let raw = (self.offsets()[i] >> 2) & 0b111; ValueType::from_num(raw) } fn size(&self, i: usize) -> u32 { - let start_raw = self.offsets[i]; + let start_raw = self.offsets()[i]; let end_raw = self - .offsets + .offsets() .get(i + 1) .copied() - .unwrap_or((self.values.len() as u32) << 5); + .unwrap_or((self.values().len() as u32) << 5); let u32_len = (end_raw >> 5) - (start_raw >> 5); let padding = start_raw & 0b11; u32_len * 4 - padding } - fn get_aligned(&self, index: usize) -> Option<&'a [u32]> { - let start_raw = self.offsets[index]; + fn get_aligned<'a>(&'a self, index: usize) -> Option<&'a [u32]> { + let start_raw = self.offsets()[index]; let end_raw = self - .offsets + .offsets() .get(index + 1) .copied() - .unwrap_or((self.values.len() as u32) << 5); + .unwrap_or((self.values().len() as u32) << 5); let start = start_raw >> 5; let end = end_raw >> 5; - Some(&self.values[start as usize..end as usize]) + Some(&self.values()[start as usize..end as usize]) } - fn get_unaligned(&self, index: usize) -> Option<&'a [u8]> { - let start_raw = self.offsets[index]; + fn get_unaligned<'a>(&'a self, index: usize) -> Option<&'a [u8]> { + let start_raw = self.offsets()[index]; let end_raw = self - .offsets + .offsets() .get(index + 1) .copied() - .unwrap_or((self.values.len() as u32) << 5); + .unwrap_or((self.values().len() as u32) << 5); let start = (start_raw >> 5) * 4; let padding = start_raw & 0b11; let end = (end_raw >> 5) * 4 - padding; - let values_u8: &[u8] = bytemuck::cast_slice(self.values); + let values_u8: &[u8] = bytemuck::cast_slice(self.values()); Some(&values_u8[start as usize..end as usize]) } #[inline] - pub fn get_typed<T: ValueLoad<'a>>(&self, index: usize) -> Option<T> { - if T::TYPE.is_aligned() { - T::load_aligned(self.get_aligned(index)?) + pub fn get_typed<'a, T: ValueMarker<'a> + ?Sized>(&'a self, index: usize) -> Option<T::Inner> { + if T::Inner::TYPE.is_aligned() { + T::Inner::load_aligned(self.get_aligned(index)?) } else { - T::load_unaligned(self.get_unaligned(index)?) + T::Inner::load_unaligned(self.get_unaligned(index)?) } } - pub fn get<T: ValueLoad<'a>>(&self, tag: TypedTag<T>) -> Option<T> { - self.get_typed(self.find_field(tag.0)?) + pub fn get<'a, T: ValueMarker<'a> + ?Sized>(&'a self, tag: TypedTag<T>) -> Option<T::Inner> { + self.get_typed::<T>(self.find_field(tag.0)?) } - pub fn keys(&self) -> KeysIter<'a> { + pub fn keys<'a>(&'a self) -> KeysIter<'a> { KeysIter { - object: *self, + object: self, index: 0, } } - pub fn entries<T>(&self) -> EntriesIter<'a, T> { + pub fn entries<'a, T: ?Sized>(&'a self) -> EntriesIter<'a, T> { EntriesIter { - object: *self, + object: self, index: 0, ty: PhantomData, } } - pub fn iter<T>(&self, tag: TypedTag<T>) -> FieldIter<'a, T> { + pub fn iter<'a, T: ?Sized>(&'a self, tag: TypedTag<T>) -> FieldIter<'a, T> { FieldIter { - object: *self, - index: self.tags.partition_point(|&x| x < tag.0.0), + object: self, + index: self.tags().partition_point(|&x| x < tag.0.0), tag: tag.0.0, ty: PhantomData, } } #[must_use] - pub fn extend<T: ValueLoad<'a> + Eq + Ord>( - &self, + pub fn extend<'a, T: ValueMarker<'a> + ?Sized>( + &'a self, tag: TypedTag<T>, - values: impl IntoIterator<Item = T>, - ) -> ObjectBuffer { + values: impl IntoIterator<Item = T::Inner>, + ) -> Box<Object> + where + T::Inner: Eq + Ord, + { self.insert_multi( tag, &self @@ -209,71 +176,75 @@ impl<'a> Object<'a> { #[must_use] pub fn extend_object( &self, - tag: TypedTag<Object<'static>>, + tag: TypedTag<Object>, ident: Tag, - values: impl IntoIterator<Item = ObjectBuffer>, - ) -> ObjectBuffer { - let ident = TypedTag(ident, PhantomData::<&[u8]>); + values: &[Box<Object>], + ) -> Box<Object> { + let ident = TypedTag::<[u8]>::new(ident); let mut new_vals = Vec::new(); for ob in self.iter(tag) { - new_vals.push(ob.dump()); + new_vals.push(ob); } let mut any_new = false; for val in values { - if new_vals - .iter() - .all(|rhs| rhs.as_object().get(ident) != val.as_object().get(ident)) - { + if new_vals.iter().all(|rhs| rhs.get(ident) != val.get(ident)) { any_new = true; - new_vals.push(val); + new_vals.push(&val); } } if any_new { - self.insert_multi(TypedTag(tag.0, PhantomData), &new_vals) + self.insert_multi(TypedTag::<Object>::new(tag.0), &new_vals) } else { - self.dump() + todo!() } } #[must_use] #[inline] - pub fn insert<T: ValueStore>(&self, tag: TypedTag<T>, value: T) -> ObjectBuffer { + pub fn insert<'a, T: ValueMarker<'a> + ?Sized>( + &'a self, + tag: TypedTag<T>, + value: T::Inner, + ) -> Box<Object> { self.insert_multi(tag, &[value]) } #[must_use] #[inline] - pub fn remove<T: ValueStore>(&self, tag: TypedTag<T>) -> ObjectBuffer { + pub fn remove<'a, T: ValueMarker<'a>>(&'a self, tag: TypedTag<T>) -> Box<Object> { self.insert_multi(tag, &[]) } #[must_use] - pub fn insert_multi<T: ValueStore>(&self, tag: TypedTag<T>, values: &[T]) -> ObjectBuffer { - let prefix = self.tags.partition_point(|&x| x < tag.0.0); - let suffix = self.tags.partition_point(|&x| x <= tag.0.0); + pub fn insert_multi<'a, T: ValueMarker<'a> + ?Sized>( + &'a self, + tag: TypedTag<T>, + values: &[T::Inner], + ) -> Box<Object> { + let prefix = self.tags().partition_point(|&x| x < tag.0.0); + let suffix = self.tags().partition_point(|&x| x <= tag.0.0); let values_prefix = self.offset(prefix); let values_suffix = self.offset(suffix); let mut buf = Vec::new(); let cut_size = suffix - prefix; - buf.push((self.tags.len() - cut_size + values.len()) as u32); + buf.push((self.tags().len() - cut_size + values.len()) as u32); - buf.extend(&self.tags[..prefix]); + buf.extend(&self.tags()[..prefix]); buf.extend(values.iter().map(|_| tag.0.0)); - buf.extend(&self.tags[suffix..]); + buf.extend(&self.tags()[suffix..]); - buf.extend(&self.offsets[..prefix]); + buf.extend(&self.offsets()[..prefix]); let new_offs = buf.len(); buf.extend(values.iter().map(|_| 0)); // placeholder let suffix_offs = buf.len(); - buf.extend(&self.offsets[suffix..]); // need offsetting later + buf.extend(&self.offsets()[suffix..]); // need offsetting later let suffix_end = buf.len(); let values_start = buf.len() as u32; - buf.extend(&self.values[..values_prefix]); + buf.extend(&self.values()[..values_prefix]); let mut temp = Vec::new(); let values_new = buf.len(); for (i, val) in values.iter().enumerate() { - let ty = val.get_type(); - let off = (buf.len() as u32 - values_start) << 5 | (ty as u32) << 2; - if ty.is_aligned() { + let off = (buf.len() as u32 - values_start) << 5 | (T::Inner::TYPE as u32) << 2; + if T::Inner::TYPE.is_aligned() { buf[new_offs + i] = off; val.store_aligned(&mut buf); } else { @@ -292,77 +263,78 @@ impl<'a> Object<'a> { .iter_mut() .for_each(|e| *e = e.strict_add_signed(suffix_offset)); } - buf.extend(&self.values[values_suffix..]); + buf.extend(&self.values()[values_suffix..]); - ObjectBuffer(buf) + vec_to_ob(buf) } #[must_use] pub fn update( &self, - tag: TypedTag<Object<'static>>, - update: impl FnOnce(Object<'a>) -> ObjectBuffer, - ) -> ObjectBuffer { - self.insert(tag, update(self.get(tag).unwrap_or_default()).as_object()) + tag: TypedTag<Object>, + update: impl FnOnce(&Object) -> Box<Object>, + ) -> Box<Object> { + let ob = update(self.get(tag).unwrap_or(EMPTY)); + self.insert(tag, &ob) } } pub struct KeysIter<'a> { - object: Object<'a>, + object: &'a Object, index: usize, } impl Iterator for KeysIter<'_> { type Item = Tag; fn next(&mut self) -> Option<Self::Item> { - if self.index >= self.object.tags.len() { + if self.index >= self.object.tags().len() { return None; } else { self.index += 1; - Some(Tag(self.object.tags[self.index - 1])) + Some(Tag(self.object.tags()[self.index - 1])) } } } -pub struct EntriesIter<'a, T> { - object: Object<'a>, +pub struct EntriesIter<'a, T: ?Sized> { + object: &'a Object, index: usize, ty: PhantomData<T>, } -impl<'a, T: ValueLoad<'a>> Iterator for EntriesIter<'a, T> { - type Item = (Tag, T); +impl<'a, T: ValueMarker<'a> + ?Sized> Iterator for EntriesIter<'a, T> { + type Item = (Tag, T::Inner); fn next(&mut self) -> Option<Self::Item> { loop { - if self.index >= self.object.tags.len() { + if self.index >= self.object.tags().len() { return None; } - if T::TYPE != self.object.offset_type(self.index) { + if T::Inner::TYPE != self.object.offset_type(self.index) { self.index += 1; continue; } - let value = self.object.get_typed(self.index)?; - let tag = self.object.tags[self.index]; + let value = self.object.get_typed::<T>(self.index)?; + let tag = self.object.tags()[self.index]; self.index += 1; return Some((Tag(tag), value)); } } } -pub struct FieldIter<'a, T> { - object: Object<'a>, +pub struct FieldIter<'a, T: ?Sized> { + object: &'a Object, index: usize, tag: u32, ty: PhantomData<T>, } -impl<'a, T: ValueLoad<'a>> Iterator for FieldIter<'a, T> { - type Item = T; +impl<'a, T: ValueMarker<'a> + ?Sized> Iterator for FieldIter<'a, T> { + type Item = T::Inner; fn next(&mut self) -> Option<Self::Item> { - if self.index >= self.object.tags.len() { + if self.index >= self.object.tags().len() { return None; } - if self.object.tags[self.index] != self.tag { + if self.object.tags()[self.index] != self.tag { return None; } - let val = self.object.get_typed(self.index); + let val = self.object.get_typed::<T>(self.index); self.index += 1; val } diff --git a/common/object/src/path.rs b/common/object/src/path.rs index 793400c..83b2c43 100644 --- a/common/object/src/path.rs +++ b/common/object/src/path.rs @@ -5,30 +5,30 @@ */ use crate::{Object, Tag, TypedTag}; -use std::{fmt::Display, marker::PhantomData, str::FromStr}; +use std::{fmt::Display, str::FromStr}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Path(pub Vec<Tag>); impl Path { - pub fn get_matching_value<'a>(&self, ob: Object<'a>) -> Option<&'a [u8]> { - fn recurse<'a>(ob: Object<'a>, path: &[Tag]) -> Option<&'a [u8]> { + pub fn get_matching_value<'a>(&self, ob: &'a Object) -> Option<&'a [u8]> { + fn recurse<'a>(ob: &'a Object, path: &[Tag]) -> Option<&'a [u8]> { if path.len() > 1 { - recurse(ob.get(TypedTag(path[0], PhantomData))?, &path[1..]) + recurse(ob.get(TypedTag::<Object>::new(path[0]))?, &path[1..]) } else { - ob.get(TypedTag(path[0], PhantomData)) + ob.get(TypedTag::<[u8]>::new(path[0])) } } recurse(ob, &self.0) } - pub fn get_matching_values<'a>(&self, ob: Object<'a>) -> Vec<&'a [u8]> { - fn recurse<'a>(ob: Object<'a>, out: &mut Vec<&'a [u8]>, path: &[Tag]) { + pub fn get_matching_values<'a>(&self, ob: &'a Object) -> Vec<&'a [u8]> { + fn recurse<'a>(ob: &'a Object, out: &mut Vec<&'a [u8]>, path: &[Tag]) { if path.len() > 1 { - for nested in ob.iter(TypedTag(path[0], PhantomData::<Object>)) { + for nested in ob.iter(TypedTag::<Object>::new(path[0])) { recurse(nested, out, &path[1..]); } } else { - out.extend(ob.iter(TypedTag(path[0], PhantomData::<&[u8]>))); + out.extend(ob.iter(TypedTag::<[u8]>::new(path[0]))); } } let mut out = Vec::new(); diff --git a/common/object/src/registry.rs b/common/object/src/registry.rs deleted file mode 100644 index af028e8..0000000 --- a/common/object/src/registry.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ - -use std::any::TypeId; - -pub mod types { - use crate::Object; - use std::any::TypeId; - - pub const OBJECT: TypeId = TypeId::of::<Object>(); - pub const STR: TypeId = TypeId::of::<&str>(); - pub const BINARY: TypeId = TypeId::of::<&[u8]>(); - pub const U32: TypeId = TypeId::of::<u32>(); - pub const U64: TypeId = TypeId::of::<u64>(); - pub const I64: TypeId = TypeId::of::<i64>(); - pub const F64: TypeId = TypeId::of::<f64>(); -} - -#[derive(Clone)] -pub struct TagInfo { - pub name: &'static str, - pub r#type: Option<TypeId>, -} - -#[macro_export] -macro_rules! fields { - ($($id:ident: $type:ty = $tag:literal;)*) => { - $(pub const $id: $crate::TypedTag<$type> = $crate::TypedTag($crate::Tag::new($tag), std::marker::PhantomData);)* - }; -} -#[macro_export] -macro_rules! enums { - ($($id:ident = $tag:literal;)*) => { - $(pub const $id: $crate::Tag = $crate::Tag::new($tag);)* - }; -} diff --git a/common/object/src/tag.rs b/common/object/src/tag.rs new file mode 100644 index 0000000..c09af27 --- /dev/null +++ b/common/object/src/tag.rs @@ -0,0 +1,73 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use core::marker::{PhantomData, Sized}; +use std::fmt::Display; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Tag(pub u32); + +impl Default for Tag { + fn default() -> Self { + Self::new(b"1111") + } +} +impl Tag { + pub const fn new(fourcc: &[u8; 4]) -> Self { + Self(u32::from_le_bytes(*fourcc)) + } +} +impl Display for Tag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(str::from_utf8(&self.0.to_le_bytes()).unwrap()) + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct TypedTag<T: ?Sized>(pub Tag, pub PhantomData<T>); +impl<T: ?Sized> Display for TypedTag<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} +impl<T: ?Sized> Clone for TypedTag<T> { + fn clone(&self) -> Self { + Self(self.0, PhantomData) + } +} +impl<T: ?Sized> Copy for TypedTag<T> {} +impl<T: ?Sized> TypedTag<T> { + pub const fn new(tag: Tag) -> Self { + Self(tag, PhantomData) + } +} + +pub mod types { + use crate::Object; + use std::any::TypeId; + + pub const OBJECT: TypeId = TypeId::of::<Object>(); + pub const STR: TypeId = TypeId::of::<&str>(); + pub const BINARY: TypeId = TypeId::of::<&[u8]>(); + pub const U32: TypeId = TypeId::of::<u32>(); + pub const U64: TypeId = TypeId::of::<u64>(); + pub const I64: TypeId = TypeId::of::<i64>(); + pub const F64: TypeId = TypeId::of::<f64>(); +} + +#[macro_export] +macro_rules! fields { + ($($id:ident: $type:ty = $tag:literal;)*) => { + $(pub const $id: $crate::TypedTag<$type> = $crate::TypedTag::new($crate::Tag::new($tag));)* + }; +} +#[macro_export] +macro_rules! enums { + ($($id:ident = $tag:literal;)*) => { + $(pub const $id: $crate::Tag = $crate::Tag::new($tag);)* + }; +} diff --git a/common/object/src/tests.rs b/common/object/src/tests.rs index 73f7b6f..b6455f5 100644 --- a/common/object/src/tests.rs +++ b/common/object/src/tests.rs @@ -4,28 +4,27 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{Object, ObjectBuffer, fields}; +use crate::{EMPTY, OBB, Object, fields}; fields! { - NAME: &str = b"name"; + NAME: str = b"name"; AGE: u32 = b"age1"; - FRIEND: &str = b"frnd"; + FRIEND: str = b"frnd"; STUFF: Object = b"stff"; } -fn test_object() -> ObjectBuffer { - ObjectBuffer::new(&mut [ - (NAME.0, &"Bob"), - (AGE.0, &35_u32), - (FRIEND.0, &"Alice"), - (FRIEND.0, &"Charlie"), - ]) +fn test_object() -> Box<Object> { + OBB::new() + .with(NAME, "Bob") + .with(AGE, 35) + .with(FRIEND, "Alice") + .with(FRIEND, "Charlie") + .finish() } #[test] fn read_single_field() { let bob = test_object(); - let bob = bob.as_object(); assert_eq!(bob.get(NAME), Some("Bob")); assert_eq!(bob.get(AGE), Some(35)); } @@ -33,7 +32,6 @@ fn read_single_field() { #[test] fn read_multi_field() { let bob = test_object(); - let bob = bob.as_object(); let mut friends = bob.iter(FRIEND); assert_eq!(friends.next(), Some("Alice")); @@ -56,10 +54,10 @@ fn vec_align_test() { #[test] fn insert() { let bob = test_object(); - let edward = bob.as_object().insert(NAME, "Edward"); + let edward = bob.insert(NAME, "Edward"); - eprintln!("{:#?}", bob.as_object()); - let edward = edward.as_object(); + eprintln!("{:#?}", bob); + let edward = edward; eprintln!("{edward:#?}"); assert_eq!(edward.get(NAME), Some("Edward")); assert_eq!(edward.get(AGE), Some(35)); @@ -72,11 +70,11 @@ fn insert() { #[test] fn insert_empty() { - let ob = Object::EMPTY.insert(NAME, "Romeo"); - assert_eq!(ob.as_object().get(NAME), Some("Romeo")); + let ob = EMPTY.insert(NAME, "Romeo"); + assert_eq!(ob.get(NAME), Some("Romeo")); - let ob = Object::EMPTY.insert(STUFF, Object::EMPTY); - assert_eq!(ob.as_object().get(STUFF), Some(Object::EMPTY)); + let ob = EMPTY.insert(STUFF, EMPTY); + assert_eq!(ob.get(STUFF), Some(EMPTY)); } #[test] diff --git a/common/object/src/value.rs b/common/object/src/value.rs index 9b9a438..0a1ceb9 100644 --- a/common/object/src/value.rs +++ b/common/object/src/value.rs @@ -4,19 +4,58 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{Object, ObjectBuffer, Tag}; +use crate::{Object, Tag, slice_to_ob}; +use core::{marker::Sized, unimplemented}; use std::{borrow::Cow, fmt::Display, str::FromStr}; -pub trait ValueLoad<'a>: ValueStore + Sized { +pub trait ValueMarker<'a> { + type Inner: ValueSer<'a>; +} + +impl<'a> ValueMarker<'a> for str { + type Inner = &'a str; +} +impl<'a> ValueMarker<'a> for Object { + type Inner = &'a Object; +} +impl<'a> ValueMarker<'a> for [u8] { + type Inner = &'a [u8]; +} + +impl<'a> ValueMarker<'a> for Tag { + type Inner = Tag; +} +impl<'a> ValueMarker<'a> for u32 { + type Inner = u32; +} +impl<'a> ValueMarker<'a> for i64 { + type Inner = i64; +} +impl<'a> ValueMarker<'a> for u64 { + type Inner = u64; +} +impl<'a> ValueMarker<'a> for () { + type Inner = (); +} +impl<'a> ValueMarker<'a> for f64 { + type Inner = f64; +} + +pub trait ValueSer<'a>: Sized { const TYPE: ValueType; - fn load_aligned(buf: &'a [u32]) -> Option<Self> { - let _ = buf; + fn load_aligned(_buf: &'a [u32]) -> Option<Self> { unimplemented!() } - fn load_unaligned(buf: &'a [u8]) -> Option<Self> { - let _ = buf; + fn load_unaligned(_buf: &'a [u8]) -> Option<Self> { + unimplemented!() + } + fn store_aligned(&self, _buf: &mut Vec<u32>) { unimplemented!() } + fn store_unaligned(&self, _buf: &mut Vec<u8>) { + unimplemented!() + } + fn size(&self) -> usize; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -126,22 +165,11 @@ impl FromStr for Value<'static> { } } -pub trait ValueStore { - fn get_type(&self) -> ValueType; - fn store_aligned(&self, _buf: &mut Vec<u32>) {} - fn store_unaligned(&self, _buf: &mut Vec<u8>) {} - fn size(&self) -> usize; -} -impl<'a> ValueLoad<'a> for &'a str { +impl<'a> ValueSer<'a> for &'a str { const TYPE: ValueType = ValueType::String; fn load_unaligned(buf: &'a [u8]) -> Option<Self> { str::from_utf8(buf).ok() } -} -impl ValueStore for &str { - fn get_type(&self) -> ValueType { - ValueType::String - } fn store_unaligned(&self, buf: &mut Vec<u8>) { buf.extend(self.as_bytes()); } @@ -149,16 +177,11 @@ impl ValueStore for &str { self.len() } } -impl ValueLoad<'_> for u32 { +impl ValueSer<'_> for u32 { const TYPE: ValueType = ValueType::UInt; fn load_aligned(buf: &[u32]) -> Option<Self> { buf.get(0).copied().map(u32::from_be) } -} -impl ValueStore for u32 { - fn get_type(&self) -> ValueType { - ValueType::UInt - } fn store_aligned(&self, buf: &mut Vec<u32>) { buf.push(self.to_be()); } @@ -166,16 +189,11 @@ impl ValueStore for u32 { 4 } } -impl ValueLoad<'_> for Tag { +impl ValueSer<'_> for Tag { const TYPE: ValueType = ValueType::Tag; fn load_aligned(buf: &[u32]) -> Option<Self> { buf.get(0).copied().map(u32::from_be).map(Tag) } -} -impl ValueStore for Tag { - fn get_type(&self) -> ValueType { - ValueType::Tag - } fn store_aligned(&self, buf: &mut Vec<u32>) { buf.push(self.0.to_be()); } @@ -183,18 +201,13 @@ impl ValueStore for Tag { 4 } } -impl ValueLoad<'_> for u64 { +impl ValueSer<'_> for u64 { const TYPE: ValueType = ValueType::UInt; fn load_aligned(buf: &[u32]) -> Option<Self> { let hi = u32::from_be(*buf.get(0)?) as u64; let lo = u32::from_be(*buf.get(1)?) as u64; Some(hi << 32 | lo) } -} -impl ValueStore for u64 { - fn get_type(&self) -> ValueType { - ValueType::UInt - } fn store_aligned(&self, buf: &mut Vec<u32>) { buf.push(((self >> 32) as u32).to_be()); buf.push((*self as u32).to_be()); @@ -203,7 +216,7 @@ impl ValueStore for u64 { 8 } } -impl ValueLoad<'_> for f64 { +impl ValueSer<'_> for f64 { const TYPE: ValueType = ValueType::Float; fn load_aligned(buf: &[u32]) -> Option<Self> { if buf.len() < 2 { @@ -215,11 +228,6 @@ impl ValueLoad<'_> for f64 { a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3], ])) } -} -impl ValueStore for f64 { - fn get_type(&self) -> ValueType { - ValueType::Float - } fn store_aligned(&self, buf: &mut Vec<u32>) { let b = self.to_be_bytes(); buf.push(u32::from_be_bytes([b[0], b[1], b[2], b[3]])); @@ -229,16 +237,11 @@ impl ValueStore for f64 { 8 } } -impl ValueLoad<'_> for i64 { +impl ValueSer<'_> for i64 { const TYPE: ValueType = ValueType::Int; fn load_aligned(buf: &[u32]) -> Option<Self> { u64::load_aligned(buf).map(|x| x as i64) } -} -impl ValueStore for i64 { - fn get_type(&self) -> ValueType { - ValueType::Int - } fn store_aligned(&self, buf: &mut Vec<u32>) { (*self as u64).store_aligned(buf); } @@ -246,47 +249,24 @@ impl ValueStore for i64 { 8 } } -impl<'a> ValueLoad<'a> for Object<'a> { +impl<'a> ValueSer<'a> for &'a Object { const TYPE: ValueType = ValueType::Object; fn load_aligned(buf: &'a [u32]) -> Option<Self> { - Self::load(buf) - } -} -impl ValueStore for Object<'_> { - fn get_type(&self) -> ValueType { - ValueType::Object - } - fn store_aligned(&self, buf: &mut Vec<u32>) { - buf.push(self.tags.len() as u32); - buf.extend(self.tags); - buf.extend(self.offsets); - buf.extend(self.values); - } - fn size(&self) -> usize { - (self.tags.len() + self.offsets.len() + self.values.len()) * size_of::<u32>() - } -} -impl ValueStore for ObjectBuffer { - fn get_type(&self) -> ValueType { - ValueType::Object + // TODO validate + Some(slice_to_ob(buf)) } fn store_aligned(&self, buf: &mut Vec<u32>) { buf.extend(&self.0); } fn size(&self) -> usize { - self.0.len() * 4 + self.0.len() * size_of::<u32>() } } -impl<'a> ValueLoad<'a> for &'a [u8] { +impl<'a> ValueSer<'a> for &'a [u8] { const TYPE: ValueType = ValueType::Binary; fn load_unaligned(buf: &'a [u8]) -> Option<Self> { Some(buf) } -} -impl ValueStore for &[u8] { - fn get_type(&self) -> ValueType { - ValueType::Binary - } fn store_unaligned(&self, buf: &mut Vec<u8>) { buf.extend(*self); } @@ -294,16 +274,11 @@ impl ValueStore for &[u8] { self.len() } } -impl<'a> ValueLoad<'a> for () { +impl<'a> ValueSer<'a> for () { const TYPE: ValueType = ValueType::Int; fn load_aligned(_buf: &'a [u32]) -> Option<Self> { Some(()) } -} -impl ValueStore for () { - fn get_type(&self) -> ValueType { - ValueType::Binary - } fn store_aligned(&self, _buf: &mut Vec<u32>) {} fn size(&self) -> usize { 0 diff --git a/common/src/api.rs b/common/src/api.rs index d9f27c5..bf79cc0 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -5,12 +5,13 @@ */ use jellyobject::{Object, Tag}; +use std::borrow::Cow; use std::collections::BTreeMap; -#[derive(Debug, Clone, Copy)] +#[derive(Debug)] pub struct Nku<'a> { - pub node: Object<'a>, - pub userdata: Object<'a>, + pub node: Cow<'a, Object>, + pub userdata: Cow<'a, Object>, pub role: Option<&'a str>, } diff --git a/common/src/internal.rs b/common/src/internal.rs index 5aedfc7..844468d 100644 --- a/common/src/internal.rs +++ b/common/src/internal.rs @@ -10,7 +10,7 @@ use jellyobject::fields; use serde::{Deserialize, Serialize}; fields! { - IM_PATH: &str = b"Ipth"; + IM_PATH: str = b"Ipth"; IM_MTIME: u64 = b"Imtm"; } diff --git a/common/src/lib.rs b/common/src/lib.rs index d94c72c..417acee 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,6 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +#![feature(phantom_variance_markers)] pub mod api; pub mod internal; pub mod node; diff --git a/common/src/node.rs b/common/src/node.rs index 92b5a5b..ff3289f 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -10,51 +10,51 @@ fields! { // Tag counter: 111 NO_KIND: Tag = b"kind"; - NO_TITLE: &str = b"titl"; + NO_TITLE: str = b"titl"; NO_PARENT: u64 = b"prnt"; // multi - NO_SUBTITLE: &str = b"sbtl"; - NO_TAGLINE: &str = b"tgln"; - NO_DESCRIPTION: &str = b"desc"; + NO_SUBTITLE: str = b"sbtl"; + NO_TAGLINE: str = b"tgln"; + NO_DESCRIPTION: str = b"desc"; NO_RELEASEDATE: i64 = b"rldt"; NO_DURATION: f64 = b"durn"; NO_INDEX: u64 = b"indx"; NO_SEASON_INDEX: u64 = b"sidx"; NO_TRACK: Object = b"trak"; // multi NO_CHAPTER: Object = b"chpt"; // multi - NO_TAG: &str = b"tag1"; // multi + NO_TAG: str = b"tag1"; // multi NO_RATINGS: Object = b"rtng"; NO_PICTURES: Object = b"pict"; NO_IDENTIFIERS: Object = b"iden"; NO_VISIBILITY: Tag = b"visi"; NO_STORAGE_SIZE: u64 = b"stsz"; NO_CREDIT: Object = b"crdt"; // multi - NO_SLUG: &str = b"slug"; + NO_SLUG: str = b"slug"; NO_METASOURCE: Object = b"msrc"; CR_NODE: u64 = b"node"; CR_KIND: Tag = b"kind"; - CR_ROLE: &str = b"role"; // multi + CR_ROLE: str = b"role"; // multi TR_KIND: Tag = b"kind"; TR_SOURCE: Object = b"sour"; - TR_NAME: &str = b"name"; - TR_CODEC: &str = b"codc"; - TR_LANGUAGE: &str = b"lang"; // BCP 47 + TR_NAME: str = b"name"; + TR_CODEC: str = b"codc"; + TR_LANGUAGE: str = b"lang"; // BCP 47 TR_RATE: f64 = b"rate"; TR_BIT_DEPTH: u32 = b"bdep"; TR_CHANNELS: u32 = b"chnl"; TR_PIXEL_WIDTH: u32 = b"pwid"; TR_PIXEL_HEIGHT: u32 = b"phei"; - TRSOURCE_LOCAL_PATH: &str = b"lpat"; + TRSOURCE_LOCAL_PATH: str = b"lpat"; TRSOURCE_LOCAL_TRACKNUM: u64 = b"ltrn"; CH_START: f64 = b"strt"; CH_END: f64 = b"end1"; - CH_NAME: &str = b"name"; + CH_NAME: str = b"name"; - PICT_COVER: &str = b"covr"; - PICT_BACKDROP: &str = b"bdro"; + PICT_COVER: str = b"covr"; + PICT_BACKDROP: str = b"bdro"; RTYP_IMDB: f64 = b"imdb"; RTYP_TMDB: f64 = b"tmdb"; @@ -65,31 +65,31 @@ fields! { RTYP_YOUTUBE_SUBSCRIBERS: f64 = b"ytsu"; RTYP_TRAKT: f64 = b"trkt"; - IDENT_MUSICBRAINZ_RECORDING: &str = b"mbrc"; - IDENT_MUSICBRAINZ_ARTIST: &str = b"mbar"; - IDENT_MUSICBRAINZ_RELEASE: &str = b"mbrl"; - IDENT_MUSICBRAINZ_RELEASE_GROUP: &str = b"mbrg"; - IDENT_ACOUST_ID_TRACK: &str = b"actr"; - IDENT_YOUTUBE_VIDEO: &str = b"ytvi"; - IDENT_YOUTUBE_CHANNEL: &str = b"ytc1"; - IDENT_YOUTUBE_CHANNEL_HANDLE: &str = b"ytch"; - IDENT_BANDCAMP: &str = b"bcmp"; - IDENT_ISRC: &str = b"isrc"; - IDENT_BARCODE: &str = b"barc"; - IDENT_TRAKT_MOVIE: &str = b"trmv"; - IDENT_TRAKT_SHOW: &str = b"trsh"; - IDENT_TRAKT_SEASON: &str = b"trse"; - IDENT_TRAKT_EPISODE: &str = b"trep"; - IDENT_TRAKT_PERSON: &str = b"trpe"; - IDENT_IMDB: &str = b"imdb"; - IDENT_IMDB_PERSON: &str = b"impe"; - IDENT_TMDB_SERIES: &str = b"tmse"; - IDENT_TMDB_MOVIE: &str = b"tmmv"; - IDENT_TMDB_PERSON: &str = b"tmpe"; - IDENT_TVDB: &str = b"tvdb"; - IDENT_OMDB: &str = b"omdb"; - IDENT_VGMDB_ARTIST: &str = b"vgar"; - IDENT_WIKIDATA: &str = b"wkdt"; + IDENT_MUSICBRAINZ_RECORDING: str = b"mbrc"; + IDENT_MUSICBRAINZ_ARTIST: str = b"mbar"; + IDENT_MUSICBRAINZ_RELEASE: str = b"mbrl"; + IDENT_MUSICBRAINZ_RELEASE_GROUP: str = b"mbrg"; + IDENT_ACOUST_ID_TRACK: str = b"actr"; + IDENT_YOUTUBE_VIDEO: str = b"ytvi"; + IDENT_YOUTUBE_CHANNEL: str = b"ytc1"; + IDENT_YOUTUBE_CHANNEL_HANDLE: str = b"ytch"; + IDENT_BANDCAMP: str = b"bcmp"; + IDENT_ISRC: str = b"isrc"; + IDENT_BARCODE: str = b"barc"; + IDENT_TRAKT_MOVIE: str = b"trmv"; + IDENT_TRAKT_SHOW: str = b"trsh"; + IDENT_TRAKT_SEASON: str = b"trse"; + IDENT_TRAKT_EPISODE: str = b"trep"; + IDENT_TRAKT_PERSON: str = b"trpe"; + IDENT_IMDB: str = b"imdb"; + IDENT_IMDB_PERSON: str = b"impe"; + IDENT_TMDB_SERIES: str = b"tmse"; + IDENT_TMDB_MOVIE: str = b"tmmv"; + IDENT_TMDB_PERSON: str = b"tmpe"; + IDENT_TVDB: str = b"tvdb"; + IDENT_OMDB: str = b"omdb"; + IDENT_VGMDB_ARTIST: str = b"vgar"; + IDENT_WIKIDATA: str = b"wkdt"; } diff --git a/common/src/user.rs b/common/src/user.rs index 6511b10..b83cc9c 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -6,13 +6,13 @@ use jellyobject::{Object, Tag, enums, fields}; -pub type User<'a> = Object<'a>; +pub type User = Object; fields! { - USER_LOGIN: &str = b"Ulgn"; - USER_PASSWORD: &[u8] = b"Upwd"; + USER_LOGIN: str = b"Ulgn"; + USER_PASSWORD: [u8] = b"Upwd"; USER_PASSWORD_REQUIRE_CHANGE: () = b"Upwc"; - USER_NAME: &str = b"Unam"; + USER_NAME: str = b"Unam"; USER_ADMIN: () = b"Uadm"; USER_THEME_PRESET: Tag = b"Utpr"; USER_THEME_ACCENT: u32 = b"Utac"; diff --git a/database/src/helper.rs b/database/src/helper.rs index 28ab1fa..d1fc29a 100644 --- a/database/src/helper.rs +++ b/database/src/helper.rs @@ -20,4 +20,4 @@ pub trait DatabaseReturnExt: Database { Ok(ret.unwrap()) } } -impl<D: Database> DatabaseReturnExt for D {} +impl<D: Database + ?Sized> DatabaseReturnExt for D {} diff --git a/database/src/kv/binning.rs b/database/src/kv/binning.rs index 253bb5c..d9b173d 100644 --- a/database/src/kv/binning.rs +++ b/database/src/kv/binning.rs @@ -22,14 +22,14 @@ impl Binning { comps.sort(); Self(comps) } - pub fn apply(&self, ob: Object<'_>, keys: &mut Vec<Vec<u8>>) { + pub fn apply(&self, ob: &Object, keys: &mut Vec<Vec<u8>>) { for f in &self.0 { f.apply(ob, keys); } } } impl BinningComponent { - pub fn apply(&self, ob: Object<'_>, keys: &mut Vec<Vec<u8>>) { + pub fn apply(&self, ob: &Object, keys: &mut Vec<Vec<u8>>) { match self { BinningComponent::Has(path) => { if path.get_matching_value(ob).is_none() { diff --git a/database/src/kv/index.rs b/database/src/kv/index.rs index ee14d77..ba5784b 100644 --- a/database/src/kv/index.rs +++ b/database/src/kv/index.rs @@ -22,7 +22,7 @@ pub fn update_index( is: SubtreeNum, ik: &IndexKey, row: RowNum, - ob: Object<'_>, + ob: &Object, remove: bool, ) -> Result<()> { let mut ks = vec![is.to_be_bytes().to_vec()]; diff --git a/database/src/kv/mod.rs b/database/src/kv/mod.rs index 837aa51..784cbef 100644 --- a/database/src/kv/mod.rs +++ b/database/src/kv/mod.rs @@ -24,7 +24,7 @@ use crate::{ }, }; use anyhow::{Result, anyhow}; -use jellyobject::ObjectBuffer; +use jellyobject::{Object, vec_to_ob, vec_u8_to_u32}; use log::{debug, info}; use std::{borrow::Cow, fmt::Write}; @@ -44,22 +44,17 @@ impl<T: jellykv::Store> Database for T { } impl Transaction for &mut dyn jellykv::Transaction { - fn insert(&mut self, entry: ObjectBuffer) -> Result<RowNum> { + fn insert(&mut self, entry: Box<Object>) -> Result<RowNum> { debug!("insert {entry:?}"); let mut id_counter = read_counter(*self, &T_ROW_COUNTER.to_be_bytes(), 0)?; let row = id_counter; id_counter += 1; write_counter(*self, &T_ROW_COUNTER.to_be_bytes(), id_counter)?; - jellykv::Transaction::set( - *self, - &row_key(row), - bytemuck::cast_slice(entry.0.as_slice()), - )?; + jellykv::Transaction::set(*self, &row_key(row), bytemuck::cast_slice(entry.export()))?; - let ob = entry.as_object(); for (is, ik) in list_indices(*self)? { - update_index(*self, is, &ik, row, ob, false)?; + update_index(*self, is, &ik, row, &entry, false)?; } Ok(row) @@ -67,34 +62,28 @@ impl Transaction for &mut dyn jellykv::Transaction { fn remove(&mut self, row: RowNum) -> Result<()> { debug!("remove {row}"); let entry = self.get(row)?.ok_or(anyhow!("row did not exist"))?; - let ob = entry.as_object(); for (is, ik) in list_indices(*self)? { - update_index(*self, is, &ik, row, ob, true)?; + update_index(*self, is, &ik, row, &entry, true)?; } jellykv::Transaction::del(*self, &row_key(row)) } - fn update(&mut self, row: RowNum, entry: ObjectBuffer) -> Result<()> { + fn update(&mut self, row: RowNum, entry: Box<Object>) -> Result<()> { debug!("update {row}"); let before = self.get(row)?.ok_or(anyhow!("row to update missing"))?; - let before = before.as_object(); - let after = entry.as_object(); - - jellykv::Transaction::set( - *self, - &row_key(row), - bytemuck::cast_slice(entry.0.as_slice()), - )?; + jellykv::Transaction::set(*self, &row_key(row), bytemuck::cast_slice(entry.export()))?; for (is, ik) in list_indices(*self)? { - update_index(*self, is, &ik, row, before, true)?; - update_index(*self, is, &ik, row, after, false)?; + update_index(*self, is, &ik, row, &before, true)?; + update_index(*self, is, &ik, row, &entry, false)?; } Ok(()) } - fn get(&self, row: RowNum) -> Result<Option<ObjectBuffer>> { - Ok(jellykv::Transaction::get(*self, &row_key(row))?.map(ObjectBuffer::from)) + fn get(&self, row: RowNum) -> Result<Option<Box<Object>>> { + Ok(jellykv::Transaction::get(*self, &row_key(row))? + .map(vec_u8_to_u32) + .map(vec_to_ob)) } fn query<'a>( @@ -181,8 +170,10 @@ fn create_index(txn: &mut dyn jellykv::Transaction, is: SubtreeNum, ik: &IndexKe for rowkey in rowkeys { let row = inv_row_key(&rowkey); - let buf = ObjectBuffer::from(jellykv::Transaction::get(txn, &rowkey)?.unwrap()); - update_index(txn, is, ik, row, buf.as_object(), false)?; + let buf = vec_to_ob(vec_u8_to_u32( + jellykv::Transaction::get(txn, &rowkey)?.unwrap(), + )); + update_index(txn, is, ik, row, &buf, false)?; } Ok(()) diff --git a/database/src/kv/tests.rs b/database/src/kv/tests.rs index e3e6b3b..5e65b4b 100644 --- a/database/src/kv/tests.rs +++ b/database/src/kv/tests.rs @@ -21,7 +21,7 @@ pub fn insert_get() -> Result<()> { Ok(()) })?; - assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Bob"); + assert_eq!(bob.unwrap().get(NAME).unwrap(), "Bob"); Ok(()) } @@ -37,7 +37,7 @@ pub fn update() -> Result<()> { Ok(()) })?; db.transaction(&mut |txn| { - let better_bob = new_bob().as_object().insert(NAME, "Better Bob"); + let better_bob = new_bob().insert(NAME, "Better Bob"); txn.update(bob_row, better_bob)?; Ok(()) })?; @@ -46,7 +46,7 @@ pub fn update() -> Result<()> { Ok(()) })?; - assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Better Bob"); + assert_eq!(bob.unwrap().get(NAME).unwrap(), "Better Bob"); Ok(()) } diff --git a/database/src/lib.rs b/database/src/lib.rs index b8a67c9..263c2fc 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -3,15 +3,15 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -#![feature(if_let_guard)] +#![feature(phantom_variance_markers)] +pub mod helper; pub mod kv; pub mod query_syntax; #[cfg(test)] pub mod test_shared; -pub mod helper; use anyhow::Result; -use jellyobject::{ObjectBuffer, Path, Value}; +use jellyobject::{Object, Path, Value}; pub type RowNum = u64; pub type RowIter = Box<dyn Iterator<Item = Result<(RowNum, Vec<u8>)>>>; @@ -21,10 +21,10 @@ pub trait Database: Send + Sync { } pub trait Transaction { - fn insert(&mut self, entry: ObjectBuffer) -> Result<RowNum>; + fn insert(&mut self, entry: Box<Object>) -> Result<RowNum>; fn remove(&mut self, row: RowNum) -> Result<()>; - fn update(&mut self, row: RowNum, entry: ObjectBuffer) -> Result<()>; - fn get(&self, row: RowNum) -> Result<Option<ObjectBuffer>>; + fn update(&mut self, row: RowNum, entry: Box<Object>) -> Result<()>; + fn get(&self, row: RowNum) -> Result<Option<Box<Object>>>; fn query<'a>( &'a mut self, query: Query, diff --git a/database/src/test_shared.rs b/database/src/test_shared.rs index 81d2e60..4d06a1f 100644 --- a/database/src/test_shared.rs +++ b/database/src/test_shared.rs @@ -4,25 +4,33 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use jellyobject::{ObjectBuffer, fields}; +use jellyobject::{OBB, Object, fields}; fields! { - NAME: &str = b"name"; + NAME: str = b"name"; AGE: u32 = b"age1"; - FRIEND: &str = b"frnd"; + FRIEND: str = b"frnd"; } -pub(crate) fn new_bob() -> ObjectBuffer { - ObjectBuffer::new(&mut [ - (NAME.0, &"Bob"), - (AGE.0, &35_u32), - (FRIEND.0, &"Alice"), - (FRIEND.0, &"Charlie"), - ]) +pub(crate) fn new_bob() -> Box<Object> { + OBB::new() + .with(NAME, "Bob") + .with(AGE, 35) + .with(FRIEND, "Alice") + .with(FRIEND, "Charlie") + .finish() } -pub(crate) fn new_alice() -> ObjectBuffer { - ObjectBuffer::new(&mut [(NAME.0, &"Alice"), (AGE.0, &31_u32), (FRIEND.0, &"Bob")]) +pub(crate) fn new_alice() -> Box<Object> { + OBB::new() + .with(NAME, "Alice") + .with(AGE, 31) + .with(FRIEND, "Bob") + .finish() } -pub(crate) fn new_charlie() -> ObjectBuffer { - ObjectBuffer::new(&mut [(NAME.0, &"Charlie"), (AGE.0, &31_u32), (FRIEND.0, &"Bob")]) +pub(crate) fn new_charlie() -> Box<Object> { + OBB::new() + .with(NAME, "Charlie") + .with(AGE, 31) + .with(FRIEND, "Bob") + .finish() } diff --git a/import/src/helpers.rs b/import/src/helpers.rs index e42ea8e..d25957f 100644 --- a/import/src/helpers.rs +++ b/import/src/helpers.rs @@ -7,7 +7,7 @@ use anyhow::Result; use jellycommon::{ NO_SLUG, - jellyobject::{ObjectBuffer, Path}, + jellyobject::{OBB, Path}, }; use jellydb::{Filter, Query, RowNum, Transaction}; @@ -17,6 +17,6 @@ pub fn get_or_insert_slug(txn: &mut dyn Transaction, slug: &str) -> Result<RowNu ..Default::default() })? { Some(r) => Ok(r), - None => txn.insert(ObjectBuffer::new(&mut [(NO_SLUG.0, &slug)])), + None => txn.insert(OBB::new().with(NO_SLUG, slug).finish()), } } diff --git a/import/src/lib.rs b/import/src/lib.rs index 684ef07..98f7216 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -#![feature(duration_constants, exit_status_error)] +#![feature(duration_constants, exit_status_error, phantom_variance_markers)] pub mod helpers; pub mod plugins; @@ -18,7 +18,7 @@ use anyhow::{Context, Result, anyhow}; use jellycache::{Cache, HashKey}; use jellycommon::{ internal::{IM_MTIME, IM_PATH}, - jellyobject::{self, ObjectBuffer, Path as TagPath, Tag}, + jellyobject::{self, OBB, Object, Path as TagPath, Tag}, *, }; use jellydb::{Database, Filter, Query, RowNum}; @@ -92,7 +92,7 @@ impl ImportConfig { pub fn update_node( &self, node: RowNum, - mut update: impl FnMut(ObjectBuffer) -> ObjectBuffer, + mut update: impl FnMut(Box<Object>) -> Box<Object>, ) -> Result<()> { self.db.transaction(&mut |txn| { let ob_before = txn.get(node)?.unwrap(); @@ -105,24 +105,24 @@ impl ImportConfig { pub fn update_node_slug( &self, slug: &str, - mut update: impl FnMut(ObjectBuffer) -> ObjectBuffer, + mut update: impl FnMut(Box<Object>) -> Box<Object>, ) -> Result<RowNum> { let mut row = 0; self.db.transaction(&mut |txn| { row = match txn.query_single(node_slug_query(slug))? { Some(r) => r, - None => txn.insert(ObjectBuffer::new(&mut [(NO_SLUG.0, &slug)]))?, + None => txn.insert(OBB::new().with(NO_SLUG, slug).finish())?, }; let node = txn.get(row)?.unwrap(); let node = update(node); - let node = node.as_object().insert(NO_SLUG, slug); + let node = node.insert(NO_SLUG, slug); txn.update(row, node)?; Ok(()) })?; Ok(row) } - pub fn get_node(&self, node: RowNum) -> Result<Option<ObjectBuffer>> { + pub fn get_node(&self, node: RowNum) -> Result<Option<Box<Object>>> { let mut buf = None; self.db.transaction(&mut |txn| { buf = txn.get(node)?; @@ -235,9 +235,9 @@ fn import_traverse( let row = ic.update_node_slug(&slug, |mut node| { if let Some(parent) = parent { - node = node.as_object().extend(NO_PARENT, [parent]); + node = node.extend(NO_PARENT, [parent]); } - node = node.as_object().insert( + node = node.insert( NO_VISIBILITY, if iflags.hidden { VISI_HIDDEN @@ -325,8 +325,8 @@ fn import_file( let slug = get_node_slug(ic, path).unwrap(); let Some(row) = reporting::catch(ic.update_node_slug(&slug, |mut node| { - node = node.as_object().extend(NO_PARENT, [parent]); - node = node.as_object().insert( + node = node.extend(NO_PARENT, [parent]); + node = node.insert( NO_VISIBILITY, if iflags.hidden { VISI_HIDDEN @@ -415,7 +415,7 @@ fn process_node( let mut slug = String::new(); reporting::catch(dba.db.transaction(&mut |txn| { let no = txn.get(node)?.unwrap(); - if let Some(s) = no.as_object().get(NO_SLUG) { + if let Some(s) = no.get(NO_SLUG) { slug = s.to_owned(); } Ok(()) @@ -460,7 +460,7 @@ fn compare_mtime(dba: &ImportConfig, path: &Path) -> Result<bool> { None => was_changed = true, Some(row) => { let meta = txn.get(row)?.unwrap(); - let prev_mtime = meta.as_object().get(IM_MTIME).unwrap_or_default(); + let prev_mtime = meta.get(IM_MTIME).unwrap_or_default(); was_changed = mtime > prev_mtime } } @@ -481,14 +481,11 @@ fn update_mtime(dba: &ImportConfig, path: &Path) -> Result<()> { ..Default::default() })? { Some(row) => row, - None => txn.insert(ObjectBuffer::new(&mut [( - IM_PATH.0, - &path.to_string_lossy().as_bytes(), - )]))?, + None => txn.insert(OBB::new().with(IM_PATH, &path.to_string_lossy()).finish())?, }; let mut ob = txn.get(row)?.unwrap(); - ob = ob.as_object().insert(IM_MTIME, mtime); + ob = ob.insert(IM_MTIME, mtime); txn.update(row, ob)?; Ok(()) diff --git a/import/src/plugins/acoustid.rs b/import/src/plugins/acoustid.rs index e0d1a96..caeeea5 100644 --- a/import/src/plugins/acoustid.rs +++ b/import/src/plugins/acoustid.rs @@ -192,10 +192,12 @@ impl ImportPlugin for AcoustID { }; ct.ic.update_node(node, |node| { - node.as_object().update(NO_IDENTIFIERS, |ids| { - ids.insert_s(ct.is, IDENT_ACOUST_ID_TRACK, &atid) - .as_object() - .insert_s(ct.is, IDENT_MUSICBRAINZ_RECORDING, &mbid) + node.update(NO_IDENTIFIERS, |ids| { + ids.insert_s(ct.is, IDENT_ACOUST_ID_TRACK, &atid).insert_s( + ct.is, + IDENT_MUSICBRAINZ_RECORDING, + &mbid, + ) }) })?; diff --git a/import/src/plugins/infojson.rs b/import/src/plugins/infojson.rs index 8a3ba5d..dfa5279 100644 --- a/import/src/plugins/infojson.rs +++ b/import/src/plugins/infojson.rs @@ -181,23 +181,23 @@ impl ImportPlugin for Infojson { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(parent)?.unwrap(); - node = node.as_object().insert_s(ct.is, NO_KIND, KIND_CHANNEL); - node = node.as_object().insert_s(ct.is, NO_TITLE, title); + node = node.insert_s(ct.is, NO_KIND, KIND_CHANNEL); + node = node.insert_s(ct.is, NO_TITLE, title); if let Some(cid) = &data.channel_id { - node = node.as_object().update(NO_IDENTIFIERS, |ids| { + node = node.update(NO_IDENTIFIERS, |ids| { ids.insert_s(ct.is, IDENT_YOUTUBE_CHANNEL, &cid) }); } if let Some(uid) = &data.uploader_id { - node = node.as_object().update(NO_IDENTIFIERS, |ids| { + node = node.update(NO_IDENTIFIERS, |ids| { ids.insert_s(ct.is, IDENT_YOUTUBE_CHANNEL_HANDLE, &uid) }) } if let Some(desc) = &data.description { - node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &desc); + node = node.insert_s(ct.is, NO_DESCRIPTION, &desc); } if let Some(followers) = data.channel_follower_count { - node = node.as_object().update(NO_RATINGS, |rat| { + node = node.update(NO_RATINGS, |rat| { rat.insert_s(ct.is, RTYP_YOUTUBE_SUBSCRIBERS, followers as f64) }); } @@ -242,45 +242,41 @@ impl ImportPlugin for Infojson { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(row)?.unwrap(); - node = node.as_object().insert_s(ct.is, NO_KIND, kind); - node = node.as_object().insert_s(ct.is, NO_TITLE, &infojson.title); + node = node.insert_s(ct.is, NO_KIND, kind); + node = node.insert_s(ct.is, NO_TITLE, &infojson.title); if let Some(title) = &infojson.alt_title && title != &infojson.title - && !node.as_object().has(NO_SUBTITLE.0) + && !node.has(NO_SUBTITLE.0) { - node = node.as_object().insert_s(ct.is, NO_SUBTITLE, &title); + node = node.insert_s(ct.is, NO_SUBTITLE, &title); } if let Some(up) = &infojson.uploader - && !node.as_object().has(NO_SUBTITLE.0) + && !node.has(NO_SUBTITLE.0) { - node = node - .as_object() - .insert_s(ct.is, NO_SUBTITLE, &clean_uploader_name(&up)); + node = node.insert_s(ct.is, NO_SUBTITLE, &clean_uploader_name(&up)); } if let Some(desc) = &infojson.description { - node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &desc); + node = node.insert_s(ct.is, NO_DESCRIPTION, &desc); } if let Some(tag) = infojson.tags.clone() { - node = node - .as_object() - .extend(NO_TAG, tag.iter().map(String::as_str)); + node = node.extend(NO_TAG, tag.iter().map(String::as_str)); } if let Some(rd) = release_date { - node = node.as_object().insert_s(ct.is, NO_RELEASEDATE, rd); + node = node.insert_s(ct.is, NO_RELEASEDATE, rd); } match infojson.extractor.as_str() { "youtube" => { - node = node.as_object().update(NO_IDENTIFIERS, |rat| { + node = node.update(NO_IDENTIFIERS, |rat| { rat.insert_s(ct.is, IDENT_YOUTUBE_VIDEO, &infojson.id) }); - node = node.as_object().update(NO_RATINGS, |rat| { + node = node.update(NO_RATINGS, |rat| { rat.insert_s( ct.is, RTYP_YOUTUBE_VIEWS, infojson.view_count.unwrap_or_default() as f64, ) }); - node = node.as_object().update(NO_RATINGS, |rat| { + node = node.update(NO_RATINGS, |rat| { rat.insert_s( ct.is, RTYP_YOUTUBE_LIKES, @@ -289,7 +285,7 @@ impl ImportPlugin for Infojson { }); } "Bandcamp" => { - node = node.as_object().update(NO_IDENTIFIERS, |rat| { + node = node.update(NO_IDENTIFIERS, |rat| { rat.insert_s(ct.is, IDENT_BANDCAMP, &infojson.id) }); } diff --git a/import/src/plugins/media_info.rs b/import/src/plugins/media_info.rs index f21386e..5daa3ba 100644 --- a/import/src/plugins/media_info.rs +++ b/import/src/plugins/media_info.rs @@ -9,10 +9,7 @@ use crate::{ source_rank::ObjectImportSourceExt, }; use anyhow::Result; -use jellycommon::{ - jellyobject::{Object, ObjectBuffer}, - *, -}; +use jellycommon::{jellyobject::OBB, *}; use jellydb::RowNum; use jellyremuxer::matroska::{Segment, TrackType}; use std::path::Path; @@ -32,84 +29,83 @@ impl ImportPlugin for MediaInfo { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(row)?.unwrap(); if let Some(tracks) = &seg.tracks { - node = node.as_object().extend_object( + node = node.extend_object( NO_TRACK, TR_SOURCE.0, - tracks.entries.iter().map(|t| { - let mut track = ObjectBuffer::empty(); - track = track.as_object().insert(TR_CODEC, &t.codec_id); - track = track.as_object().insert(TR_LANGUAGE, t.language.as_str()); - if let Some(name) = &t.name { - track = track.as_object().insert(TR_NAME, name); - } - track = track.as_object().insert( - TR_KIND, - match t.track_type { - TrackType::Audio => TRKIND_AUDIO, - TrackType::Video => TRKIND_VIDEO, - TrackType::Subtitle => TRKIND_TEXT, - _ => TRKIND_UNKNOWN, - }, - ); - if let Some(v) = &t.video { - track = track - .as_object() - .insert(TR_PIXEL_WIDTH, v.pixel_width as u32); - track = track - .as_object() - .insert(TR_PIXEL_HEIGHT, v.pixel_height as u32); - if let Some(fr) = v.frame_rate { - track = track.as_object().insert(TR_RATE, fr); + &tracks + .entries + .iter() + .map(|t| { + let mut track = OBB::new(); + track.push(TR_CODEC, &t.codec_id); + track.push(TR_LANGUAGE, &t.language); + if let Some(name) = &t.name { + track.push(TR_NAME, name); + } + track.push( + TR_KIND, + match t.track_type { + TrackType::Audio => TRKIND_AUDIO, + TrackType::Video => TRKIND_VIDEO, + TrackType::Subtitle => TRKIND_TEXT, + _ => TRKIND_UNKNOWN, + }, + ); + if let Some(v) = &t.video { + track.push(TR_PIXEL_WIDTH, v.pixel_width as u32); + track.push(TR_PIXEL_HEIGHT, v.pixel_height as u32); + if let Some(fr) = v.frame_rate { + track.push(TR_RATE, fr); + } } - } - if let Some(a) = &t.audio { - track = track.as_object().insert(TR_CHANNELS, a.channels as u32); - track = track.as_object().insert(TR_RATE, a.sampling_frequency); - if let Some(d) = a.bit_depth { - track = track.as_object().insert(TR_BIT_DEPTH, d as u32); + if let Some(a) = &t.audio { + track.push(TR_CHANNELS, a.channels as u32); + track.push(TR_RATE, a.sampling_frequency); + if let Some(d) = a.bit_depth { + track.push(TR_BIT_DEPTH, d as u32); + } } - } - let source = Object::EMPTY - .insert(TRSOURCE_LOCAL_PATH, &path.to_string_lossy()) - .as_object() - .insert(TRSOURCE_LOCAL_TRACKNUM, t.track_number); - track = track.as_object().insert(TR_SOURCE, source.as_object()); + let source = OBB::new() + .with(TRSOURCE_LOCAL_PATH, &path.to_string_lossy()) + .with(TRSOURCE_LOCAL_TRACKNUM, t.track_number) + .finish(); + track.push(TR_SOURCE, &source); - track - }), + track.finish() + }) + .collect::<Vec<_>>(), ); } if let Some(chapters) = &seg.chapters { - node = node.as_object().extend_object( + node = node.extend_object( NO_CHAPTER, CH_NAME.0, - chapters + &chapters .edition_entries .iter() .flat_map(|e| &e.chapter_atoms) .map(|cha| { - let mut chapter = ObjectBuffer::empty(); - chapter = chapter - .as_object() - .insert(CH_START, cha.time_start as f64 * 1e-9); + let mut chapter = OBB::new(); + chapter.push(CH_START, cha.time_start as f64 * 1e-9); if let Some(end) = cha.time_end { - chapter = chapter.as_object().insert(CH_END, end as f64 * 1e-9); + chapter.push(CH_END, end as f64 * 1e-9); } if let Some(display) = cha.displays.iter().next() { - chapter = chapter.as_object().insert(CH_NAME, &display.string); + chapter.push(CH_NAME, &display.string); } - chapter - }), + chapter.finish() + }) + .collect::<Vec<_>>(), ); } let runtime = fix_invalid_runtime( seg.info.duration.unwrap_or_default() * seg.info.timestamp_scale as f64 * 1e-9, ); - node = node.as_object().insert_s(ct.is, NO_DURATION, runtime); - node = node.as_object().insert_s(ct.is, NO_STORAGE_SIZE, size); + node = node.insert_s(ct.is, NO_DURATION, runtime); + node = node.insert_s(ct.is, NO_STORAGE_SIZE, size); txn.update(row, node)?; diff --git a/import/src/plugins/misc.rs b/import/src/plugins/misc.rs index 2c56dae..db9a8ab 100644 --- a/import/src/plugins/misc.rs +++ b/import/src/plugins/misc.rs @@ -49,9 +49,7 @@ impl ImportPlugin for ImageFiles { )?; ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(row)?.unwrap(); - node = node - .as_object() - .update(NO_PICTURES, |picts| picts.insert_s(ct.is, slot, &asset)); + node = node.update(NO_PICTURES, |picts| picts.insert_s(ct.is, slot, &asset)); txn.update(row, node)?; Ok(()) })?; @@ -84,7 +82,7 @@ impl ImportPlugin for ImageAttachments { }; ct.ic.update_node(row, |node| { - node.as_object().update(NO_PICTURES, |picts| { + node.update(NO_PICTURES, |picts| { picts.insert_s(ct.is, PICT_COVER, &cover) }) })?; @@ -105,13 +103,12 @@ impl ImportPlugin for General { fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> { if line == "hidden" { ct.ic.update_node(node, |node| { - node.as_object().insert_s(ct.is, NO_VISIBILITY, VISI_HIDDEN) + node.insert_s(ct.is, NO_VISIBILITY, VISI_HIDDEN) })?; } if line == "reduced" { ct.ic.update_node(node, |node| { - node.as_object() - .insert_s(ct.is, NO_VISIBILITY, VISI_REDUCED) + node.insert_s(ct.is, NO_VISIBILITY, VISI_REDUCED) })?; } if let Some(kind) = line.strip_prefix("kind-").or(line.strip_prefix("kind=")) { @@ -129,18 +126,16 @@ impl ImportPlugin for General { _ => bail!("unknown node kind"), }; ct.ic - .update_node(node, |node| node.as_object().insert_s(ct.is, NO_KIND, kind))?; + .update_node(node, |node| node.insert_s(ct.is, NO_KIND, kind))?; } if let Some(title) = line.strip_prefix("title=") { - ct.ic.update_node(node, |node| { - node.as_object().insert_s(ct.is, NO_TITLE, title) - })?; + ct.ic + .update_node(node, |node| node.insert_s(ct.is, NO_TITLE, title))?; } if let Some(index) = line.strip_prefix("index=") { let index = index.parse().context("parse index")?; - ct.ic.update_node(node, |node| { - node.as_object().insert_s(ct.is, NO_INDEX, index) - })?; + ct.ic + .update_node(node, |node| node.insert_s(ct.is, NO_INDEX, index))?; } Ok(()) } @@ -166,7 +161,7 @@ impl ImportPlugin for Children { continue; } ct.ic - .update_node_slug(line, |n| n.as_object().extend(NO_PARENT, [parent]))?; + .update_node_slug(line, |n| n.extend(NO_PARENT, [parent]))?; } } Ok(()) @@ -198,9 +193,9 @@ impl ImportPlugin for EpisodeIndex { .context("parse season num")?; ct.ic.update_node(node, |mut node| { - node = node.as_object().insert_s(ct.is, NO_SEASON_INDEX, season); - node = node.as_object().insert_s(ct.is, NO_INDEX, episode); - node = node.as_object().insert_s(ct.is, NO_KIND, KIND_EPISODE); + node = node.insert_s(ct.is, NO_SEASON_INDEX, season); + node = node.insert_s(ct.is, NO_INDEX, episode); + node = node.insert_s(ct.is, NO_KIND, KIND_EPISODE); node })?; } diff --git a/import/src/plugins/musicbrainz.rs b/import/src/plugins/musicbrainz.rs index ec44d11..deed38b 100644 --- a/import/src/plugins/musicbrainz.rs +++ b/import/src/plugins/musicbrainz.rs @@ -15,7 +15,10 @@ use crate::{ }; use anyhow::{Context, Result}; use jellycache::Cache; -use jellycommon::{jellyobject::ObjectBuffer, *}; +use jellycommon::{ + jellyobject::{EMPTY, OBB}, + *, +}; use jellydb::RowNum; use log::info; use reqwest::{ @@ -354,11 +357,11 @@ impl ImportPlugin for MusicBrainz { impl MusicBrainz { fn process_recording(&self, ct: &PluginContext, node_row: RowNum) -> Result<()> { let data = ct.ic.get_node(node_row)?.unwrap(); - let data = data.as_object(); + let data = data; let Some(mbid) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_MUSICBRAINZ_RECORDING) else { return Ok(()); @@ -369,13 +372,11 @@ impl MusicBrainz { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(node_row)?.unwrap(); - node = node.as_object().insert_s(ct.is, NO_TITLE, &rec.title); + node = node.insert_s(ct.is, NO_TITLE, &rec.title); if let Some(a) = rec.artist_credit.first() { - node = node - .as_object() - .insert_s(ct.is, NO_SUBTITLE, &a.artist.name); + node = node.insert_s(ct.is, NO_SUBTITLE, &a.artist.name); } - node = node.as_object().update(NO_IDENTIFIERS, |ids| { + node = node.update(NO_IDENTIFIERS, |ids| { ids.insert_multi( IDENT_ISRC, &rec.isrcs.iter().map(|e| e.as_str()).collect::<Vec<_>>(), @@ -401,21 +402,19 @@ impl MusicBrainz { let artist_row = get_or_insert_slug(txn, &artist_slug)?; let mut artist_node = txn.get(artist_row)?.unwrap(); - artist_node = artist_node.as_object().update(NO_IDENTIFIERS, |ids| { + artist_node = artist_node.update(NO_IDENTIFIERS, |ids| { ids.insert_s(ct.is, IDENT_MUSICBRAINZ_ARTIST, &artist.id) }); - artist_node = artist_node.as_object().insert(NO_KIND, KIND_PERSON); + artist_node = artist_node.insert(NO_KIND, KIND_PERSON); txn.update(artist_row, artist_node)?; ct.pending_nodes.lock().unwrap().insert(artist_row); - let credit = ObjectBuffer::new(&mut [ - (CR_NODE.0, &artist_row), - (CR_KIND.0, &cat), - (CR_ROLE.0, &role), - ]); - node = node - .as_object() - .extend_object(NO_CREDIT, CR_NODE.0, Some(credit)) + let credit = OBB::new() + .with(CR_NODE, artist_row) + .with(CR_KIND, cat) + .with(CR_ROLE, role) + .finish(); + node = node.extend_object(NO_CREDIT, CR_NODE.0, &[credit]) } txn.update(node_row, node) @@ -426,11 +425,11 @@ impl MusicBrainz { fn process_artist(&self, ct: &PluginContext, node_row: RowNum) -> Result<()> { let data = ct.ic.get_node(node_row)?.unwrap(); - let data = data.as_object(); + let data = data; let Some(mbid) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_MUSICBRAINZ_ARTIST) else { return Ok(()); @@ -440,7 +439,7 @@ impl MusicBrainz { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(node_row)?.unwrap(); - node = node.as_object().insert_s(ct.is, NO_TITLE, &artist.name); + node = node.insert_s(ct.is, NO_TITLE, &artist.name); for rel in &artist.relations { let url = rel.url.as_ref().map(|u| u.resource.clone()); @@ -449,7 +448,7 @@ impl MusicBrainz { if let Some(url) = url && let Some(id) = url.strip_prefix("https://www.wikidata.org/wiki/") { - node = node.as_object().update(NO_IDENTIFIERS, |ids| { + node = node.update(NO_IDENTIFIERS, |ids| { ids.insert_s(ct.is, IDENT_WIKIDATA, id) }) } @@ -458,7 +457,7 @@ impl MusicBrainz { if let Some(url) = url && let Some(id) = url.strip_prefix("https://vgmdb.net/artist/") { - node = node.as_object().update(NO_IDENTIFIERS, |ids| { + node = node.update(NO_IDENTIFIERS, |ids| { ids.insert_s(ct.is, IDENT_VGMDB_ARTIST, id) }) } diff --git a/import/src/plugins/omdb.rs b/import/src/plugins/omdb.rs index 3b97a9d..5d99fcb 100644 --- a/import/src/plugins/omdb.rs +++ b/import/src/plugins/omdb.rs @@ -4,14 +4,9 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use std::sync::Arc; - use anyhow::{Context, Result, anyhow}; use jellycache::Cache; -use jellycommon::{ - IDENT_IMDB, MSOURCE_OMDB, NO_DESCRIPTION, NO_IDENTIFIERS, NO_RATINGS, NO_TITLE, RTYP_IMDB, - RTYP_METACRITIC, RTYP_ROTTEN_TOMATOES, -}; +use jellycommon::{jellyobject::EMPTY, *}; use jellydb::RowNum; use log::info; use reqwest::{ @@ -19,6 +14,7 @@ use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, }; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use tokio::runtime::Handle; use crate::{ @@ -124,9 +120,8 @@ impl ImportPlugin for Omdb { } fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); - let Some(id) = data.get(NO_IDENTIFIERS).unwrap_or_default().get(IDENT_IMDB) else { + let Some(id) = data.get(NO_IDENTIFIERS).unwrap_or(EMPTY).get(IDENT_IMDB) else { return Ok(()); }; @@ -154,10 +149,8 @@ impl ImportPlugin for Omdb { .transpose()?; ct.ic.update_node(node, |mut node| { - node = node.as_object().insert_s(ct.is, NO_TITLE, &entry.title); - node = node - .as_object() - .insert_s(ct.is, NO_DESCRIPTION, &entry.plot); + node = node.insert_s(ct.is, NO_TITLE, &entry.title); + node = node.insert_s(ct.is, NO_DESCRIPTION, &entry.plot); for (typ, val) in [ (RTYP_METACRITIC, metascore), @@ -165,9 +158,7 @@ impl ImportPlugin for Omdb { (RTYP_ROTTEN_TOMATOES, rotten_tomatoes), ] { if let Some(x) = val { - node = node - .as_object() - .update(NO_RATINGS, |rts| rts.insert_s(ct.is, typ, x)); + node = node.update(NO_RATINGS, |rts| rts.insert_s(ct.is, typ, x)); } } node diff --git a/import/src/plugins/tags.rs b/import/src/plugins/tags.rs index c27ea70..901bcc6 100644 --- a/import/src/plugins/tags.rs +++ b/import/src/plugins/tags.rs @@ -39,16 +39,16 @@ impl ImportPlugin for Tags { ct.ic.update_node(node, |mut node| { if let Some(title) = &seg.info.title { - node = node.as_object().insert_s(ct.is, NO_TITLE, title); + node = node.insert_s(ct.is, NO_TITLE, title); } for (key, value) in &tags { match key.as_str() { "DESCRIPTION" | "SYNOPSIS" => { - node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &value) + node = node.insert_s(ct.is, NO_DESCRIPTION, &value) } - "COMMENT" => node = node.as_object().insert(NO_TAGLINE, &value), + "COMMENT" => node = node.insert(NO_TAGLINE, &value), "CONTENT_TYPE" => { - node = node.as_object().insert_s( + node = node.insert_s( ct.is, NO_KIND, match value.to_lowercase().trim() { @@ -69,7 +69,7 @@ impl ImportPlugin for Tags { "BARCODE" => IDENT_BARCODE, _ => continue, }; - node = node.as_object().update(NO_IDENTIFIERS, |idents| { + node = node.update(NO_IDENTIFIERS, |idents| { idents.insert_s(ct.is, idty, &value) }); } diff --git a/import/src/plugins/tmdb.rs b/import/src/plugins/tmdb.rs index 39f8115..832e240 100644 --- a/import/src/plugins/tmdb.rs +++ b/import/src/plugins/tmdb.rs @@ -11,7 +11,7 @@ use crate::{ use anyhow::{Context, Result, anyhow, bail}; use chrono::{Utc, format::Parsed}; use jellycache::{Cache, EscapeKey, HashKey}; -use jellycommon::*; +use jellycommon::{jellyobject::EMPTY, *}; use jellydb::RowNum; use log::info; use reqwest::{ @@ -196,17 +196,16 @@ impl ImportPlugin for Tmdb { impl Tmdb { fn process_primary(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); let (tmdb_kind, tmdb_id): (_, u64) = if let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TMDB_SERIES) { (TmdbKind::Tv, id.parse()?) } else if let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TMDB_MOVIE) { (TmdbKind::Movie, id.parse()?) @@ -237,31 +236,25 @@ impl Tmdb { ct.ic.update_node(node, |mut node| { if let Some(title) = &details.title { - node = node.as_object().insert_s(ct.is, NO_TITLE, &title); + node = node.insert_s(ct.is, NO_TITLE, &title); } if let Some(tagline) = &details.tagline { - node = node.as_object().insert_s(ct.is, NO_TAGLINE, &tagline); + node = node.insert_s(ct.is, NO_TAGLINE, &tagline); } - node = node - .as_object() - .insert_s(ct.is, NO_DESCRIPTION, &details.overview); - node = node.as_object().update(NO_RATINGS, |rat| { + node = node.insert_s(ct.is, NO_DESCRIPTION, &details.overview); + node = node.update(NO_RATINGS, |rat| { rat.insert_s(ct.is, RTYP_TMDB, details.vote_average) }); if let Some(poster) = &poster { - node = node - .as_object() - .update(NO_PICTURES, |rat| rat.insert_s(ct.is, PICT_COVER, &poster)); + node = node.update(NO_PICTURES, |rat| rat.insert_s(ct.is, PICT_COVER, &poster)); } if let Some(backdrop) = &backdrop { - node = node.as_object().update(NO_PICTURES, |rat| { + node = node.update(NO_PICTURES, |rat| { rat.insert_s(ct.is, PICT_BACKDROP, &backdrop) }); } if let Some(releasedate) = release_date { - node = node - .as_object() - .insert_s(ct.is, NO_RELEASEDATE, releasedate); + node = node.insert_s(ct.is, NO_RELEASEDATE, releasedate); } node })?; @@ -269,7 +262,7 @@ impl Tmdb { } fn process_episode(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); + let data = data; let (Some(episode), Some(season)) = (data.get(NO_INDEX), data.get(NO_SEASON_INDEX)) else { return Ok(()); @@ -279,9 +272,8 @@ impl Tmdb { for parent in data.iter(NO_PARENT) { let parent_data = txn.get(parent)?.ok_or(anyhow!("parent missing"))?; if let Some(id) = parent_data - .as_object() .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TMDB_SERIES) { series_id = Some(id.parse::<u64>()?); @@ -302,20 +294,16 @@ impl Tmdb { .context("still image download")?; let release_date = parse_release_date(&details.air_date)?; ct.ic.update_node(node, |mut node| { - node = node.as_object().insert_s(ct.is, NO_TITLE, &details.name); - node = node - .as_object() - .insert_s(ct.is, NO_DESCRIPTION, &details.overview); + node = node.insert_s(ct.is, NO_TITLE, &details.name); + node = node.insert_s(ct.is, NO_DESCRIPTION, &details.overview); if let Some(release_date) = release_date { - node = node - .as_object() - .insert_s(ct.is, NO_RELEASEDATE, release_date) + node = node.insert_s(ct.is, NO_RELEASEDATE, release_date) } - node = node.as_object().update(NO_RATINGS, |rat| { + node = node.update(NO_RATINGS, |rat| { rat.insert_s(ct.is, RTYP_TMDB, details.vote_average) }); if let Some(cover) = &cover { - node = node.as_object().update(NO_PICTURES, |picts| { + node = node.update(NO_PICTURES, |picts| { picts.insert_s(ct.is, PICT_COVER, &cover) }); } @@ -325,11 +313,11 @@ impl Tmdb { fn process_person(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); + let data = data; let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TMDB_PERSON) else { return Ok(()); @@ -344,8 +332,7 @@ impl Tmdb { let image = self.image(&ct.ic.cache, &prof.file_path, ct.rt)?; ct.ic.update_node(node, |node| { - node.as_object() - .update(NO_PICTURES, |pict| pict.insert_s(ct.is, PICT_COVER, &image)) + node.update(NO_PICTURES, |pict| pict.insert_s(ct.is, PICT_COVER, &image)) })?; Ok(()) diff --git a/import/src/plugins/trakt.rs b/import/src/plugins/trakt.rs index c7ad53e..9c5786c 100644 --- a/import/src/plugins/trakt.rs +++ b/import/src/plugins/trakt.rs @@ -12,7 +12,7 @@ use crate::{ use anyhow::{Context, Result, anyhow, bail}; use jellycache::{Cache, HashKey}; use jellycommon::{ - jellyobject::{ObjectBuffer, Tag}, + jellyobject::{EMPTY, OBB, Tag}, *, }; use jellydb::RowNum; @@ -423,8 +423,7 @@ impl ImportPlugin for Trakt { _ => bail!("unknown trakt kind"), }; ct.ic.update_node(node, |node| { - node.as_object() - .update(NO_IDENTIFIERS, |idents| idents.insert(ty, id)) + node.update(NO_IDENTIFIERS, |idents| idents.insert(ty, id)) })?; } Ok(()) @@ -439,16 +438,15 @@ impl ImportPlugin for Trakt { impl Trakt { fn process_primary(&self, ct: &PluginContext, node_row: RowNum) -> Result<()> { let data = ct.ic.get_node(node_row)?.unwrap(); - let data = data.as_object(); let (trakt_kind, trakt_id): (_, u64) = if let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TRAKT_SHOW) { (TraktKind::Show, id.parse()?) } else if let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TRAKT_MOVIE) { (TraktKind::Movie, id.parse()?) @@ -472,23 +470,21 @@ impl Trakt { ct.ic.db.transaction(&mut |txn| { let mut node = txn.get(node_row)?.unwrap(); - node = node - .as_object() - .insert_s(ct.is, NO_KIND, trakt_kind.as_node_kind()); - node = node.as_object().insert_s(ct.is, NO_TITLE, &details.title); + node = node.insert_s(ct.is, NO_KIND, trakt_kind.as_node_kind()); + node = node.insert_s(ct.is, NO_TITLE, &details.title); if let Some(overview) = &details.overview { - node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &overview); + node = node.insert_s(ct.is, NO_DESCRIPTION, &overview); } if let Some(tagline) = &details.tagline { - node = node.as_object().insert_s(ct.is, NO_TAGLINE, &tagline); + node = node.insert_s(ct.is, NO_TAGLINE, &tagline); } if let Some(x) = details.ids.imdb.clone() { - node = node.as_object().update(NO_IDENTIFIERS, |idents| { + node = node.update(NO_IDENTIFIERS, |idents| { idents.insert_s(ct.is, IDENT_IMDB, &x) }); } if let Some(x) = details.ids.tvdb.clone() { - node = node.as_object().update(NO_IDENTIFIERS, |idents| { + node = node.update(NO_IDENTIFIERS, |idents| { idents.insert_s(ct.is, IDENT_TVDB, &x.to_string()) }); } @@ -498,15 +494,11 @@ impl Trakt { TraktKind::Show => Some(IDENT_TMDB_SERIES), _ => None, } { - node = node - .as_object() - .update(NO_IDENTIFIERS, |idents| idents.insert(key, &x.to_string())); + node = node.update(NO_IDENTIFIERS, |idents| idents.insert(key, &x.to_string())); }; } if let Some(rating) = details.rating { - node = node - .as_object() - .update(NO_RATINGS, |idents| idents.insert(RTYP_TRAKT, rating)); + node = node.update(NO_RATINGS, |idents| idents.insert(RTYP_TRAKT, rating)); } let mut credits = Vec::new(); @@ -524,39 +516,35 @@ impl Trakt { let row = get_or_insert_slug(txn, &slug)?; let mut c = txn.get(row)?.unwrap(); - c = c.as_object().insert_s(ct.is, NO_KIND, KIND_PERSON); - c = c.as_object().insert_s(ct.is, NO_VISIBILITY, VISI_VISIBLE); - c = c.as_object().insert_s(ct.is, NO_TITLE, &ap.person.name); - c = c.as_object().update(NO_IDENTIFIERS, |ids| { + c = c.insert_s(ct.is, NO_KIND, KIND_PERSON); + c = c.insert_s(ct.is, NO_VISIBILITY, VISI_VISIBLE); + c = c.insert_s(ct.is, NO_TITLE, &ap.person.name); + c = c.update(NO_IDENTIFIERS, |ids| { let mut ids = ids.insert(IDENT_TRAKT_PERSON, &traktid.to_string()); if let Some(tmdbid) = ap.person.ids.tmdb { - ids = ids.as_object().insert_s( - ct.is, - IDENT_TMDB_PERSON, - &tmdbid.to_string(), - ); + ids = ids.insert_s(ct.is, IDENT_TMDB_PERSON, &tmdbid.to_string()); } if let Some(imdbid) = &ap.person.ids.imdb { - ids = ids.as_object().insert_s(ct.is, IDENT_IMDB_PERSON, imdbid); + ids = ids.insert_s(ct.is, IDENT_IMDB_PERSON, imdbid); } ids }); txn.update(row, c)?; - credits.push(ObjectBuffer::new(&mut [ - (CR_KIND.0, crcat), - (CR_ROLE.0, &role.as_str()), - (CR_NODE.0, &row), - ])); + credits.push( + OBB::new() + .with(CR_KIND, *crcat) + .with(CR_ROLE, &role) + .with(CR_NODE, row) + .finish(), + ); ct.pending_nodes.lock().unwrap().insert(row); } } - node = node - .as_object() - .extend_object(NO_CREDIT, CR_NODE.0, credits); + node = node.extend_object(NO_CREDIT, CR_NODE.0, &credits); txn.update(node_row, node)?; Ok(()) @@ -565,7 +553,6 @@ impl Trakt { } fn process_episode(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let node_data = ct.ic.get_node(node)?.unwrap(); - let node_data = node_data.as_object(); let (Some(episode), Some(season)) = (node_data.get(NO_INDEX), node_data.get(NO_SEASON_INDEX)) @@ -577,9 +564,8 @@ impl Trakt { for parent in node_data.iter(NO_PARENT) { let parent_data = txn.get(parent)?.ok_or(anyhow!("parent missing"))?; if let Some(id) = parent_data - .as_object() .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_TRAKT_SHOW) { show_id = Some(id.parse::<u64>()?); @@ -597,16 +583,14 @@ impl Trakt { let episodes = self.show_season_episodes(&ct.ic.cache, show_id, season, ct.rt)?; if let Some(episode) = episodes.get(episode.saturating_sub(1) as usize) { ct.ic.update_node(node, |mut node| { - node = node.as_object().insert_s(ct.is, NO_KIND, KIND_EPISODE); - node = node.as_object().insert_s(ct.is, NO_INDEX, episode.number); - node = node.as_object().insert_s(ct.is, NO_TITLE, &episode.title); + node = node.insert_s(ct.is, NO_KIND, KIND_EPISODE); + node = node.insert_s(ct.is, NO_INDEX, episode.number); + node = node.insert_s(ct.is, NO_TITLE, &episode.title); if let Some(overview) = &episode.overview { - node = node.as_object().insert_s(ct.is, NO_DESCRIPTION, &overview); + node = node.insert_s(ct.is, NO_DESCRIPTION, &overview); } if let Some(r) = episode.rating { - node = node - .as_object() - .update(NO_RATINGS, |rats| rats.insert_s(ct.is, RTYP_TRAKT, r)); + node = node.update(NO_RATINGS, |rats| rats.insert_s(ct.is, RTYP_TRAKT, r)); } node })?; diff --git a/import/src/plugins/vgmdb.rs b/import/src/plugins/vgmdb.rs index 4e1b273..f93e84c 100644 --- a/import/src/plugins/vgmdb.rs +++ b/import/src/plugins/vgmdb.rs @@ -11,7 +11,7 @@ use crate::{ }; use anyhow::{Context, Result}; use jellycache::{Cache, HashKey}; -use jellycommon::*; +use jellycommon::{jellyobject::EMPTY, *}; use jellydb::RowNum; use log::info; use regex::Regex; @@ -149,11 +149,10 @@ impl ImportPlugin for Vgmdb { fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_VGMDB_ARTIST) else { return Ok(()); @@ -165,8 +164,7 @@ impl ImportPlugin for Vgmdb { }; ct.ic.update_node(node, |node| { - node.as_object() - .update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image)) + node.update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image)) })?; Ok(()) diff --git a/import/src/plugins/wikidata.rs b/import/src/plugins/wikidata.rs index b92fdbb..5b79592 100644 --- a/import/src/plugins/wikidata.rs +++ b/import/src/plugins/wikidata.rs @@ -11,7 +11,7 @@ use crate::{ }; use anyhow::{Context, Result, bail}; use jellycache::{Cache, EscapeKey}; -use jellycommon::*; +use jellycommon::{jellyobject::EMPTY, *}; use jellydb::RowNum; use log::info; use reqwest::{ @@ -203,11 +203,10 @@ impl ImportPlugin for Wikidata { } fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.ic.get_node(node)?.unwrap(); - let data = data.as_object(); let Some(id) = data .get(NO_IDENTIFIERS) - .unwrap_or_default() + .unwrap_or(EMPTY) .get(IDENT_WIKIDATA) else { return Ok(()); @@ -222,8 +221,7 @@ impl ImportPlugin for Wikidata { .image_by_filename(&ct.ic.cache, filename, ct.rt)?; ct.ic.update_node(node, |node| { - node.as_object() - .update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image)) + node.update(NO_PICTURES, |pics| pics.insert_s(ct.is, PICT_COVER, &image)) })?; Ok(()) diff --git a/import/src/source_rank.rs b/import/src/source_rank.rs index 28ab4f7..3058df4 100644 --- a/import/src/source_rank.rs +++ b/import/src/source_rank.rs @@ -5,10 +5,9 @@ */ use jellycommon::{ - jellyobject::{Object, ObjectBuffer, Tag, TypedTag, ValueStore}, + jellyobject::{EMPTY, Object, Tag, TypedTag, ValueMarker}, *, }; -use std::marker::PhantomData; pub struct SourceRanks { list: Vec<Tag>, @@ -21,27 +20,30 @@ pub struct ImportSource<'a> { } pub trait ObjectImportSourceExt { - fn insert_s<T: ValueStore>(&self, is: ImportSource, key: TypedTag<T>, value: T) - -> ObjectBuffer; + fn insert_s<'a, T: ValueMarker<'a> + ?Sized>( + &'a self, + is: ImportSource, + key: TypedTag<T>, + value: T::Inner, + ) -> Box<Object>; } -impl<'a> ObjectImportSourceExt for Object<'a> { - fn insert_s<T: ValueStore>( - &self, +impl ObjectImportSourceExt for Object { + fn insert_s<'a, T: ValueMarker<'a> + ?Sized>( + &'a self, is: ImportSource, key: TypedTag<T>, - value: T, - ) -> ObjectBuffer { - let ms = self.get(NO_METASOURCE).unwrap_or_default(); - let ms_key = TypedTag::<Tag>(key.0, PhantomData); + value: T::Inner, + ) -> Box<Object> { + let ms = self.get(NO_METASOURCE).unwrap_or(EMPTY); + let ms_key = TypedTag::<Tag>::new(key.0); if let Some(current_source) = ms.get(ms_key) { if !is.ranks.compare(key.0, current_source, is.tag) { - return self.dump(); + return self.to_owned(); } } self.insert(key, value) - .as_object() .update(NO_METASOURCE, |ms| ms.insert(ms_key, is.tag)) } } diff --git a/server/src/auth.rs b/server/src/auth.rs index d973dfc..0174e13 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -7,13 +7,10 @@ use crate::State; use anyhow::{Result, anyhow, bail}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; -use jellycommon::{ - jellyobject::{ObjectBuffer, Path}, - *, -}; +use jellycommon::{jellyobject::Path, *}; use jellydb::{Filter, Query}; -pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> { +pub fn token_to_user(state: &State, token: &str) -> Result<Box<User>> { let user_row = token::validate(&state.session_key, token)?; let mut user = None; @@ -49,7 +46,7 @@ pub fn login( let (Some(user_row), Some(user)) = (user_row, user) else { bail!("unknown user"); }; - let Some(correct_pw) = user.as_object().get(USER_PASSWORD) else { + let Some(correct_pw) = user.get(USER_PASSWORD) else { bail!("password login is disabled") }; if password != correct_pw { @@ -62,7 +59,7 @@ pub fn login( user_row, expire.unwrap_or(60 * 60 * 24 * 30), ), - user.as_object().has(USER_PASSWORD_REQUIRE_CHANGE.0), + user.has(USER_PASSWORD_REQUIRE_CHANGE.0), )) } diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index 4ba3dc8..917adc0 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -30,10 +30,7 @@ pub fn r_youtube_watch(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { Ok(()) })?; let node = res.ok_or(anyhow!("video not found"))?; - let slug = node - .as_object() - .get(NO_SLUG) - .ok_or(anyhow!("node has no slug"))?; + let slug = node.get(NO_SLUG).ok_or(anyhow!("node has no slug"))?; Ok(Redirect::found(u_node_id(slug))) } diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index e332811..38d2b7b 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -65,7 +65,7 @@ pub async fn r_stream( Ok(()) })?; - let Some(node) = node.as_ref().map(|n| n.as_object()) else { + let Some(node) = node else { Err(anyhow!("node not found"))? }; diff --git a/server/src/main.rs b/server/src/main.rs index 8209879..d6f1e1e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -15,7 +15,7 @@ use anyhow::{Result, anyhow}; use jellycache::Cache; use jellycommon::{ USER_ADMIN, USER_LOGIN, USER_PASSWORD, - jellyobject::{ObjectBuffer, Path}, + jellyobject::{OBB, Path}, }; use jellydb::{Database, Filter, Query}; use log::{error, info}; @@ -105,11 +105,13 @@ fn create_admin_user(state: &State) -> Result<()> { if admin_row.is_none() { info!("Creating new admin user"); let pwhash = hash_password("admin", &state.config.admin_password); - txn.insert(ObjectBuffer::new(&mut [ - (USER_LOGIN.0, &"admin"), - (USER_PASSWORD.0, &pwhash.as_slice()), - (USER_ADMIN.0, &()), - ]))?; + txn.insert( + OBB::new() + .with(USER_LOGIN, "admin") + .with(USER_PASSWORD, &pwhash) + .with(USER_ADMIN, ()) + .finish(), + )?; } Ok(()) diff --git a/server/src/request_info.rs b/server/src/request_info.rs index 442c7fb..0adfe96 100644 --- a/server/src/request_info.rs +++ b/server/src/request_info.rs @@ -10,10 +10,7 @@ use crate::{ ui::error::{MyError, MyResult}, }; use anyhow::anyhow; -use jellycommon::{ - USER_ADMIN, - jellyobject::{Object, ObjectBuffer}, -}; +use jellycommon::{USER_ADMIN, User, jellyobject::Object}; use jellyui::{Page, RenderInfo, Scaffold}; use rocket::{ Request, async_trait, @@ -27,7 +24,7 @@ pub struct RequestInfo<'a> { pub lang: &'a str, pub accept: Accept, pub debug: &'a str, - pub user: Option<ObjectBuffer>, + pub user: Option<Box<User>>, pub state: Arc<State>, pub flash: Option<FlashMessage<'a>>, } @@ -59,13 +56,12 @@ impl<'a> RequestInfo<'a> { flash: FlashMessage::from_request(request).await.succeeded(), }) } - pub fn require_user(&'a self) -> MyResult<Object<'a>> { + pub fn require_user(&'a self) -> MyResult<&'a Object> { self.user - .as_ref() - .map(|u| u.as_object()) + .as_deref() .ok_or(MyError(anyhow!("user required"))) } - pub fn require_admin(&'a self) -> MyResult<Object<'a>> { + pub fn require_admin(&'a self) -> MyResult<&'a Object> { let user = self.require_user()?; if !user.has(USER_ADMIN.0) { Err(anyhow!("admin required"))? @@ -76,7 +72,7 @@ impl<'a> RequestInfo<'a> { RenderInfo { lang: self.lang, status_message: None, - user: self.user.as_ref().map(|u| u.as_object()), + user: self.user.as_deref(), config: &self.state.config.ui, message: self.flash.as_ref().map(|f| (f.kind(), f.message())), } @@ -129,7 +125,7 @@ fn accept_language<'a>(request: &'a Request<'_>) -> &'a str { .unwrap_or("en") } -fn user_from_request(state: &State, req: &Request<'_>) -> Result<Option<ObjectBuffer>, MyError> { +fn user_from_request(state: &State, req: &Request<'_>) -> Result<Option<Box<User>>, MyError> { let Some(token) = req .query_value("session") .map(|e| e.unwrap()) diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index b106fc7..9e5fa39 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -82,10 +82,10 @@ pub fn r_account_login_post( })?; if let Some(ur) = user_row { let mut user = txn.get(ur)?.unwrap(); - user = user.as_object().remove(USER_PASSWORD_REQUIRE_CHANGE); - user = user.as_object().insert(USER_PASSWORD, &password_hash); + user = user.remove(USER_PASSWORD_REQUIRE_CHANGE); + user = user.insert(USER_PASSWORD, &password_hash); if let Some(name) = &form.display_name { - user = user.as_object().insert(USER_NAME, &name); + user = user.insert(USER_NAME, &name); } txn.update(ur, user)?; } diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index 9a98c09..2585061 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -7,7 +7,7 @@ use super::format_form_error; use crate::{auth::hash_password, request_info::RequestInfo, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, Path, Tag}, + jellyobject::{Object, Path, Tag}, routes::u_account_settings, *, }; @@ -104,7 +104,7 @@ pub fn r_account_settings_post( Ok(Flash::success(Redirect::to(u_account_settings()), out)) } -fn update_user(ri: &RequestInfo, update: impl Fn(Object) -> ObjectBuffer) -> MyResult<()> { +fn update_user(ri: &RequestInfo, update: impl Fn(&Object) -> Box<Object>) -> MyResult<()> { let login = ri .require_user()? .get(USER_LOGIN) @@ -118,7 +118,7 @@ fn update_user(ri: &RequestInfo, update: impl Fn(Object) -> ObjectBuffer) -> MyR .ok_or(anyhow!("user vanished"))?; let user = txn.get(user_row)?.unwrap(); - let new_user = update(user.as_object()); + let new_user = update(&user); txn.update(user_row, new_user)?; Ok(()) diff --git a/server/src/ui/admin/users.rs b/server/src/ui/admin/users.rs index 85f241b..4b11279 100644 --- a/server/src/ui/admin/users.rs +++ b/server/src/ui/admin/users.rs @@ -46,7 +46,7 @@ pub fn r_admin_users(ri: RequestInfo) -> MyResult<RawHtml<String>> { Ok(ri.respond_ui(&AdminUserList { ri: &ri.render_info(), - users: &users.iter().map(|u| u.as_object()).collect::<Vec<_>>(), + users: &users.iter().map(|u| &**u).collect::<Vec<_>>(), })) } @@ -97,7 +97,7 @@ pub fn r_admin_user(ri: RequestInfo<'_>, name: &str) -> MyResult<RawHtml<String> Ok(ri.respond_ui(&AdminUser { ri: &ri.render_info(), - user: user.as_object(), + user: &user, })) } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 6cb6a77..17cac83 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -7,11 +7,11 @@ use super::error::MyResult; use crate::request_info::RequestInfo; use anyhow::{Context, Result}; -use jellycommon::jellyobject::{Object, ObjectBuffer}; -use jellydb::Query; -use jellyui::components::home::HomeRow; +use jellycommon::{Nku, jellyobject::EMPTY}; +use jellydb::{Query, helper::DatabaseReturnExt}; +use jellyui::components::home::{Home, HomeRow}; use rocket::{get, response::content::RawHtml}; -use std::str::FromStr; +use std::{borrow::Cow, str::FromStr}; #[get("/home")] pub fn r_home(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> { @@ -28,7 +28,7 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> { "home.bin.latest_music", "FILTER (visi = visi AND kind = musi) SORT DESCENDING BY FIRST rldt", )?); - rows.push(home_row_highlight( + rows.extend(home_row_highlight( &ri, "home.bin.daily_random", "FILTER (visi = visi AND kind = movi) SORT RANDOM", @@ -38,51 +38,59 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> { "home.bin.max_rating", "SORT DESCENDING BY FIRST rtng.imdb", )?); - rows.push(home_row_highlight( + rows.extend(home_row_highlight( &ri, "home.bin.daily_random", "FILTER (visi = visi AND kind = show) SORT RANDOM", )?); - Ok(ri.respond_ui(rows)) + Ok(ri.respond_ui(&Home { + ri: &ri.render_info(), + rows: &rows, + })) } -fn home_row(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<HomeRow> { +fn home_row( + ri: &RequestInfo<'_>, + title: &'static str, + query: &str, +) -> Result<(&'static str, HomeRow<'static>)> { let q = Query::from_str(query).context("parse query")?; - let mut res = ObjectBuffer::empty(); - ri.state.database.transaction(&mut |txn| { + ri.state.database.transaction_ret(|txn| { let rows = txn.query(q.clone())?.take(16).collect::<Result<Vec<_>>>()?; let mut nkus = Vec::new(); for (row, _) in rows { let node = txn.get(row)?.unwrap(); - nkus.push(ObjectBuffer::new(&mut [(NKU_NODE.0, &node.as_object())])); + nkus.push(Nku { + node: Cow::Owned(node), + role: None, + userdata: Cow::Borrowed(EMPTY), + }); } - let nkus = nkus.iter().map(|n| n.as_object()).collect::<Vec<_>>(); // TODO -_- - - res = Object::EMPTY.insert(NODELIST_DISPLAYSTYLE, NLSTYLE_INLINE); - res = res.as_object().insert(NODELIST_TITLE, title); - res = res.as_object().insert_multi(NODELIST_ITEM, &nkus); - - Ok(()) - })?; - Ok(res) + Ok((title, HomeRow::Inline(nkus))) + }) } -fn home_row_highlight(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<HomeRow> { +fn home_row_highlight( + ri: &RequestInfo<'_>, + title: &'static str, + query: &str, +) -> Result<Option<(&'static str, HomeRow<'static>)>> { let q = Query::from_str(query).context("parse query")?; - let mut res = ObjectBuffer::empty(); - ri.state.database.transaction(&mut |txn| { + ri.state.database.transaction_ret(|txn| { let Some(row) = txn.query(q.clone())?.next() else { - return Ok(()); + return Ok(None); }; let row = row?.0; let node = txn.get(row)?.unwrap(); - let nku = ObjectBuffer::new(&mut [(NKU_NODE.0, &node.as_object())]); - res = Object::EMPTY.insert(NODELIST_DISPLAYSTYLE, NLSTYLE_HIGHLIGHT); - res = res.as_object().insert(NODELIST_TITLE, title); - res = res.as_object().insert(NODELIST_ITEM, nku.as_object()); - Ok(()) - })?; - Ok(res) + Ok(Some(( + title, + HomeRow::Highlight(Nku { + node: Cow::Owned(node), + role: None, + userdata: Cow::Borrowed(EMPTY), + }), + ))) + }) } diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index b800914..6071bcc 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -8,12 +8,13 @@ use crate::{request_info::RequestInfo, ui::error::MyResult}; use anyhow::anyhow; use base64::{Engine, prelude::BASE64_URL_SAFE}; use jellycommon::{ - jellyobject::{Object, Path}, + jellyobject::{EMPTY, Path}, *, }; use jellydb::{Filter, Query}; use jellyui::components::items::Items; use rocket::{get, response::content::RawHtml}; +use std::borrow::Cow; #[get("/items?<cont>")] pub fn r_items(ri: RequestInfo, cont: Option<&str>) -> MyResult<RawHtml<String>> { @@ -49,8 +50,8 @@ pub fn r_items(ri: RequestInfo, cont: Option<&str>) -> MyResult<RawHtml<String>> items: &items .iter() .map(|node| Nku { - node: node.as_object(), - userdata: Object::EMPTY, + node: Cow::Borrowed(&node), + userdata: Cow::Borrowed(EMPTY), role: None, }) .collect::<Vec<_>>(), diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 55a1d09..ca07bac 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -5,186 +5,183 @@ */ use super::error::MyResult; -use crate::{request_info::RequestInfo, ui_responder::UiResponse}; -use anyhow::Result; +use crate::request_info::RequestInfo; +use anyhow::anyhow; use jellycommon::{ - jellyobject::{OBB, Object, ObjectBuffer, ObjectBufferBuilder, Path}, + jellyobject::{EMPTY, Path}, *, }; -use jellydb::{Filter, MultiBehaviour, Query, Sort, SortOrder, Transaction, ValueSort}; -use rocket::get; -use std::collections::BTreeMap; +use jellydb::{Filter, Query}; +use jellyui::components::node_page::NodePage; +use rocket::{get, response::content::RawHtml}; +use std::borrow::Cow; #[get("/n/<slug>")] -pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<UiResponse> { +pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<RawHtml<String>> { ri.require_user()?; - let mut page = OBB::new(); + let mut nku = None; ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), ..Default::default() })? { let n = txn.get(row)?.unwrap(); - let nku = Object::EMPTY.insert(NKU_NODE, n.as_object()); - let nku = nku.as_object(); - - page = OBB::new(); - let title = nku - .get(NKU_NODE) - .unwrap_or_default() - .get(NO_TITLE) - .unwrap_or_default(); - - page.push(VIEW_TITLE, title); - page.push(VIEW_NODE_PAGE, nku); - c_children(&mut page, txn, row, &nku)?; - c_credits(&mut page, txn, &nku)?; - c_credited(&mut page, txn, row)?; + nku = Some(Nku { + node: Cow::Owned(n), + userdata: Cow::Borrowed(EMPTY), + role: None, + }); } Ok(()) })?; + let Some(nku) = nku else { + Err(anyhow!("no such node"))? + }; - Ok(ri.respond_ui(page)) + Ok(ri.respond_ui(&NodePage { + ri: &ri.render_info(), + nku, + })) } -fn c_children( - page: &mut ObjectBufferBuilder, - txn: &mut dyn Transaction, - row: u64, - nku: &Object, -) -> Result<()> { - let kind = nku - .get(NKU_NODE) - .unwrap_or_default() - .get(NO_KIND) - .unwrap_or(KIND_COLLECTION); +// fn c_children( +// page: &mut ObjectBufferBuilder, +// txn: &mut dyn Transaction, +// row: u64, +// nku: &Object, +// ) -> Result<()> { +// let kind = nku +// .get(NKU_NODE) +// .unwrap_or_default() +// .get(NO_KIND) +// .unwrap_or(KIND_COLLECTION); - let (order, path) = match kind { - KIND_CHANNEL => (SortOrder::Descending, Path(vec![NO_RELEASEDATE.0])), - KIND_SEASON | KIND_SHOW => (SortOrder::Ascending, Path(vec![NO_INDEX.0])), - _ => (SortOrder::Ascending, Path(vec![NO_TITLE.0])), - }; +// let (order, path) = match kind { +// KIND_CHANNEL => (SortOrder::Descending, Path(vec![NO_RELEASEDATE.0])), +// KIND_SEASON | KIND_SHOW => (SortOrder::Ascending, Path(vec![NO_INDEX.0])), +// _ => (SortOrder::Ascending, Path(vec![NO_TITLE.0])), +// }; - let children_rows = txn - .query(Query { - sort: Sort::Value(ValueSort { - multi: MultiBehaviour::First, - offset: None, - order, - path, - }), - filter: Filter::All(vec![ - Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), - Filter::Match(Path(vec![NO_PARENT.0]), row.into()), - ]), - ..Default::default() - })? - .collect::<Result<Vec<_>>>()?; +// let children_rows = txn +// .query(Query { +// sort: Sort::Value(ValueSort { +// multi: MultiBehaviour::First, +// offset: None, +// order, +// path, +// }), +// filter: Filter::All(vec![ +// Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), +// Filter::Match(Path(vec![NO_PARENT.0]), row.into()), +// ]), +// ..Default::default() +// })? +// .collect::<Result<Vec<_>>>()?; - if children_rows.is_empty() { - return Ok(()); - } +// if children_rows.is_empty() { +// return Ok(()); +// } - let mut list = ObjectBufferBuilder::default(); +// let mut list = ObjectBufferBuilder::default(); - list.push( - NODELIST_DISPLAYSTYLE, - match kind { - KIND_SEASON | KIND_SHOW => NLSTYLE_LIST, - _ => NLSTYLE_GRID, - }, - ); +// list.push( +// NODELIST_DISPLAYSTYLE, +// match kind { +// KIND_SEASON | KIND_SHOW => NLSTYLE_LIST, +// _ => NLSTYLE_GRID, +// }, +// ); - for (row, _) in children_rows { - list.push( - NODELIST_ITEM, - Object::EMPTY - .insert(NKU_NODE, txn.get(row)?.unwrap().as_object()) - .as_object(), - ); - } +// for (row, _) in children_rows { +// list.push( +// NODELIST_ITEM, +// Object::EMPTY +// .insert(NKU_NODE, txn.get(row)?.unwrap().as_object()) +// .as_object(), +// ); +// } - page.push(VIEW_NODE_LIST, list.finish().as_object()); - Ok(()) -} +// page.push(VIEW_NODE_LIST, list.finish().as_object()); +// Ok(()) +// } -fn c_credits( - page: &mut ObjectBufferBuilder, - txn: &mut dyn Transaction, - nku: &Object, -) -> Result<()> { - if !nku.get(NKU_NODE).unwrap_or_default().has(NO_CREDIT.0) { - return Ok(()); - } +// fn c_credits( +// page: &mut ObjectBufferBuilder, +// txn: &mut dyn Transaction, +// nku: &Object, +// ) -> Result<()> { +// if !nku.get(NKU_NODE).unwrap_or_default().has(NO_CREDIT.0) { +// return Ok(()); +// } - let mut cats = BTreeMap::<_, Vec<_>>::new(); - for cred in nku.get(NKU_NODE).unwrap_or_default().iter(NO_CREDIT) { - let mut o = ObjectBuffer::empty(); - if let Some(row) = cred.get(CR_NODE) { - let node = txn.get(row)?.unwrap(); - o = o.as_object().insert(NKU_NODE, node.as_object()); - } - if let Some(role) = cred.get(CR_ROLE) { - o = o.as_object().insert(NKU_ROLE, role) - } - cats.entry(cred.get(CR_KIND).unwrap_or(CRCAT_CREW)) - .or_default() - .push(o); - } - let mut cats = cats.into_iter().collect::<Vec<_>>(); - cats.sort_by_key(|(c, _)| match *c { - CRCAT_CAST => 0, - CRCAT_CREW => 1, - _ => 100, - }); - for (cat, elems) in cats { - let mut list = ObjectBufferBuilder::default(); - list.push(NODELIST_DISPLAYSTYLE, NLSTYLE_INLINE); - list.push(NODELIST_TITLE, &format!("tag.cred.kind.{cat}")); - for item in elems { - list.push(NODELIST_ITEM, item.as_object()); - } - page.push(VIEW_NODE_LIST, list.finish().as_object()); - } +// let mut cats = BTreeMap::<_, Vec<_>>::new(); +// for cred in nku.get(NKU_NODE).unwrap_or_default().iter(NO_CREDIT) { +// let mut o = ObjectBuffer::empty(); +// if let Some(row) = cred.get(CR_NODE) { +// let node = txn.get(row)?.unwrap(); +// o = o.as_object().insert(NKU_NODE, node.as_object()); +// } +// if let Some(role) = cred.get(CR_ROLE) { +// o = o.as_object().insert(NKU_ROLE, role) +// } +// cats.entry(cred.get(CR_KIND).unwrap_or(CRCAT_CREW)) +// .or_default() +// .push(o); +// } +// let mut cats = cats.into_iter().collect::<Vec<_>>(); +// cats.sort_by_key(|(c, _)| match *c { +// CRCAT_CAST => 0, +// CRCAT_CREW => 1, +// _ => 100, +// }); +// for (cat, elems) in cats { +// let mut list = ObjectBufferBuilder::default(); +// list.push(NODELIST_DISPLAYSTYLE, NLSTYLE_INLINE); +// list.push(NODELIST_TITLE, &format!("tag.cred.kind.{cat}")); +// for item in elems { +// list.push(NODELIST_ITEM, item.as_object()); +// } +// page.push(VIEW_NODE_LIST, list.finish().as_object()); +// } - Ok(()) -} +// Ok(()) +// } -fn c_credited(page: &mut ObjectBufferBuilder, txn: &mut dyn Transaction, row: u64) -> Result<()> { - let children_rows = txn - .query(Query { - sort: Sort::Value(ValueSort { - multi: MultiBehaviour::First, - offset: None, - order: SortOrder::Ascending, - path: Path(vec![NO_TITLE.0]), - }), - filter: Filter::All(vec![ - Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), - Filter::Match(Path(vec![NO_CREDIT.0, CR_NODE.0]), row.into()), - ]), - ..Default::default() - })? - .collect::<Result<Vec<_>>>()?; +// fn c_credited(page: &mut ObjectBufferBuilder, txn: &mut dyn Transaction, row: u64) -> Result<()> { +// let children_rows = txn +// .query(Query { +// sort: Sort::Value(ValueSort { +// multi: MultiBehaviour::First, +// offset: None, +// order: SortOrder::Ascending, +// path: Path(vec![NO_TITLE.0]), +// }), +// filter: Filter::All(vec![ +// Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), +// Filter::Match(Path(vec![NO_CREDIT.0, CR_NODE.0]), row.into()), +// ]), +// ..Default::default() +// })? +// .collect::<Result<Vec<_>>>()?; - if children_rows.is_empty() { - return Ok(()); - } +// if children_rows.is_empty() { +// return Ok(()); +// } - let mut list = ObjectBufferBuilder::default(); - list.push(NODELIST_DISPLAYSTYLE, NLSTYLE_GRID); - list.push(NODELIST_TITLE, "node.credited"); +// let mut list = ObjectBufferBuilder::default(); +// list.push(NODELIST_DISPLAYSTYLE, NLSTYLE_GRID); +// list.push(NODELIST_TITLE, "node.credited"); - for (row, _) in children_rows { - list.push( - NODELIST_ITEM, - Object::EMPTY - .insert(NKU_NODE, txn.get(row)?.unwrap().as_object()) - .as_object(), - ); - } +// for (row, _) in children_rows { +// list.push( +// NODELIST_ITEM, +// Object::EMPTY +// .insert(NKU_NODE, txn.get(row)?.unwrap().as_object()) +// .as_object(), +// ); +// } - page.push(VIEW_NODE_LIST, list.finish().as_object()); - Ok(()) -} +// page.push(VIEW_NODE_LIST, list.finish().as_object()); +// Ok(()) +// } diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 1050abb..c6c177e 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -5,13 +5,15 @@ */ use super::error::MyResult; use crate::request_info::RequestInfo; +use anyhow::anyhow; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, Path}, + jellyobject::{EMPTY, Path}, *, }; use jellydb::{Filter, Query}; use jellyui::components::node_page::Player; use rocket::{get, response::content::RawHtml}; +use std::borrow::Cow; // fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { // let protocol = if CONF.tls { "https" } else { "http" }; @@ -31,22 +33,25 @@ pub fn r_player(ri: RequestInfo<'_>, t: Option<f64>, slug: &str) -> MyResult<Raw ri.require_user()?; let _ = t; - let mut node = ObjectBuffer::empty(); + let mut node = None; ri.state.database.transaction(&mut |txn| { if let Some(row) = txn.query_single(Query { filter: Filter::Match(Path(vec![NO_SLUG.0]), slug.into()), ..Default::default() })? { - node = txn.get(row)?.unwrap(); + node = Some(txn.get(row)?.unwrap()); } Ok(()) })?; + let Some(node) = node else { + Err(anyhow!("no such node"))? + }; Ok(ri.respond_ui(&Player { ri: &ri.render_info(), nku: Nku { - node: node.as_object(), - userdata: Object::EMPTY, + node: Cow::Borrowed(&node), + userdata: Cow::Borrowed(EMPTY), role: None, }, })) diff --git a/ui/src/components/admin.rs b/ui/src/components/admin.rs index 76dfe6a..ddf49ca 100644 --- a/ui/src/components/admin.rs +++ b/ui/src/components/admin.rs @@ -61,7 +61,7 @@ markup::define!( } } - AdminUserList<'a>(ri: &'a RenderInfo<'a>, users: &'a [User<'a>]) { + AdminUserList<'a>(ri: &'a RenderInfo<'a>, users: &'a [&'a User]) { h1 { @tr(ri.lang, "admin.users") } form[method="POST", action=u_admin_new_user()] { label[for="login"] { "Login: " } @@ -73,7 +73,7 @@ markup::define!( }} } - AdminUser<'a>(ri: &'a RenderInfo<'a>, user: User<'a>) { + AdminUser<'a>(ri: &'a RenderInfo<'a>, user: &'a User) { h2 { @user.get(USER_NAME).unwrap_or("nameless user") } p { @tr(ri.lang, "tag.Ulgn") ": " @user.get(USER_LOGIN) } form[method="POST", action=u_admin_user_remove(user.get(USER_LOGIN).unwrap())] { diff --git a/ui/src/components/home.rs b/ui/src/components/home.rs index 5ae6a66..f7444eb 100644 --- a/ui/src/components/home.rs +++ b/ui/src/components/home.rs @@ -12,7 +12,7 @@ use crate::{ use jellycommon::Nku; use jellyui_locale::tr; -pub enum HomeRow { +pub enum HomeRow<'a> { Inline(Vec<Nku<'a>>), Highlight(Nku<'a>), } diff --git a/ui/src/components/node_card.rs b/ui/src/components/node_card.rs index e1baec1..2f5bae7 100644 --- a/ui/src/components/node_card.rs +++ b/ui/src/components/node_card.rs @@ -9,16 +9,16 @@ use crate::{ components::{node_page::aspect_class, props::Props}, }; use jellycommon::{ - jellyobject::Object, + jellyobject::{EMPTY, Object}, routes::{u_image, u_image_fallback_person, u_node_slug, u_node_slug_player}, *, }; markup::define! { NodeCard<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>) { - @let node = nku.node; + @let node = &nku.node; @let slug = node.get(NO_SLUG).unwrap_or_default(); - div[class=&format!("card {}", aspect_class(node))] { + div[class=&format!("card {}", aspect_class(&node))] { .poster { a[href=u_node_slug(&slug)] { img[src=cover_image(&node, 512), loading="lazy"]; @@ -44,7 +44,7 @@ markup::define! { } NodeCardWide<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) { - @let node = nku.node; + @let node = &nku.node; @let slug = node.get(NO_SLUG).unwrap_or_default(); div[class="card wide"] { div[class=&format!("poster {}", aspect_class(node))] { @@ -66,9 +66,9 @@ markup::define! { } NodeCardHightlight<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>) { - @let node = nku.node; + @let node = &nku.node; @let slug = node.get(NO_SLUG).unwrap_or_default(); - @let backdrop = u_image(node.get(NO_PICTURES).unwrap_or_default().get(PICT_BACKDROP).unwrap_or_default(), 2048); + @let backdrop = u_image(node.get(NO_PICTURES).unwrap_or(EMPTY).get(PICT_BACKDROP).unwrap_or_default(), 2048); div[class="card highlight", style=format!("background-image: url(\"{backdrop}\")")] { .inner { div.overview { @@ -92,7 +92,7 @@ markup::define! { } fn cover_image(node: &Object, size: usize) -> String { - if let Some(cover) = node.get(NO_PICTURES).unwrap_or_default().get(PICT_COVER) { + if let Some(cover) = node.get(NO_PICTURES).unwrap_or(EMPTY).get(PICT_COVER) { return u_image(cover, size); } if let Some(title) = node.get(NO_TITLE) diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs index 5cd71ce..2a5848b 100644 --- a/ui/src/components/node_page.rs +++ b/ui/src/components/node_page.rs @@ -6,7 +6,7 @@ use crate::{RenderInfo, components::props::Props, page}; use jellycommon::{ - jellyobject::{Object, Tag, TypedTag}, + jellyobject::{EMPTY, Object, Tag, TypedTag}, routes::{u_image, u_node_slug_player}, *, }; @@ -30,9 +30,9 @@ page!(Player<'_>, |x| x markup::define! { NodePage<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) { - @let node = nku.node; + @let node = &nku.node; @let slug = node.get(NO_SLUG).unwrap_or_default(); - @let pics = node.get(NO_PICTURES).unwrap_or_default(); + @let pics = node.get(NO_PICTURES).unwrap_or(EMPTY); @if let Some(path) = pics.get(PICT_BACKDROP) { img.backdrop[src=u_image(path, 2048)]; } @@ -106,7 +106,7 @@ markup::define! { details { summary { @tr(ri.lang, "tag.iden") } table { - @for (key, value) in idents.entries::<&str>() { tr { + @for (key, value) in idents.entries::<str>() { tr { td { @tr(ri.lang, &format!("tag.iden.{key}")) } @if let Some(url) = external_id_url(key, value) { td { a[href=url] { pre { @value } } } @@ -130,13 +130,13 @@ markup::define! { summary { @tr(ri.lang, "tag.msrc") } table { tr { th {"Attribute"} th {"Source"} } - @for (key, source) in node.get(NO_METASOURCE).unwrap_or_default().entries::<Tag>() { tr { + @for (key, source) in node.get(NO_METASOURCE).unwrap_or(EMPTY).entries::<Tag>() { tr { td { @tr(ri.lang, &format!("tag.{key}")) } td { @tr(ri.lang, &format!("tag.msrc.{source}")) } }} @for nkey in [NO_PICTURES, NO_IDENTIFIERS, NO_RATINGS] { - @let nob = node.get(nkey).unwrap_or_default(); - @for (key, source) in nob.get(NO_METASOURCE).unwrap_or_default().entries::<Tag>() { tr { + @let nob = node.get(nkey).unwrap_or(EMPTY); + @for (key, source) in nob.get(NO_METASOURCE).unwrap_or(EMPTY).entries::<Tag>() { tr { td { @tr(ri.lang, &format!("tag.{nkey}")) ": " @tr(ri.lang, &format!("tag.{nkey}.{key}")) } td { @tr(ri.lang, &format!("tag.msrc.{source}")) } }} @@ -158,7 +158,7 @@ markup::define! { Player<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) { @let _ = ri; - @let pics = nku.node.get(NO_PICTURES).unwrap_or_default(); + @let pics = nku.node.get(NO_PICTURES).unwrap_or(EMPTY); video[id="player", poster=pics.get(PICT_COVER).map(|p| u_image(p, 2048))] {} } } @@ -169,7 +169,7 @@ markup::define! { // start * 0.8 + end * 0.2 // } -pub fn aspect_class(node: Object<'_>) -> &'static str { +pub fn aspect_class(node: &Object) -> &'static str { let kind = node.get(NO_KIND).unwrap_or(KIND_COLLECTION); match kind { KIND_VIDEO | KIND_EPISODE => "aspect-thumb", diff --git a/ui/src/components/props.rs b/ui/src/components/props.rs index e672919..db59019 100644 --- a/ui/src/components/props.rs +++ b/ui/src/components/props.rs @@ -9,38 +9,39 @@ use crate::{ format::{format_count, format_duration}, }; use chrono::DateTime; -use jellycommon::{jellyobject::TypedTag, *}; +use jellycommon::{ + jellyobject::{EMPTY, TypedTag}, + *, +}; use jellyui_locale::tr; -use std::marker::PhantomData; markup::define! { Props<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>, full: bool) { - @let node = nku.node; .props { - @if let Some(dur) = node.get(NO_DURATION) { + @if let Some(dur) = nku.node.get(NO_DURATION) { p { @format_duration(dur) } } - // @if let Some(res) = node.get(NO_TRACK) { + // @if let Some(res) = nku.node.get(NO_TRACK) { // p { @m.resolution_name() } // } - @if let Some(d) = node.get(NO_RELEASEDATE) { + @if let Some(d) = nku.node.get(NO_RELEASEDATE) { p { @if *full { @DateTime::from_timestamp_millis(d).unwrap().naive_utc().to_string() } else { @DateTime::from_timestamp_millis(d).unwrap().date_naive().to_string() }} } - @match node.get(NO_VISIBILITY).unwrap_or(VISI_VISIBLE) { + @match nku.node.get(NO_VISIBILITY).unwrap_or(VISI_VISIBLE) { VISI_REDUCED => {p.visibility{@tr(ri.lang, "prop.vis.reduced")}} VISI_HIDDEN => {p.visibility{@tr(ri.lang, "prop.vis.hidden")}} VISI_VISIBLE | _ => {} } // TODO - // @if !node.children.is_empty() { - // p { @format!("{} items", node.children.len()) } + // @if !nku.node.children.is_empty() { + // p { @format!("{} items", nku.node.children.len()) } // } - @for (kind, value) in node.get(NO_RATINGS).unwrap_or_default().entries::<f64>() { - @match TypedTag(kind, PhantomData) { + @for (kind, value) in nku.node.get(NO_RATINGS).unwrap_or(EMPTY).entries::<f64>() { + @match TypedTag::new(kind) { RTYP_YOUTUBE_LIKES => {p.likes{ @format_count(value as usize) " Likes" }} RTYP_YOUTUBE_VIEWS => {p{ @format_count(value as usize) " Views" }} RTYP_YOUTUBE_SUBSCRIBERS => {p{ @format_count(value as usize) " Subscribers" }} diff --git a/ui/src/components/user.rs b/ui/src/components/user.rs index 9dabffc..a964696 100644 --- a/ui/src/components/user.rs +++ b/ui/src/components/user.rs @@ -14,7 +14,7 @@ use jellyui_locale::tr; page!(UserSettings<'_>, |x| tr(x.ri.lang, "settings")); markup::define! { - UserSettings<'a>(ri: &'a RenderInfo<'a>, user: User<'a>) { + UserSettings<'a>(ri: &'a RenderInfo<'a>, user: &'a User) { h1 { @tr(ri.lang, "settings") } h2 { @tr(ri.lang, "settings.account") } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 93dd38a..76e2018 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -3,6 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ +#![feature(phantom_variance_markers)] pub mod components; pub(crate) mod format; mod scaffold; @@ -33,7 +34,7 @@ pub trait Page { } pub struct RenderInfo<'a> { - pub user: Option<Object<'a>>, + pub user: Option<&'a Object>, pub message: Option<(&'a str, &'a str)>, pub lang: &'a str, pub status_message: Option<&'a str>, |