Skip to main content

kcode_k1_web_code_ktool_service/
lib.rs

1use kcode_k1_access::AccessContext;
2use kcode_k1_groups::K1Groups;
3use kcode_k1_objects::K1Objects;
4use kcode_k1_web_code_authority::authorize_workspace;
5pub use kcode_k1_web_code_document::{CodeDocument, Language};
6use kcode_k1_web_code_runtime::{K1WebCodeRuntime, RuntimeError};
7pub use kcode_k1_web_code_runtime::{
8    RuntimeConfig as ServiceConfig, RuntimeRevisions as ServiceRevisions,
9};
10pub use kcode_k1_web_code_session::{CheckEvidence, DocumentPart, OpenedLibrary};
11use kcode_k1_web_code_session::{SessionError, SessionScope};
12pub use kcode_k1_web_coding::{CheckOutcome, PublishResult};
13use kcode_k1_web_coding::{K1WebCoding, WebCodingError};
14use kcode_k1_web_package::{SourcePackage, WebFamily, WebId};
15use kcode_k1_web_projection::K1WebProjection;
16use kcode_k1_web_source_object::{CaptureStateError, SourceObjectCapture};
17use std::fmt::{Display, Formatter};
18use std::sync::Arc;
19
20pub use kcode_k1_objects::TxId;
21
22const EMPTY_DOCUMENTATION: &[u8] = b"<!-- k1-web/v1\n{\"dependencies\":[]}\n-->\n";
23type ServiceResult<T> = Result<T, ServiceError>;
24
25pub struct FreshCheck {
26    pub outcome: CheckOutcome,
27    evidence: CheckEvidence,
28}
29
30impl FreshCheck {
31    pub fn evidence(&self) -> &CheckEvidence {
32        &self.evidence
33    }
34}
35
36pub struct PublishCompletion {
37    pub source_object: TxId,
38    pub publication: PublishResult,
39    pub precheck: Option<CheckOutcome>,
40    pub evidence: CheckEvidence,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum ServiceFailure {
45    State,
46    Authorization,
47    WorkspaceDenied,
48    SourceUnavailable,
49    LanguageRequired,
50    LanguageMismatch,
51    ForeignHandle,
52    StaleSource,
53    InvalidDocument,
54    CheckFailed,
55    SourcePreservation,
56    PublicReleaseDenied,
57    Publication,
58    Dependency,
59}
60
61#[derive(Debug)]
62pub struct ServiceError {
63    failure: ServiceFailure,
64    message: String,
65    source_object: Option<TxId>,
66    session_error: Option<SessionError>,
67}
68
69impl ServiceError {
70    pub fn failure(&self) -> ServiceFailure {
71        self.failure
72    }
73
74    pub fn source_object(&self) -> Option<TxId> {
75        self.source_object
76    }
77
78    fn new(failure: ServiceFailure, message: impl Into<String>) -> Self {
79        Self {
80            failure,
81            message: message.into(),
82            source_object: None,
83            session_error: None,
84        }
85    }
86
87    fn from_session(error: SessionError) -> Self {
88        let failure = match &error {
89            SessionError::ForeignHandle => ServiceFailure::ForeignHandle,
90            SessionError::StaleSource => ServiceFailure::StaleSource,
91            SessionError::InvalidDocument(_) => ServiceFailure::InvalidDocument,
92        };
93        let message = match &error {
94            SessionError::ForeignHandle => {
95                "opened library does not belong to this service and user".to_owned()
96            }
97            SessionError::StaleSource => {
98                "opened library is not the current exact source".to_owned()
99            }
100            SessionError::InvalidDocument(message) => message.clone(),
101        };
102        Self {
103            failure,
104            message,
105            source_object: None,
106            session_error: Some(error),
107        }
108    }
109
110    fn with_source_object(mut self, source_object: Option<TxId>) -> Self {
111        self.source_object = source_object;
112        self
113    }
114}
115
116impl Display for ServiceError {
117    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
118        formatter.write_str(&self.message)
119    }
120}
121
122impl std::error::Error for ServiceError {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        self.session_error
125            .as_ref()
126            .map(|error| error as &(dyn std::error::Error + 'static))
127    }
128}
129
130struct Inner {
131    runtime: K1WebCodeRuntime,
132    groups: Arc<K1Groups>,
133    objects: Arc<K1Objects>,
134    session: SessionScope,
135}
136
137#[derive(Clone)]
138pub struct K1WebCodeKtoolService {
139    inner: Arc<Inner>,
140}
141
142impl K1WebCodeKtoolService {
143    pub fn new(
144        config: ServiceConfig,
145        groups: Arc<K1Groups>,
146        objects: Arc<K1Objects>,
147        projection: Arc<K1WebProjection>,
148    ) -> Self {
149        Self {
150            inner: Arc::new(Inner {
151                runtime: K1WebCodeRuntime::new(config, projection),
152                groups,
153                objects,
154                session: SessionScope::new(),
155            }),
156        }
157    }
158
159    pub fn docs(&self, context: &AccessContext, id: &WebId) -> ServiceResult<Option<String>> {
160        let user = user_bytes(context);
161        self.with_coding(user, |coding| {
162            let source = coding
163                .view(id, &|family| self.authorize(context, family))
164                .map_err(service_coding_error)?;
165            source
166                .map(|source| {
167                    let opened = self
168                        .inner
169                        .session
170                        .recover(user, source)
171                        .map_err(ServiceError::from_session)?;
172                    String::from_utf8(opened.document().documentation().to_vec()).map_err(|error| {
173                        ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
174                    })
175                })
176                .transpose()
177        })
178    }
179
180    pub fn open(
181        &self,
182        context: &AccessContext,
183        id: &WebId,
184        language: Option<Language>,
185    ) -> ServiceResult<OpenedLibrary> {
186        let user = user_bytes(context);
187        self.with_coding(user, |coding| {
188            self.require_authorized(context, id.family())?;
189            let current = coding
190                .view(id, &|family| self.authorize(context, family))
191                .map_err(service_coding_error)?;
192            let candidate = match current {
193                Some(source) => {
194                    let recovered = self
195                        .inner
196                        .session
197                        .recover(user, source)
198                        .map_err(ServiceError::from_session)?;
199                    if language.is_some_and(|value| value != recovered.document().language()) {
200                        return Err(ServiceError::new(
201                            ServiceFailure::LanguageMismatch,
202                            "supplied language does not match the current document",
203                        ));
204                    }
205                    recovered
206                        .document()
207                        .to_source_package()
208                        .map_err(document_error)?
209                }
210                None => CodeDocument::new(
211                    id.clone(),
212                    EMPTY_DOCUMENTATION.to_vec(),
213                    language.ok_or_else(|| {
214                        ServiceError::new(
215                            ServiceFailure::LanguageRequired,
216                            "language is required for an absent library",
217                        )
218                    })?,
219                    Vec::new(),
220                )
221                .map_err(document_error)?
222                .to_source_package()
223                .map_err(document_error)?,
224            };
225            let written = coding
226                .write(&candidate, &|family| self.authorize(context, family))
227                .map_err(service_coding_error)?;
228            self.inner
229                .session
230                .refresh(user, written)
231                .map_err(ServiceError::from_session)
232        })
233    }
234
235    pub fn validate_current(
236        &self,
237        context: &AccessContext,
238        opened: &OpenedLibrary,
239    ) -> ServiceResult<()> {
240        let user = user_bytes(context);
241        self.with_coding(user, |coding| {
242            self.current_source(context, coding, user, opened)?;
243            Ok(())
244        })
245    }
246
247    pub fn overwrite(
248        &self,
249        context: &AccessContext,
250        opened: &OpenedLibrary,
251        part: DocumentPart,
252        contents: String,
253    ) -> ServiceResult<OpenedLibrary> {
254        let user = user_bytes(context);
255        self.with_coding(user, |coding| {
256            let current = self.current_source(context, coding, user, opened)?;
257            let candidate = self
258                .inner
259                .session
260                .replacement(user, opened, current.as_ref(), part, contents)
261                .map_err(ServiceError::from_session)?;
262            let written = coding
263                .write(&candidate, &|family| self.authorize(context, family))
264                .map_err(service_coding_error)?;
265            self.inner
266                .session
267                .refresh(user, written)
268                .map_err(ServiceError::from_session)
269        })
270    }
271
272    pub fn check(
273        &self,
274        context: &AccessContext,
275        opened: &OpenedLibrary,
276    ) -> ServiceResult<FreshCheck> {
277        let user = user_bytes(context);
278        self.with_coding(user, |coding| {
279            let current = self.current_source(context, coding, user, opened)?;
280            let outcome = coding
281                .check_fresh(opened.document().id(), &|family| {
282                    self.authorize(context, family)
283                })
284                .map_err(service_coding_error)?;
285            let evidence = self
286                .inner
287                .session
288                .mint_evidence(user, opened, current.as_ref())
289                .map_err(ServiceError::from_session)?;
290            Ok(FreshCheck { outcome, evidence })
291        })
292    }
293
294    pub fn publish(
295        &self,
296        context: &AccessContext,
297        opened: &OpenedLibrary,
298        evidence: Option<&CheckEvidence>,
299    ) -> ServiceResult<PublishCompletion> {
300        let user = user_bytes(context);
301        self.with_coding(user, |coding| {
302            let current = self.current_source(context, coding, user, opened)?;
303            let evidence_matches = evidence
304                .map(|value| {
305                    self.inner
306                        .session
307                        .evidence_matches(user, opened, current.as_ref(), value)
308                        .map_err(ServiceError::from_session)
309                })
310                .transpose()?
311                .unwrap_or(false);
312            let precheck = if evidence_matches {
313                None
314            } else {
315                Some(
316                    coding
317                        .check_fresh(opened.document().id(), &|family| {
318                            self.authorize(context, family)
319                        })
320                        .map_err(service_coding_error)?,
321                )
322            };
323            let completion_evidence = self
324                .inner
325                .session
326                .mint_evidence(user, opened, current.as_ref())
327                .map_err(ServiceError::from_session)?;
328            let source_object = SourceObjectCapture::new(self.inner.objects.as_ref());
329            let publication = coding.publish_with_source_preservation(
330                opened.document().id(),
331                &|family| self.authorize(context, family),
332                &|source, _digest| source_object.preserve(source),
333                &|source, _digest| self.authorize(context, source.id().family()),
334            );
335            let publication = match publication {
336                Ok(value) => value,
337                Err(error) => {
338                    let captured = source_object.captured();
339                    return Err(service_coding_error(error).with_source_object(captured));
340                }
341            };
342            let source_object = source_object.finish().map_err(source_capture_error)?;
343            Ok(PublishCompletion {
344                source_object,
345                publication,
346                precheck,
347                evidence: completion_evidence,
348            })
349        })
350    }
351
352    fn with_coding<T>(
353        &self,
354        user: [u8; 12],
355        operation: impl FnOnce(&mut K1WebCoding) -> ServiceResult<T>,
356    ) -> ServiceResult<T> {
357        self.inner
358            .runtime
359            .with_user(user, |coding| Ok(operation(coding)))
360            .map_err(service_runtime_error)?
361    }
362
363    fn authorize(&self, context: &AccessContext, family: &WebFamily) -> Result<bool, String> {
364        authorize_workspace(context, &self.inner.groups, family)
365    }
366
367    fn require_authorized(&self, context: &AccessContext, family: &WebFamily) -> ServiceResult<()> {
368        match self.authorize(context, family) {
369            Ok(true) => Ok(()),
370            Ok(false) => Err(ServiceError::new(
371                ServiceFailure::WorkspaceDenied,
372                "workspace is denied",
373            )),
374            Err(message) => Err(ServiceError::new(ServiceFailure::Authorization, message)),
375        }
376    }
377
378    fn current_source(
379        &self,
380        context: &AccessContext,
381        coding: &K1WebCoding,
382        user: [u8; 12],
383        opened: &OpenedLibrary,
384    ) -> ServiceResult<Arc<SourcePackage>> {
385        let current = coding
386            .view(opened.document().id(), &|family| {
387                self.authorize(context, family)
388            })
389            .map_err(service_coding_error)?
390            .ok_or_else(|| ServiceError::from_session(SessionError::StaleSource))?;
391        self.inner
392            .session
393            .validate(user, opened, current.as_ref())
394            .map_err(ServiceError::from_session)?;
395        Ok(current)
396    }
397}
398
399fn user_bytes(context: &AccessContext) -> [u8; 12] {
400    *context.user().as_tx_id().as_bytes()
401}
402
403fn document_error(error: impl Display) -> ServiceError {
404    ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
405}
406
407fn source_capture_error(error: CaptureStateError) -> ServiceError {
408    let message = match error {
409        CaptureStateError::Poisoned => "source object capture mutex poisoned",
410        CaptureStateError::Missing => "publication completed without a captured source object",
411    };
412    ServiceError::new(ServiceFailure::State, message)
413}
414
415fn service_runtime_error(error: RuntimeError) -> ServiceError {
416    match error {
417        RuntimeError::Coding(error) => service_coding_error(error),
418        RuntimeError::LockPoisoned(message) => {
419            let message = if message == "runtime slot map mutex poisoned" {
420                "service slot map mutex poisoned".to_owned()
421            } else {
422                message
423            };
424            ServiceError::new(ServiceFailure::State, message)
425        }
426    }
427}
428
429fn service_coding_error(error: WebCodingError) -> ServiceError {
430    let failure = match &error {
431        WebCodingError::Authorization(_) => ServiceFailure::Authorization,
432        WebCodingError::WorkspaceDenied => ServiceFailure::WorkspaceDenied,
433        WebCodingError::CandidateUnavailable => ServiceFailure::SourceUnavailable,
434        WebCodingError::CheckFailed(_) => ServiceFailure::CheckFailed,
435        WebCodingError::SourcePreservation(_) => ServiceFailure::SourcePreservation,
436        WebCodingError::PublicReleaseDenied => ServiceFailure::PublicReleaseDenied,
437        WebCodingError::Projection(_) | WebCodingError::ProjectionAfterSubmit { .. } => {
438            ServiceFailure::Publication
439        }
440        WebCodingError::State(_)
441        | WebCodingError::Cache(_)
442        | WebCodingError::DependencyUnavailable
443        | WebCodingError::Podman(_) => ServiceFailure::Dependency,
444    };
445    ServiceError::new(failure, error.to_string())
446}