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}