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}