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}