Skip to main content

kcode_k1_rust_code_ktool_service/
lib.rs

1use 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        let source = self
152            .projection
153            .open_source(family, selector)
154            .map_err(RustCodeKtoolServiceError::Projection)?
155            .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
156        if matches!(source.selector(), SourceSelector::Unpublished(_)) {
157            self.authorize(authenticated, family)?;
158        }
159        OpenedDocument::from_source(source)
160    }
161
162    pub fn latest_published(
163        &self,
164        family: &LibraryFamily,
165    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
166        if let Some(source) = self
167            .projection
168            .latest_published(family)
169            .map_err(RustCodeKtoolServiceError::Projection)?
170        {
171            return OpenedDocument::from_source(source);
172        }
173        if self
174            .projection
175            .family_exists(family)
176            .map_err(RustCodeKtoolServiceError::Projection)?
177        {
178            Err(RustCodeKtoolServiceError::NoPublishedVersion)
179        } else {
180            Err(RustCodeKtoolServiceError::LibraryAbsent)
181        }
182    }
183
184    pub fn overwrite(
185        &self,
186        authenticated: &AccessContext,
187        opened: &OpenedDocument,
188        next: RustCodeDocument,
189    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
190        let family = opened.document.identity().family();
191        self.authorize(authenticated, family)?;
192        if next.identity() != opened.document.identity() {
193            return Err(RustCodeKtoolServiceError::WorktreeChanged);
194        }
195        let source = next.into_source_package();
196        let worktree = match opened.selector() {
197            SourceSelector::Published(version) => {
198                if opened.revision().is_some() {
199                    return Err(RustCodeKtoolServiceError::WorktreeChanged);
200                }
201                let published = self
202                    .projection
203                    .open_source(family, Some(&SourceSelector::Published(version.clone())))
204                    .map_err(RustCodeKtoolServiceError::Projection)?
205                    .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
206                if published.source() != &opened.document.clone().into_source_package() {
207                    return Err(RustCodeKtoolServiceError::WorktreeChanged);
208                }
209                self.projection.fork(&source).map_err(map_write_error)?
210            }
211            SourceSelector::Unpublished(id) => {
212                let expected_revision = opened
213                    .revision()
214                    .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
215                let current = self
216                    .projection
217                    .load_unpublished(family, *id)
218                    .map_err(RustCodeKtoolServiceError::Projection)?
219                    .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
220                if current.revision() != expected_revision
221                    || current.source() != &opened.document.clone().into_source_package()
222                {
223                    return Err(RustCodeKtoolServiceError::WorktreeChanged);
224                }
225                self.projection
226                    .overwrite(*id, expected_revision, &source)
227                    .map_err(map_write_error)?
228            }
229        };
230        OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
231    }
232
233    pub fn versioned(
234        &self,
235        document: &RustCodeDocument,
236        version: Version,
237    ) -> Result<RustCodeDocument, RustCodeKtoolServiceError> {
238        let package = publication_source(&document.clone().into_source_package(), version)
239            .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
240        RustCodeDocument::from_source_package(package)
241            .map_err(RustCodeKtoolServiceError::Unsupported)
242    }
243
244    pub fn check_fresh(
245        &self,
246        authenticated: &AccessContext,
247        document: &RustCodeDocument,
248    ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
249        let package = document.clone().into_source_package();
250        let coding = self.coding(authenticated)?;
251        let mut coding = coding
252            .lock()
253            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
254        let verified = coding
255            .check_fresh(&package)
256            .map_err(RustCodeKtoolServiceError::Coding)?;
257        Ok(ServiceCheck {
258            user: authenticated.user(),
259            package,
260            verified,
261        })
262    }
263
264    pub fn current_check(
265        &self,
266        authenticated: &AccessContext,
267        document: &RustCodeDocument,
268    ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
269        let package = document.clone().into_source_package();
270        let coding = self.coding(authenticated)?;
271        let mut coding = coding
272            .lock()
273            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
274        let verified = coding
275            .current_check(&package)
276            .map_err(RustCodeKtoolServiceError::Coding)?;
277        Ok(verified.map(|verified| ServiceCheck {
278            user: authenticated.user(),
279            package,
280            verified,
281        }))
282    }
283
284    pub fn publish_checked(
285        &self,
286        authenticated: &AccessContext,
287        document: &RustCodeDocument,
288        checked: ServiceCheck,
289    ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
290        let package = document.clone().into_source_package();
291        if checked.user != authenticated.user() || checked.package != package {
292            return Err(RustCodeKtoolServiceError::CheckMismatch);
293        }
294        if self
295            .projection
296            .version_exists(package.id())
297            .map_err(RustCodeKtoolServiceError::Projection)?
298        {
299            return Err(RustCodeKtoolServiceError::VersionUsed);
300        }
301        let coding = self.coding(authenticated)?;
302        let mut coding = coding
303            .lock()
304            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
305        let bytes = encode(&RustSourceTransaction::Publish(package.clone()))
306            .map_err(RustCodeKtoolServiceError::SourceEncoding)?;
307        self.objects
308            .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
309            .map_err(RustCodeKtoolServiceError::Object)?;
310        let gate = AuthorityGate {
311            authenticated,
312            groups: &self.groups,
313        };
314        coding
315            .publish_checked(&package, &checked.verified, &gate)
316            .map_err(RustCodeKtoolServiceError::Coding)
317    }
318
319    fn authorize(
320        &self,
321        authenticated: &AccessContext,
322        family: &LibraryFamily,
323    ) -> Result<(), RustCodeKtoolServiceError> {
324        match (AuthorityGate {
325            authenticated,
326            groups: &self.groups,
327        })
328        .authorize_publish(family)
329        {
330            Ok(true) => Ok(()),
331            Ok(false) => Err(RustCodeKtoolServiceError::AccessDenied),
332            Err(error) => Err(RustCodeKtoolServiceError::Authorization(error)),
333        }
334    }
335
336    fn coding(
337        &self,
338        authenticated: &AccessContext,
339    ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
340        let slot = {
341            let mut users = self.users.lock().map_err(|_| {
342                RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
343            })?;
344            users
345                .entry(authenticated.user())
346                .or_insert_with(|| Arc::new(UserCoding::default()))
347                .clone()
348        };
349        match slot.value.get_or_init(|| {
350            let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
351            K1RustCoding::open(
352                &self.cache_root,
353                user,
354                self.config.clone(),
355                self.projection.clone(),
356            )
357            .map(|coding| Arc::new(Mutex::new(coding)))
358            .map_err(|error| error.to_string())
359        }) {
360            Ok(coding) => Ok(coding.clone()),
361            Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
362                error.clone(),
363            )),
364        }
365    }
366}
367
368fn map_write_error(error: String) -> RustCodeKtoolServiceError {
369    if error == "unpublished version changed" || error == "source transaction was rejected" {
370        RustCodeKtoolServiceError::WorktreeChanged
371    } else if error == "library family already exists" {
372        RustCodeKtoolServiceError::LibraryExists
373    } else {
374        RustCodeKtoolServiceError::Projection(error)
375    }
376}
377
378struct AuthorityGate<'a> {
379    authenticated: &'a AccessContext,
380    groups: &'a K1Groups,
381}
382
383impl RustPublishAuthorization for AuthorityGate<'_> {
384    fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
385        let authority = family.authority();
386        let authority = authority.transaction_id();
387        if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
388            return Ok(true);
389        }
390        let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
391        if group.sentinel().is_some()
392            || self
393                .authenticated
394                .filter()
395                .contains(Authority::Group(group))
396        {
397            return Ok(false);
398        }
399        let memberships = self
400            .groups
401            .memberships(self.authenticated.user(), self.authenticated.model())?;
402        Ok(memberships.shared_groups().contains(&group))
403    }
404}