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