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 ¤t.document,
266 ¤t.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(¤t.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(¤t.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}