harmos_domain/model/storage/
keys.rs1use std::fmt;
20use std::num::{NonZeroU32, NonZeroUsize};
21use serde::{Deserialize, Serialize};
23use crate::{Error, Identifier, MemorySize, Result};
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct SchemaKey {
37 pub id: Identifier,
38 pub version: NonZeroU32,
39
40 #[serde(default)]
41 pub payload_encoding: PayloadEncoding,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct ObjectValue {
47 pub schema: SchemaKey,
48 pub item: ObjectKey,
49 pub payload: Vec<u8>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
53#[serde(transparent)]
54pub struct ObjectKey(String);
55
56struct KeyWriter {
57 value: String,
58 limit: usize,
59}
60
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum PayloadEncoding {
66 #[default]
67 ProtobufV1 = 0,
68
69 PostcardV1 = 1,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
73pub enum ObjectRetention {
74 UntilReleased,
75
76 Latest,
77 Versions(NonZeroUsize),
78}
79
80impl SchemaKey {
85 pub fn require_postcard(&self) -> Result<()> {
86 if self.payload_encoding != PayloadEncoding::PostcardV1 {
87 return Err(invalid("payload requires an explicitly selected legacy decoder or migration"));
88 }
89 Ok(())
90 }
91}
92
93impl MemorySize for SchemaKey {
94 fn memory_size(&self) -> usize {
95 std::mem::size_of::<Self>().saturating_add(self.id.memory_size().saturating_sub(std::mem::size_of::<Identifier>()))
96 }
97}
98
99impl MemorySize for ObjectValue {
100 fn memory_size(&self) -> usize {
101 std::mem::size_of::<Self>()
102 .saturating_add(self.schema.memory_size().saturating_sub(std::mem::size_of::<SchemaKey>()))
103 .saturating_add(self.item.memory_size().saturating_sub(std::mem::size_of::<ObjectKey>()))
104 .saturating_add(self.payload.capacity())
105 }
106}
107
108impl MemorySize for ObjectKey {
109 fn memory_size(&self) -> usize {
110 self.0.memory_size()
111 }
112}
113
114impl ObjectKey {
115 pub fn new<K: fmt::Display + ?Sized>(key: &K, max_bytes: NonZeroUsize) -> Result<Self> {
116 object_key(key, max_bytes)
117 }
118
119 pub fn as_str(&self) -> &str {
120 &self.0
121 }
122
123 pub fn from_owned(value: String) -> Result<Self> {
124 if value.is_empty() {
125 return Err(invalid("object key is empty"));
126 }
127
128 Ok(Self(value))
129 }
130}
131
132impl fmt::Display for ObjectKey {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter.write_str(&self.0)
135 }
136}
137
138impl<'de> Deserialize<'de> for ObjectKey {
139 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
140 Self::from_owned(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
141 }
142}
143
144impl fmt::Write for KeyWriter {
145 fn write_str(&mut self, value: &str) -> fmt::Result {
146 if value.len() > self.limit - self.value.len() {
147 return Err(fmt::Error);
148 }
149 let remaining = self.value.capacity() - self.value.len();
150 if value.len() > remaining {
151 self.value.reserve_exact(value.len());
152 }
153 self.value.push_str(value);
154 Ok(())
155 }
156}
157
158impl ObjectRetention {
159 pub const fn versions(self) -> NonZeroUsize {
160 match self {
161 Self::UntilReleased | Self::Latest => NonZeroUsize::MIN,
162 Self::Versions(count) => count,
163 }
164 }
165}
166
167pub fn object_key<K: fmt::Display + ?Sized>(key: &K, limit: NonZeroUsize) -> Result<ObjectKey> {
170 let mut writer = KeyWriter { value: String::new(), limit: limit.get() };
171 fmt::write(&mut writer, format_args!("{key}")).map_err(|_| invalid("invalid or oversized object key"))?;
172 ObjectKey::from_owned(writer.value)
173}
174
175pub fn check_length(length: usize, max_payload_bytes: NonZeroUsize) -> Result<()> {
176 if length > max_payload_bytes.get() {
177 return Err(invalid("object payload exceeds byte limit"));
178 }
179 Ok(())
180}
181
182pub fn invalid(reason: &'static str) -> Error {
183 Error::InvalidInput { field: "object catalog", reason }
184}
185
186