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