kcode_k1_rust_code_ktool_service/
lib.rs1use kcode_k1_access_types::{AccessContext, Authority, GroupId, TxId as AccessTxId, UserId};
2use kcode_k1_groups::K1Groups;
3use kcode_k1_objects::K1Objects;
4use kcode_k1_rust_code_document::{RustCodeDocument, RustCodeDocumentError};
5use kcode_k1_rust_coding::{
6 K1RustCoding, RustCodingConfig, RustCodingError, RustPublishAuthorization, VerifiedCheck,
7};
8use kcode_k1_rust_package::{LibraryFamily, SourcePackage};
9use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
10use kcode_k1_rust_transaction::{RustSourceTransaction, TransactionError, encode};
11use kcode_k1_rust_worktree::{OpenedSource, SourceSelector, publication_source};
12use kcode_k1_transaction_id::TxId as RustTxId;
13use semver::Version;
14use std::collections::HashMap;
15use std::error::Error;
16use std::fmt::{Debug, Display, Formatter};
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex, OnceLock};
19
20const OBJECT_FILENAME: &str = "rust-publication.k1rust";
21const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-transaction.v1";
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct OpenedDocument {
25 selector: SourceSelector,
26 revision: Option<RustTxId>,
27 document: RustCodeDocument,
28}
29
30impl OpenedDocument {
31 pub fn selector(&self) -> &SourceSelector {
32 &self.selector
33 }
34
35 pub const fn revision(&self) -> Option<RustTxId> {
36 self.revision
37 }
38
39 pub fn document(&self) -> &RustCodeDocument {
40 &self.document
41 }
42
43 fn from_source(source: OpenedSource) -> Result<Self, RustCodeKtoolServiceError> {
44 let selector = source.selector().clone();
45 let revision = source.revision();
46 let document = RustCodeDocument::from_source_package(source.into_source())
47 .map_err(RustCodeKtoolServiceError::Unsupported)?;
48 Ok(Self {
49 selector,
50 revision,
51 document,
52 })
53 }
54}
55
56#[derive(Debug)]
57pub enum RustCodeKtoolServiceError {
58 State(String),
59 Authorization(String),
60 AccessDenied,
61 LibraryExists,
62 LibraryAbsent,
63 NoPublishedVersion,
64 WorktreeChanged,
65 VersionUsed,
66 Unsupported(RustCodeDocumentError),
67 CodingInitialization(String),
68 Coding(RustCodingError),
69 SourceEncoding(TransactionError),
70 Object(String),
71 CheckMismatch,
72 Projection(String),
73}
74
75impl Display for RustCodeKtoolServiceError {
76 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
77 Debug::fmt(self, formatter)
78 }
79}
80
81impl Error for RustCodeKtoolServiceError {}
82
83pub struct ServiceCheck {
84 user: UserId,
85 package: SourcePackage,
86 verified: VerifiedCheck,
87}
88
89#[derive(Default)]
90struct UserCoding {
91 value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
92}
93
94pub struct RustCodeKtoolService {
95 cache_root: PathBuf,
96 config: RustCodingConfig,
97 projection: Arc<K1RustProjection>,
98 objects: Arc<K1Objects>,
99 groups: Arc<K1Groups>,
100 users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
101}
102
103impl RustCodeKtoolService {
104 pub fn new(
105 cache_root: impl AsRef<Path>,
106 config: RustCodingConfig,
107 projection: Arc<K1RustProjection>,
108 objects: Arc<K1Objects>,
109 groups: Arc<K1Groups>,
110 ) -> Self {
111 Self {
112 cache_root: cache_root.as_ref().to_path_buf(),
113 config,
114 projection,
115 objects,
116 groups,
117 users: Mutex::new(HashMap::new()),
118 }
119 }
120
121 pub fn create(
122 &self,
123 authenticated: &AccessContext,
124 family: LibraryFamily,
125 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
126 self.authorize(authenticated, &family)?;
127 if self
128 .projection
129 .family_exists(&family)
130 .map_err(RustCodeKtoolServiceError::Projection)?
131 {
132 return Err(RustCodeKtoolServiceError::LibraryExists);
133 }
134 let identity = kcode_k1_rust_package::LibraryId::new(family, Version::new(0, 0, 0))
135 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
136 let document = RustCodeDocument::approved_default(identity)
137 .map_err(RustCodeKtoolServiceError::Unsupported)?;
138 let worktree = self
139 .projection
140 .create(&document.clone().into_source_package())
141 .map_err(map_write_error)?;
142 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
143 }
144
145 pub fn open(
146 &self,
147 authenticated: &AccessContext,
148 family: &LibraryFamily,
149 selector: Option<&SourceSelector>,
150 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
151 if !matches!(selector, Some(SourceSelector::Published(_))) {
152 self.authorize(authenticated, family)?;
153 }
154 let source = self
155 .projection
156 .open_source(family, selector)
157 .map_err(RustCodeKtoolServiceError::Projection)?
158 .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
159 OpenedDocument::from_source(source)
160 }
161
162 pub fn latest_published(
163 &self,
164 family: &LibraryFamily,
165 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
166 let source = self
167 .projection
168 .latest_published(family)
169 .map_err(RustCodeKtoolServiceError::Projection)?
170 .ok_or_else(|| {
171 if self.projection.family_exists(family).unwrap_or(false) {
172 RustCodeKtoolServiceError::NoPublishedVersion
173 } else {
174 RustCodeKtoolServiceError::LibraryAbsent
175 }
176 })?;
177 OpenedDocument::from_source(source)
178 }
179
180 pub fn overwrite(
181 &self,
182 authenticated: &AccessContext,
183 opened: &OpenedDocument,
184 next: RustCodeDocument,
185 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
186 let family = opened.document.identity().family();
187 self.authorize(authenticated, family)?;
188 if next.identity() != opened.document.identity() {
189 return Err(RustCodeKtoolServiceError::WorktreeChanged);
190 }
191 let source = next.into_source_package();
192 let worktree = match opened.selector() {
193 SourceSelector::Published(version) => {
194 if opened.revision().is_some() {
195 return Err(RustCodeKtoolServiceError::WorktreeChanged);
196 }
197 let published = self
198 .projection
199 .open_source(family, Some(&SourceSelector::Published(version.clone())))
200 .map_err(RustCodeKtoolServiceError::Projection)?
201 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
202 if published.source() != &opened.document.clone().into_source_package() {
203 return Err(RustCodeKtoolServiceError::WorktreeChanged);
204 }
205 self.projection.fork(&source).map_err(map_write_error)?
206 }
207 SourceSelector::Unpublished(id) => {
208 let expected_revision = opened
209 .revision()
210 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
211 let current = self
212 .projection
213 .load_unpublished(family, *id)
214 .map_err(RustCodeKtoolServiceError::Projection)?
215 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
216 if current.revision() != expected_revision
217 || current.source() != &opened.document.clone().into_source_package()
218 {
219 return Err(RustCodeKtoolServiceError::WorktreeChanged);
220 }
221 self.projection
222 .overwrite(*id, expected_revision, &source)
223 .map_err(map_write_error)?
224 }
225 };
226 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
227 }
228
229 pub fn versioned(
230 &self,
231 document: &RustCodeDocument,
232 version: Version,
233 ) -> Result<RustCodeDocument, RustCodeKtoolServiceError> {
234 let package = publication_source(&document.clone().into_source_package(), version)
235 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
236 RustCodeDocument::from_source_package(package)
237 .map_err(RustCodeKtoolServiceError::Unsupported)
238 }
239
240 pub fn check_fresh(
241 &self,
242 authenticated: &AccessContext,
243 document: &RustCodeDocument,
244 ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
245 let package = document.clone().into_source_package();
246 let coding = self.coding(authenticated)?;
247 let mut coding = coding
248 .lock()
249 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
250 let verified = coding
251 .check_fresh(&package)
252 .map_err(RustCodeKtoolServiceError::Coding)?;
253 Ok(ServiceCheck {
254 user: authenticated.user(),
255 package,
256 verified,
257 })
258 }
259
260 pub fn current_check(
261 &self,
262 authenticated: &AccessContext,
263 document: &RustCodeDocument,
264 ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
265 let package = document.clone().into_source_package();
266 let coding = self.coding(authenticated)?;
267 let mut coding = coding
268 .lock()
269 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
270 let verified = coding
271 .current_check(&package)
272 .map_err(RustCodeKtoolServiceError::Coding)?;
273 Ok(verified.map(|verified| ServiceCheck {
274 user: authenticated.user(),
275 package,
276 verified,
277 }))
278 }
279
280 pub fn publish_checked(
281 &self,
282 authenticated: &AccessContext,
283 document: &RustCodeDocument,
284 checked: ServiceCheck,
285 ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
286 let package = document.clone().into_source_package();
287 if checked.user != authenticated.user() || checked.package != package {
288 return Err(RustCodeKtoolServiceError::CheckMismatch);
289 }
290 if self
291 .projection
292 .version_exists(package.id())
293 .map_err(RustCodeKtoolServiceError::Projection)?
294 {
295 return Err(RustCodeKtoolServiceError::VersionUsed);
296 }
297 let coding = self.coding(authenticated)?;
298 let mut coding = coding
299 .lock()
300 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
301 let bytes = encode(&RustSourceTransaction::Publish(package.clone()))
302 .map_err(RustCodeKtoolServiceError::SourceEncoding)?;
303 self.objects
304 .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
305 .map_err(RustCodeKtoolServiceError::Object)?;
306 let gate = AuthorityGate {
307 authenticated,
308 groups: &self.groups,
309 };
310 coding
311 .publish_checked(&package, &checked.verified, &gate)
312 .map_err(RustCodeKtoolServiceError::Coding)
313 }
314
315 fn authorize(
316 &self,
317 authenticated: &AccessContext,
318 family: &LibraryFamily,
319 ) -> Result<(), RustCodeKtoolServiceError> {
320 match (AuthorityGate {
321 authenticated,
322 groups: &self.groups,
323 })
324 .authorize_publish(family)
325 {
326 Ok(true) => Ok(()),
327 Ok(false) => Err(RustCodeKtoolServiceError::AccessDenied),
328 Err(error) => Err(RustCodeKtoolServiceError::Authorization(error)),
329 }
330 }
331
332 fn coding(
333 &self,
334 authenticated: &AccessContext,
335 ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
336 let slot = {
337 let mut users = self.users.lock().map_err(|_| {
338 RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
339 })?;
340 users
341 .entry(authenticated.user())
342 .or_insert_with(|| Arc::new(UserCoding::default()))
343 .clone()
344 };
345 match slot.value.get_or_init(|| {
346 let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
347 K1RustCoding::open(
348 &self.cache_root,
349 user,
350 self.config.clone(),
351 self.projection.clone(),
352 )
353 .map(|coding| Arc::new(Mutex::new(coding)))
354 .map_err(|error| error.to_string())
355 }) {
356 Ok(coding) => Ok(coding.clone()),
357 Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
358 error.clone(),
359 )),
360 }
361 }
362}
363
364fn map_write_error(error: String) -> RustCodeKtoolServiceError {
365 if error == "unpublished version changed" || error == "source transaction was rejected" {
366 RustCodeKtoolServiceError::WorktreeChanged
367 } else if error == "library family already exists" {
368 RustCodeKtoolServiceError::LibraryExists
369 } else {
370 RustCodeKtoolServiceError::Projection(error)
371 }
372}
373
374struct AuthorityGate<'a> {
375 authenticated: &'a AccessContext,
376 groups: &'a K1Groups,
377}
378
379impl RustPublishAuthorization for AuthorityGate<'_> {
380 fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
381 let authority = family.authority();
382 let authority = authority.transaction_id();
383 if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
384 return Ok(true);
385 }
386 let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
387 if group.sentinel().is_some()
388 || self
389 .authenticated
390 .filter()
391 .contains(Authority::Group(group))
392 {
393 return Ok(false);
394 }
395 let memberships = self
396 .groups
397 .memberships(self.authenticated.user(), self.authenticated.model())?;
398 Ok(memberships.shared_groups().contains(&group))
399 }
400}