Skip to main content

kcode_k1_web_code_ktool_service/
lib.rs

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