1use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7use serde::{Deserialize, Serialize};
8
9use crate::persistence::{self, StoreError};
10use crate::records::{ModelRecord, ModelState};
11
12const STORE_FILE: &str = "models.json";
13const SCHEMA_VERSION: u32 = 1;
14
15#[derive(Debug, thiserror::Error)]
17pub enum RegistryError {
18 #[error("corrupt registry store: {0}")]
21 CorruptStore(String),
22
23 #[error("registry store schema {found} is newer than supported {supported}")]
26 FutureSchema {
27 found: u32,
29 supported: u32,
31 },
32
33 #[error(transparent)]
35 Store(#[from] StoreError),
36}
37
38#[derive(Debug, Deserialize)]
39struct Envelope {
40 schema_version: u32,
41 models: Vec<ModelRecord>,
42}
43
44#[derive(Serialize)]
45struct EnvelopeRef<'a> {
46 schema_version: u32,
47 models: Vec<&'a ModelRecord>,
48}
49
50#[derive(Debug)]
55pub struct Registry {
56 directory: PathBuf,
57 models: BTreeMap<String, ModelRecord>,
58}
59
60impl Registry {
61 pub fn open(directory: &Path) -> Result<Self, RegistryError> {
67 let file = directory.join(STORE_FILE);
68 let models = match persistence::read_json::<Envelope>(&file) {
69 Ok(Some(envelope)) => {
70 if envelope.schema_version > SCHEMA_VERSION {
71 return Err(RegistryError::FutureSchema {
72 found: envelope.schema_version,
73 supported: SCHEMA_VERSION,
74 });
75 }
76 envelope
77 .models
78 .into_iter()
79 .map(|record| (record.id.clone(), record))
80 .collect()
81 }
82 Ok(None) => BTreeMap::new(),
83 Err(StoreError::Corrupt { source, .. }) => {
84 return Err(RegistryError::CorruptStore(source.to_string()));
85 }
86 Err(other) => return Err(RegistryError::Store(other)),
87 };
88 Ok(Self {
89 directory: directory.to_path_buf(),
90 models,
91 })
92 }
93
94 pub fn get(&self, id: &str) -> Option<&ModelRecord> {
96 self.models.get(id)
97 }
98
99 pub fn contains(&self, id: &str) -> bool {
101 self.models.contains_key(id)
102 }
103
104 pub fn len(&self) -> usize {
106 self.models.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.models.is_empty()
112 }
113
114 pub fn list(&self) -> Vec<&ModelRecord> {
116 let mut records: Vec<&ModelRecord> = self.models.values().collect();
117 records.sort_by_cached_key(|record| (record.name.to_lowercase(), record.id.clone()));
118 records
119 }
120
121 pub fn register(&mut self, record: ModelRecord) -> Result<bool, RegistryError> {
124 if self.models.get(&record.id) == Some(&record) {
125 return Ok(false);
126 }
127 self.models.insert(record.id.clone(), record);
128 self.save()?;
129 Ok(true)
130 }
131
132 pub fn register_all(&mut self, records: Vec<ModelRecord>) -> Result<usize, RegistryError> {
136 let mut changed = 0;
137 for record in records {
138 if self.models.get(&record.id) != Some(&record) {
139 self.models.insert(record.id.clone(), record);
140 changed += 1;
141 }
142 }
143 if changed > 0 {
144 self.save()?;
145 }
146 Ok(changed)
147 }
148
149 pub fn unregister(&mut self, id: &str) -> Result<Option<ModelRecord>, RegistryError> {
151 let removed = self.models.remove(id);
152 if removed.is_some() {
153 self.save()?;
154 }
155 Ok(removed)
156 }
157
158 pub fn set_state_if_present(
161 &mut self,
162 id: &str,
163 state: ModelState,
164 ) -> Result<bool, RegistryError> {
165 let Some(record) = self.models.get_mut(id) else {
166 return Ok(false);
167 };
168 if record.state == state {
169 return Ok(true);
170 }
171 record.state = state;
172 self.save()?;
173 Ok(true)
174 }
175
176 pub fn update(
182 &mut self,
183 ids: &[String],
184 transform: impl Fn(&ModelRecord) -> Option<ModelRecord>,
185 ) -> Result<Vec<ModelRecord>, RegistryError> {
186 let mut changed = Vec::new();
187 for id in ids {
188 let Some(next) = self.models.get(id).and_then(&transform) else {
189 continue;
190 };
191 if self.models.get(id) == Some(&next) {
192 continue;
193 }
194 if next.id != *id {
195 self.models.remove(id);
196 }
197 self.models.insert(next.id.clone(), next.clone());
198 changed.push(next);
199 }
200 if !changed.is_empty() {
201 self.save()?;
202 }
203 Ok(changed)
204 }
205
206 fn save(&self) -> Result<(), RegistryError> {
207 let envelope = EnvelopeRef {
208 schema_version: SCHEMA_VERSION,
209 models: self.models.values().collect(),
210 };
211 persistence::write_json_atomic(&self.directory.join(STORE_FILE), &envelope)?;
212 Ok(())
213 }
214}