Skip to main content

kcode_k1_web_code_ktool_service/
lib.rs

1#![forbid(unsafe_code)]
2
3use kcode_k1_access::{AccessContext, Authority};
4use kcode_k1_groups::K1Groups;
5use kcode_k1_objects::K1Objects;
6use kcode_k1_web_code_authority::authorize_workspace;
7pub use kcode_k1_web_code_document::{CodeDocument, Language};
8use kcode_k1_web_code_runtime::{K1WebCodeRuntime, RuntimeError};
9pub use kcode_k1_web_code_runtime::{
10    RuntimeConfig as ServiceConfig, RuntimeRevisions as ServiceRevisions,
11};
12pub use kcode_k1_web_code_session::{DocumentPart, OpenedLibrary, OpenedSource};
13use kcode_k1_web_code_session::{SessionError, SessionScope};
14pub use kcode_k1_web_code_workspace::TxId as WorkspaceId;
15use kcode_k1_web_code_workspace::{K1WebCodeWorkspace, Workspace};
16pub use kcode_k1_web_coding::{CheckOutcome, PublishResult};
17use kcode_k1_web_coding::{K1WebCoding, WebCodingError};
18use kcode_k1_web_package::{AuthorityId, SourcePackage, WebFamily, WebId};
19use kcode_k1_web_projection::K1WebProjection;
20use kcode_k1_web_source_object::{CaptureStateError, SourceObjectCapture};
21use semver::Version;
22use std::{
23    fmt::{Display, Formatter},
24    sync::Arc,
25};
26
27pub use kcode_k1_objects::TxId;
28
29const EMPTY_DOCUMENTATION: &[u8] = b"<!-- k1-web/v1\n{\"dependencies\":[]}\n-->\n";
30type ServiceResult<T> = Result<T, ServiceError>;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct DocsResult {
34    pub version: String,
35    pub documentation: String,
36}
37
38pub struct PublishCompletion {
39    pub source_object: TxId,
40    pub publication: PublishResult,
41    pub check: CheckOutcome,
42    pub opened: OpenedLibrary,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum ServiceFailure {
47    State,
48    Authorization,
49    WorkspaceDenied,
50    FamilyExists,
51    SourceUnavailable,
52    VersionUsed,
53    ForeignHandle,
54    StaleSource,
55    InvalidDocument,
56    CheckFailed,
57    SourcePreservation,
58    PublicReleaseDenied,
59    Publication,
60    Dependency,
61}
62
63#[derive(Debug)]
64pub struct ServiceError {
65    failure: ServiceFailure,
66    message: String,
67    source_object: Option<TxId>,
68    session_error: Option<SessionError>,
69}
70
71impl ServiceError {
72    pub const fn failure(&self) -> ServiceFailure {
73        self.failure
74    }
75
76    pub const fn source_object(&self) -> Option<TxId> {
77        self.source_object
78    }
79
80    fn new(failure: ServiceFailure, message: impl Into<String>) -> Self {
81        Self {
82            failure,
83            message: message.into(),
84            source_object: None,
85            session_error: None,
86        }
87    }
88
89    fn session(error: SessionError) -> Self {
90        let failure = match error {
91            SessionError::ForeignHandle => ServiceFailure::ForeignHandle,
92            SessionError::StaleSource => ServiceFailure::StaleSource,
93            SessionError::InvalidDocument(_) => ServiceFailure::InvalidDocument,
94        };
95        Self {
96            failure,
97            message: error.to_string(),
98            source_object: None,
99            session_error: Some(error),
100        }
101    }
102
103    fn with_object(mut self, object: Option<TxId>) -> Self {
104        self.source_object = object;
105        self
106    }
107}
108
109impl Display for ServiceError {
110    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
111        formatter.write_str(&self.message)
112    }
113}
114
115impl std::error::Error for ServiceError {
116    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117        self.session_error.as_ref().map(|error| error as _)
118    }
119}
120
121struct Inner {
122    runtime: K1WebCodeRuntime,
123    groups: Arc<K1Groups>,
124    objects: Arc<K1Objects>,
125    public: Arc<K1WebProjection>,
126    workspaces: Arc<K1WebCodeWorkspace>,
127    session: SessionScope,
128}
129
130#[derive(Clone)]
131pub struct K1WebCodeKtoolService {
132    inner: Arc<Inner>,
133}
134
135impl K1WebCodeKtoolService {
136    pub fn new(
137        config: ServiceConfig,
138        groups: Arc<K1Groups>,
139        objects: Arc<K1Objects>,
140        public: Arc<K1WebProjection>,
141        workspaces: Arc<K1WebCodeWorkspace>,
142    ) -> Self {
143        Self {
144            inner: Arc::new(Inner {
145                runtime: K1WebCodeRuntime::new(config, public.clone()),
146                groups,
147                objects,
148                public,
149                workspaces,
150                session: SessionScope::new(),
151            }),
152        }
153    }
154
155    pub fn create(
156        &self,
157        context: &AccessContext,
158        authority: Authority,
159        library: &str,
160        language: Language,
161    ) -> ServiceResult<OpenedLibrary> {
162        let family = family(authority, library)?;
163        self.require_authorized(context, &family)?;
164        let snapshot = self.inner.public.snapshot().map_err(dependency)?;
165        let workspace_exists = self
166            .inner
167            .workspaces
168            .latest(&family)
169            .map_err(dependency)?
170            .is_some();
171        if !snapshot.versions(&family).is_empty() || workspace_exists {
172            return Err(ServiceError::new(
173                ServiceFailure::FamilyExists,
174                "library already exists; use WebCodeOpen",
175            ));
176        }
177        let document =
178            CodeDocument::new(family, EMPTY_DOCUMENTATION.to_vec(), language, Vec::new())
179                .map_err(document_error)?;
180        let workspace = self
181            .inner
182            .workspaces
183            .create(document)
184            .map_err(workspace_error)?;
185        self.open_workspace(authority, workspace)
186    }
187
188    pub fn docs(
189        &self,
190        context: &AccessContext,
191        authority: Authority,
192        library: &str,
193        version: Option<VersionChoice>,
194    ) -> ServiceResult<Option<DocsResult>> {
195        let family = family(authority, library)?;
196        let resolved = match version {
197            Some(VersionChoice::Published(version)) => self.published(&family, &version)?,
198            Some(VersionChoice::Unpublished(id)) => {
199                self.require_authorized(context, &family)?;
200                self.workspace(&family, id)?
201            }
202            None => self.latest_published(&family)?,
203        };
204        resolved
205            .map(|value| {
206                Ok(DocsResult {
207                    version: value.source.text(),
208                    documentation: String::from_utf8(value.document.documentation().to_vec())
209                        .map_err(document_error)?,
210                })
211            })
212            .transpose()
213    }
214
215    pub fn open(
216        &self,
217        context: &AccessContext,
218        authority: Authority,
219        library: &str,
220        version: Option<VersionChoice>,
221    ) -> ServiceResult<Option<OpenedLibrary>> {
222        let family = family(authority, library)?;
223        let resolved = match version {
224            Some(VersionChoice::Published(version)) => self.published(&family, &version)?,
225            Some(VersionChoice::Unpublished(id)) => {
226                self.require_authorized(context, &family)?;
227                self.workspace(&family, id)?
228            }
229            None => self.default_open(context, &family)?,
230        };
231        resolved
232            .map(|value| {
233                self.inner
234                    .session
235                    .open(authority_bytes(authority), value.document, value.source)
236                    .map_err(ServiceError::session)
237            })
238            .transpose()
239    }
240
241    pub fn validate_current(
242        &self,
243        context: &AccessContext,
244        authority: Authority,
245        opened: &OpenedLibrary,
246    ) -> ServiceResult<()> {
247        self.current(context, authority, opened).map(|_| ())
248    }
249
250    pub fn overwrite(
251        &self,
252        context: &AccessContext,
253        authority: Authority,
254        opened: &OpenedLibrary,
255        part: DocumentPart,
256        contents: String,
257    ) -> ServiceResult<OpenedLibrary> {
258        let current = self.current(context, authority, opened)?;
259        let replacement = self
260            .inner
261            .session
262            .replacement(
263                authority_bytes(authority),
264                opened,
265                &current.document,
266                &current.source,
267                part,
268                contents,
269            )
270            .map_err(ServiceError::session)?;
271        let workspace = match current.source {
272            OpenedSource::Published(_) => self
273                .inner
274                .workspaces
275                .branch(replacement)
276                .map_err(workspace_error)?,
277            OpenedSource::Unpublished { .. } if self.is_published(&current.document)? => self
278                .inner
279                .workspaces
280                .branch(replacement)
281                .map_err(workspace_error)?,
282            OpenedSource::Unpublished { id, revision } => self
283                .inner
284                .workspaces
285                .overwrite(id, revision, replacement)
286                .map_err(workspace_error)?,
287        };
288        self.open_workspace(authority, workspace)
289    }
290
291    pub fn check(
292        &self,
293        context: &AccessContext,
294        authority: Authority,
295        opened: &OpenedLibrary,
296    ) -> ServiceResult<CheckOutcome> {
297        let current = self.current(context, authority, opened)?;
298        let package = current
299            .document
300            .to_source_package(check_version(&current.source))
301            .map_err(document_error)?;
302        self.with_coding(context, |coding| {
303            coding
304                .write(&package, &|family| self.authorize(context, family))
305                .map_err(coding_error)?;
306            coding
307                .check_fresh(package.id(), &|family| self.authorize(context, family))
308                .map_err(coding_error)
309        })
310    }
311
312    pub fn publish(
313        &self,
314        context: &AccessContext,
315        authority: Authority,
316        opened: &OpenedLibrary,
317        version: Version,
318    ) -> ServiceResult<PublishCompletion> {
319        let current = self.current(context, authority, opened)?;
320        let id = WebId::new(current.document.family().clone(), version.clone())
321            .map_err(document_error)?;
322        if self.inner.public.load(&id).map_err(dependency)?.is_some() {
323            return Err(ServiceError::new(
324                ServiceFailure::VersionUsed,
325                "version is already published",
326            ));
327        }
328        let package = current
329            .document
330            .to_source_package(version.clone())
331            .map_err(document_error)?;
332        let (check, publication, source_object) = self.with_coding(context, |coding| {
333            coding
334                .write(&package, &|family| self.authorize(context, family))
335                .map_err(coding_error)?;
336            let check = coding
337                .check_fresh(package.id(), &|family| self.authorize(context, family))
338                .map_err(coding_error)?;
339            let capture = SourceObjectCapture::new(self.inner.objects.as_ref());
340            let publication = coding.publish_with_source_preservation(
341                package.id(),
342                &|family| self.authorize(context, family),
343                &|source, _| capture.preserve(source),
344                &|source, _| self.authorize(context, source.id().family()),
345            );
346            let publication =
347                publication.map_err(|error| coding_error(error).with_object(capture.captured()))?;
348            let object = capture.finish().map_err(capture_error)?;
349            Ok((check, publication, object))
350        })?;
351        let opened = self
352            .inner
353            .session
354            .refresh(
355                authority_bytes(authority),
356                current.document,
357                OpenedSource::Published(version),
358            )
359            .map_err(ServiceError::session)?;
360        Ok(PublishCompletion {
361            source_object,
362            publication,
363            check,
364            opened,
365        })
366    }
367
368    fn current(
369        &self,
370        context: &AccessContext,
371        authority: Authority,
372        opened: &OpenedLibrary,
373    ) -> ServiceResult<Resolved> {
374        let family = opened.document().family();
375        if family.authority().transaction_id().as_bytes() != &authority_bytes(authority) {
376            return Err(ServiceError::session(SessionError::ForeignHandle));
377        }
378        self.require_authorized(context, family)?;
379        let resolved = match opened.source() {
380            OpenedSource::Published(version) => self.published(family, version)?,
381            OpenedSource::Unpublished { id, .. } => self.workspace(family, *id)?,
382        }
383        .ok_or_else(|| ServiceError::session(SessionError::StaleSource))?;
384        self.inner
385            .session
386            .validate(
387                authority_bytes(authority),
388                opened,
389                &resolved.document,
390                &resolved.source,
391            )
392            .map_err(ServiceError::session)?;
393        Ok(resolved)
394    }
395
396    fn default_open(
397        &self,
398        context: &AccessContext,
399        family: &WebFamily,
400    ) -> ServiceResult<Option<Resolved>> {
401        if let Some(workspace) = self.inner.workspaces.latest(family).map_err(dependency)?
402            && !self.is_published(workspace.document())?
403        {
404            self.require_authorized(context, family)?;
405            return Ok(Some(from_workspace(workspace)));
406        }
407        self.latest_published(family)
408    }
409
410    fn latest_published(&self, family: &WebFamily) -> ServiceResult<Option<Resolved>> {
411        let snapshot = self.inner.public.snapshot().map_err(dependency)?;
412        let Some(version) = snapshot.versions(family).pop() else {
413            return Ok(None);
414        };
415        snapshot
416            .load(&WebId::new(family.clone(), version.clone()).map_err(document_error)?)
417            .map(|source| from_public(source, version))
418            .transpose()
419    }
420
421    fn published(&self, family: &WebFamily, version: &Version) -> ServiceResult<Option<Resolved>> {
422        let id = WebId::new(family.clone(), version.clone()).map_err(document_error)?;
423        self.inner
424            .public
425            .load(&id)
426            .map_err(dependency)?
427            .map(|source| from_public(source, version.clone()))
428            .transpose()
429    }
430
431    fn workspace(&self, family: &WebFamily, id: WorkspaceId) -> ServiceResult<Option<Resolved>> {
432        Ok(self
433            .inner
434            .workspaces
435            .get(id)
436            .map_err(dependency)?
437            .filter(|value| value.document().family() == family)
438            .map(from_workspace))
439    }
440
441    fn is_published(&self, document: &CodeDocument) -> ServiceResult<bool> {
442        let snapshot = self.inner.public.snapshot().map_err(dependency)?;
443        for version in snapshot.versions(document.family()) {
444            let id = WebId::new(document.family().clone(), version).map_err(document_error)?;
445            if let Some(source) = snapshot.load(&id)
446                && CodeDocument::from_source_package(&source).map_err(document_error)? == *document
447            {
448                return Ok(true);
449            }
450        }
451        Ok(false)
452    }
453
454    fn open_workspace(
455        &self,
456        authority: Authority,
457        workspace: Workspace,
458    ) -> ServiceResult<OpenedLibrary> {
459        let source = OpenedSource::Unpublished {
460            id: workspace.id(),
461            revision: workspace.revision(),
462        };
463        self.inner
464            .session
465            .refresh(
466                authority_bytes(authority),
467                workspace.document().clone(),
468                source,
469            )
470            .map_err(ServiceError::session)
471    }
472
473    fn with_coding<T>(
474        &self,
475        context: &AccessContext,
476        operation: impl FnOnce(&mut K1WebCoding) -> ServiceResult<T>,
477    ) -> ServiceResult<T> {
478        self.inner
479            .runtime
480            .with_user(*context.user().as_tx_id().as_bytes(), |coding| {
481                Ok(operation(coding))
482            })
483            .map_err(runtime_error)?
484    }
485
486    fn authorize(&self, context: &AccessContext, family: &WebFamily) -> Result<bool, String> {
487        authorize_workspace(context, &self.inner.groups, family)
488    }
489
490    fn require_authorized(&self, context: &AccessContext, family: &WebFamily) -> ServiceResult<()> {
491        match self.authorize(context, family) {
492            Ok(true) => Ok(()),
493            Ok(false) => Err(ServiceError::new(
494                ServiceFailure::WorkspaceDenied,
495                "workspace is denied",
496            )),
497            Err(error) => Err(ServiceError::new(ServiceFailure::Authorization, error)),
498        }
499    }
500}
501
502#[derive(Clone, Debug, Eq, PartialEq)]
503pub enum VersionChoice {
504    Published(Version),
505    Unpublished(WorkspaceId),
506}
507
508struct Resolved {
509    document: CodeDocument,
510    source: OpenedSource,
511}
512
513fn from_workspace(value: Workspace) -> Resolved {
514    let source = OpenedSource::Unpublished {
515        id: value.id(),
516        revision: value.revision(),
517    };
518    Resolved {
519        document: value.document().clone(),
520        source,
521    }
522}
523
524fn from_public(source: Arc<SourcePackage>, version: Version) -> ServiceResult<Resolved> {
525    Ok(Resolved {
526        document: CodeDocument::from_source_package(&source).map_err(document_error)?,
527        source: OpenedSource::Published(version),
528    })
529}
530
531fn family(authority: Authority, library: &str) -> ServiceResult<WebFamily> {
532    WebFamily::new(
533        AuthorityId::new(WorkspaceId::from_bytes(authority_bytes(authority))),
534        library,
535    )
536    .map_err(document_error)
537}
538
539fn authority_bytes(authority: Authority) -> [u8; 12] {
540    match authority {
541        Authority::User(value) => *value.as_tx_id().as_bytes(),
542        Authority::Group(value) => *value.txid().as_bytes(),
543    }
544}
545
546fn check_version(source: &OpenedSource) -> Version {
547    match source {
548        OpenedSource::Published(value) => value.clone(),
549        OpenedSource::Unpublished { id, .. } => {
550            let bytes = id.as_bytes();
551            Version::new(
552                u32::from_be_bytes(bytes[0..4].try_into().expect("four bytes")) as u64,
553                u32::from_be_bytes(bytes[4..8].try_into().expect("four bytes")) as u64,
554                u32::from_be_bytes(bytes[8..12].try_into().expect("four bytes")) as u64,
555            )
556        }
557    }
558}
559
560fn document_error(error: impl Display) -> ServiceError {
561    ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
562}
563fn dependency(error: impl Display) -> ServiceError {
564    ServiceError::new(ServiceFailure::Dependency, error.to_string())
565}
566fn workspace_error(error: impl Display) -> ServiceError {
567    ServiceError::new(ServiceFailure::Dependency, error.to_string())
568}
569fn runtime_error(error: RuntimeError) -> ServiceError {
570    match error {
571        RuntimeError::Coding(error) => coding_error(error),
572        RuntimeError::LockPoisoned(message) => ServiceError::new(ServiceFailure::State, message),
573    }
574}
575fn coding_error(error: WebCodingError) -> ServiceError {
576    let failure = match error {
577        WebCodingError::Authorization(_) => ServiceFailure::Authorization,
578        WebCodingError::WorkspaceDenied => ServiceFailure::WorkspaceDenied,
579        WebCodingError::CandidateUnavailable => ServiceFailure::SourceUnavailable,
580        WebCodingError::CheckFailed(_) => ServiceFailure::CheckFailed,
581        WebCodingError::SourcePreservation(_) => ServiceFailure::SourcePreservation,
582        WebCodingError::PublicReleaseDenied => ServiceFailure::PublicReleaseDenied,
583        WebCodingError::Projection(_) | WebCodingError::ProjectionAfterSubmit { .. } => {
584            ServiceFailure::Publication
585        }
586        _ => ServiceFailure::Dependency,
587    };
588    ServiceError::new(failure, error.to_string())
589}
590fn capture_error(error: CaptureStateError) -> ServiceError {
591    let message = match error {
592        CaptureStateError::Poisoned => "source object capture mutex poisoned",
593        CaptureStateError::Missing => "publication completed without a captured source object",
594    };
595    ServiceError::new(ServiceFailure::State, message)
596}