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