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        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}