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}