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 overwrite(
236        &self,
237        context: &AccessContext,
238        opened: &OpenedLibrary,
239        part: DocumentPart,
240        contents: String,
241    ) -> ServiceResult<OpenedLibrary> {
242        let user = user_bytes(context);
243        self.with_coding(user, |coding| {
244            let current = self.current_source(context, coding, user, opened)?;
245            let candidate = self
246                .inner
247                .session
248                .replacement(user, opened, current.as_ref(), part, contents)
249                .map_err(ServiceError::from_session)?;
250            let written = coding
251                .write(&candidate, &|family| self.authorize(context, family))
252                .map_err(service_coding_error)?;
253            self.inner
254                .session
255                .refresh(user, written)
256                .map_err(ServiceError::from_session)
257        })
258    }
259
260    pub fn check(
261        &self,
262        context: &AccessContext,
263        opened: &OpenedLibrary,
264    ) -> ServiceResult<FreshCheck> {
265        let user = user_bytes(context);
266        self.with_coding(user, |coding| {
267            let current = self.current_source(context, coding, user, opened)?;
268            let outcome = coding
269                .check_fresh(opened.document().id(), &|family| {
270                    self.authorize(context, family)
271                })
272                .map_err(service_coding_error)?;
273            let evidence = self
274                .inner
275                .session
276                .mint_evidence(user, opened, current.as_ref())
277                .map_err(ServiceError::from_session)?;
278            Ok(FreshCheck { outcome, evidence })
279        })
280    }
281
282    pub fn publish(
283        &self,
284        context: &AccessContext,
285        opened: &OpenedLibrary,
286        evidence: Option<&CheckEvidence>,
287    ) -> ServiceResult<PublishCompletion> {
288        let user = user_bytes(context);
289        self.with_coding(user, |coding| {
290            let current = self.current_source(context, coding, user, opened)?;
291            let evidence_matches = evidence
292                .map(|value| {
293                    self.inner
294                        .session
295                        .evidence_matches(user, opened, current.as_ref(), value)
296                        .map_err(ServiceError::from_session)
297                })
298                .transpose()?
299                .unwrap_or(false);
300            let precheck = if evidence_matches {
301                None
302            } else {
303                Some(
304                    coding
305                        .check_fresh(opened.document().id(), &|family| {
306                            self.authorize(context, family)
307                        })
308                        .map_err(service_coding_error)?,
309                )
310            };
311            let completion_evidence = self
312                .inner
313                .session
314                .mint_evidence(user, opened, current.as_ref())
315                .map_err(ServiceError::from_session)?;
316            let source_object = SourceObjectCapture::new(self.inner.objects.as_ref());
317            let publication = coding.publish_with_source_preservation(
318                opened.document().id(),
319                &|family| self.authorize(context, family),
320                &|source, _digest| source_object.preserve(source),
321                &|source, _digest| self.authorize(context, source.id().family()),
322            );
323            let publication = match publication {
324                Ok(value) => value,
325                Err(error) => {
326                    let captured = source_object.captured();
327                    return Err(service_coding_error(error).with_source_object(captured));
328                }
329            };
330            let source_object = source_object.finish().map_err(source_capture_error)?;
331            Ok(PublishCompletion {
332                source_object,
333                publication,
334                precheck,
335                evidence: completion_evidence,
336            })
337        })
338    }
339
340    fn with_coding<T>(
341        &self,
342        user: [u8; 12],
343        operation: impl FnOnce(&mut K1WebCoding) -> ServiceResult<T>,
344    ) -> ServiceResult<T> {
345        self.inner
346            .runtime
347            .with_user(user, |coding| Ok(operation(coding)))
348            .map_err(service_runtime_error)?
349    }
350
351    fn authorize(&self, context: &AccessContext, family: &WebFamily) -> Result<bool, String> {
352        authorize_workspace(context, &self.inner.groups, family)
353    }
354
355    fn require_authorized(&self, context: &AccessContext, family: &WebFamily) -> ServiceResult<()> {
356        match self.authorize(context, family) {
357            Ok(true) => Ok(()),
358            Ok(false) => Err(ServiceError::new(
359                ServiceFailure::WorkspaceDenied,
360                "workspace is denied",
361            )),
362            Err(message) => Err(ServiceError::new(ServiceFailure::Authorization, message)),
363        }
364    }
365
366    fn current_source(
367        &self,
368        context: &AccessContext,
369        coding: &K1WebCoding,
370        user: [u8; 12],
371        opened: &OpenedLibrary,
372    ) -> ServiceResult<Arc<SourcePackage>> {
373        let current = coding
374            .view(opened.document().id(), &|family| {
375                self.authorize(context, family)
376            })
377            .map_err(service_coding_error)?
378            .ok_or_else(|| ServiceError::from_session(SessionError::StaleSource))?;
379        self.inner
380            .session
381            .validate(user, opened, current.as_ref())
382            .map_err(ServiceError::from_session)?;
383        Ok(current)
384    }
385}
386
387fn user_bytes(context: &AccessContext) -> [u8; 12] {
388    *context.user().as_tx_id().as_bytes()
389}
390
391fn document_error(error: impl Display) -> ServiceError {
392    ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
393}
394
395fn source_capture_error(error: CaptureStateError) -> ServiceError {
396    let message = match error {
397        CaptureStateError::Poisoned => "source object capture mutex poisoned",
398        CaptureStateError::Missing => "publication completed without a captured source object",
399    };
400    ServiceError::new(ServiceFailure::State, message)
401}
402
403fn service_runtime_error(error: RuntimeError) -> ServiceError {
404    match error {
405        RuntimeError::Coding(error) => service_coding_error(error),
406        RuntimeError::LockPoisoned(message) => {
407            let message = if message == "runtime slot map mutex poisoned" {
408                "service slot map mutex poisoned".to_owned()
409            } else {
410                message
411            };
412            ServiceError::new(ServiceFailure::State, message)
413        }
414    }
415}
416
417fn service_coding_error(error: WebCodingError) -> ServiceError {
418    let failure = match &error {
419        WebCodingError::Authorization(_) => ServiceFailure::Authorization,
420        WebCodingError::WorkspaceDenied => ServiceFailure::WorkspaceDenied,
421        WebCodingError::CandidateUnavailable => ServiceFailure::SourceUnavailable,
422        WebCodingError::CheckFailed(_) => ServiceFailure::CheckFailed,
423        WebCodingError::SourcePreservation(_) => ServiceFailure::SourcePreservation,
424        WebCodingError::PublicReleaseDenied => ServiceFailure::PublicReleaseDenied,
425        WebCodingError::Projection(_) | WebCodingError::ProjectionAfterSubmit { .. } => {
426            ServiceFailure::Publication
427        }
428        WebCodingError::State(_)
429        | WebCodingError::Cache(_)
430        | WebCodingError::DependencyUnavailable
431        | WebCodingError::Podman(_) => ServiceFailure::Dependency,
432    };
433    ServiceError::new(failure, error.to_string())
434}