#![forbid(unsafe_code)]
#![doc = include_str!("../Documentation.md")]
use kcode_k1_access::{AccessContext, Authority};
use kcode_k1_groups::K1Groups;
use kcode_k1_objects::K1Objects;
use kcode_k1_web_code_authority::authorize_workspace;
pub use kcode_k1_web_code_document::{CodeDocument, Language};
use kcode_k1_web_code_runtime::K1WebCodeRuntime;
pub use kcode_k1_web_code_runtime::{
RuntimeConfig as ServiceConfig, RuntimeRevisions as ServiceRevisions,
};
pub use kcode_k1_web_code_service_errors::{ServiceError, ServiceFailure};
use kcode_k1_web_code_session::SessionScope;
pub use kcode_k1_web_code_session::{DocumentPart, OpenedLibrary, OpenedSource};
use kcode_k1_web_code_source_selection::{ResolvedSource, SourceSelection, draft_check_version};
pub use kcode_k1_web_code_source_selection::{VersionChoice, WorkspaceId};
use kcode_k1_web_code_workspace::{K1WebCodeWorkspace, Workspace};
pub use kcode_k1_web_coding::{CheckOutcome, PublishResult};
use kcode_k1_web_package::WebFamily;
use kcode_k1_web_projection::K1WebProjection;
use kcode_k1_web_source_object::SourceObjectCapture;
use semver::Version;
use std::{fmt::Display, sync::Arc};
pub use kcode_k1_objects::TxId;
const EMPTY_DOCUMENTATION: &[u8] = b"<!-- k1-web/v1\n{\"dependencies\":[]}\n-->\n";
type ServiceResult<T> = Result<T, ServiceError>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocsResult {
pub version: String,
pub documentation: String,
}
pub struct PublishCompletion {
pub source_object: TxId,
pub publication: PublishResult,
pub check: CheckOutcome,
pub opened: OpenedLibrary,
}
struct Inner {
runtime: K1WebCodeRuntime,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
workspaces: Arc<K1WebCodeWorkspace>,
selection: SourceSelection,
session: SessionScope,
}
#[derive(Clone)]
pub struct K1WebCodeKtoolService {
inner: Arc<Inner>,
}
impl K1WebCodeKtoolService {
pub fn new(
config: ServiceConfig,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
public: Arc<K1WebProjection>,
workspaces: Arc<K1WebCodeWorkspace>,
) -> Self {
Self {
inner: Arc::new(Inner {
runtime: K1WebCodeRuntime::new(config, public.clone()),
groups,
objects,
selection: SourceSelection::new(public, workspaces.clone()),
workspaces,
session: SessionScope::new(),
}),
}
}
pub fn create(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
language: Language,
) -> ServiceResult<OpenedLibrary> {
let family = self.family(authority, library)?;
self.require_authorized(context, &family)?;
if self
.inner
.selection
.family_exists(&family)
.map_err(dependency)?
{
return Err(ServiceError::new(
ServiceFailure::FamilyExists,
"library already exists; use WebCodeOpen",
));
}
let document =
CodeDocument::new(family, EMPTY_DOCUMENTATION.to_vec(), language, Vec::new())
.map_err(document_error)?;
let workspace = self
.inner
.workspaces
.create(document)
.map_err(workspace_error)?;
self.open_workspace(authority, workspace)
}
pub fn docs(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
version: Option<VersionChoice>,
) -> ServiceResult<Option<DocsResult>> {
let family = self.family(authority, library)?;
let resolved = match version {
Some(choice @ VersionChoice::Published(_)) => self.exact(&family, &choice)?,
Some(choice @ VersionChoice::Unpublished(_)) => {
self.require_authorized(context, &family)?;
self.exact(&family, &choice)?
}
None => self
.inner
.selection
.latest_published(&family)
.map_err(dependency)?,
};
resolved
.map(|value| {
Ok(DocsResult {
version: value.source().text(),
documentation: String::from_utf8(value.document().documentation().to_vec())
.map_err(document_error)?,
})
})
.transpose()
}
pub fn open(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
version: Option<VersionChoice>,
) -> ServiceResult<Option<OpenedLibrary>> {
let family = self.family(authority, library)?;
let resolved = match version {
Some(choice @ VersionChoice::Published(_)) => self.exact(&family, &choice)?,
Some(choice @ VersionChoice::Unpublished(_)) => {
self.require_authorized(context, &family)?;
self.exact(&family, &choice)?
}
None => {
let value = self
.inner
.selection
.default_open(&family)
.map_err(dependency)?;
if value
.as_ref()
.is_some_and(|item| matches!(item.source(), OpenedSource::Unpublished { .. }))
{
self.require_authorized(context, &family)?;
}
value
}
};
resolved
.map(|value| {
let (document, source) = value.into_parts();
self.inner
.session
.open(authority_bytes(authority), document, source)
.map_err(ServiceError::from_session)
})
.transpose()
}
pub fn validate_current(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<()> {
self.current(context, authority, opened).map(|_| ())
}
pub fn overwrite(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
part: DocumentPart,
contents: String,
) -> ServiceResult<OpenedLibrary> {
let current = self.current(context, authority, opened)?;
let replacement = self
.inner
.session
.replacement(
authority_bytes(authority),
opened,
current.document(),
current.source(),
part,
contents,
)
.map_err(ServiceError::from_session)?;
let workspace = match current.source() {
OpenedSource::Published(_) => self
.inner
.workspaces
.branch(replacement)
.map_err(workspace_error)?,
OpenedSource::Unpublished { .. }
if self
.inner
.selection
.is_published(current.document())
.map_err(dependency)? =>
{
self.inner
.workspaces
.branch(replacement)
.map_err(workspace_error)?
}
OpenedSource::Unpublished { id, revision } => self
.inner
.workspaces
.overwrite(*id, *revision, replacement)
.map_err(workspace_error)?,
};
self.open_workspace(authority, workspace)
}
pub fn check(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<CheckOutcome> {
let current = self.current(context, authority, opened)?;
let package = current
.document()
.to_source_package(draft_check_version(current.source()))
.map_err(document_error)?;
self.with_coding(context, |coding| {
coding
.write(&package, &|family| self.authorize(context, family))
.map_err(ServiceError::from_coding)?;
coding
.check_fresh(package.id(), &|family| self.authorize(context, family))
.map_err(ServiceError::from_coding)
})
}
pub fn publish(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
version: Version,
) -> ServiceResult<PublishCompletion> {
let current = self.current(context, authority, opened)?;
if self
.inner
.selection
.published(current.document().family(), &version)
.map_err(dependency)?
.is_some()
{
return Err(ServiceError::new(
ServiceFailure::VersionUsed,
"version is already published",
));
}
let package = current
.document()
.to_source_package(version.clone())
.map_err(document_error)?;
let (check, publication, source_object) = self.with_coding(context, |coding| {
coding
.write(&package, &|family| self.authorize(context, family))
.map_err(ServiceError::from_coding)?;
let check = coding
.check_fresh(package.id(), &|family| self.authorize(context, family))
.map_err(ServiceError::from_coding)?;
let capture = SourceObjectCapture::new(self.inner.objects.as_ref());
let publication = coding.publish_with_source_preservation(
package.id(),
&|family| self.authorize(context, family),
&|source, _| capture.preserve(source),
&|source, _| self.authorize(context, source.id().family()),
);
let publication = publication.map_err(|error| {
ServiceError::from_coding(error).with_source_object(capture.captured())
})?;
let object = capture.finish().map_err(ServiceError::from_capture)?;
Ok((check, publication, object))
})?;
let (document, _) = current.into_parts();
let opened = self
.inner
.session
.refresh(
authority_bytes(authority),
document,
OpenedSource::Published(version),
)
.map_err(ServiceError::from_session)?;
Ok(PublishCompletion {
source_object,
publication,
check,
opened,
})
}
fn current(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<ResolvedSource> {
let family = opened.document().family();
if family.authority().transaction_id().as_bytes() != &authority_bytes(authority) {
return Err(ServiceError::from_session(
kcode_k1_web_code_session::SessionError::ForeignHandle,
));
}
self.require_authorized(context, family)?;
let choice = match opened.source() {
OpenedSource::Published(version) => VersionChoice::Published(version.clone()),
OpenedSource::Unpublished { id, .. } => VersionChoice::Unpublished(*id),
};
let resolved = self
.inner
.selection
.exact(family, &choice)
.map_err(dependency)?
.ok_or_else(|| {
ServiceError::from_session(kcode_k1_web_code_session::SessionError::StaleSource)
})?;
self.inner
.session
.validate(
authority_bytes(authority),
opened,
resolved.document(),
resolved.source(),
)
.map_err(ServiceError::from_session)?;
Ok(resolved)
}
fn exact(
&self,
family: &WebFamily,
choice: &VersionChoice,
) -> ServiceResult<Option<ResolvedSource>> {
self.inner
.selection
.exact(family, choice)
.map_err(dependency)
}
fn family(&self, authority: Authority, library: &str) -> ServiceResult<WebFamily> {
SourceSelection::family(authority_bytes(authority), library).map_err(document_error)
}
fn open_workspace(
&self,
authority: Authority,
workspace: Workspace,
) -> ServiceResult<OpenedLibrary> {
let source = OpenedSource::Unpublished {
id: workspace.id(),
revision: workspace.revision(),
};
self.inner
.session
.refresh(
authority_bytes(authority),
workspace.document().clone(),
source,
)
.map_err(ServiceError::from_session)
}
fn with_coding<T>(
&self,
context: &AccessContext,
operation: impl FnOnce(&mut kcode_k1_web_coding::K1WebCoding) -> ServiceResult<T>,
) -> ServiceResult<T> {
self.inner
.runtime
.with_user(*context.user().as_tx_id().as_bytes(), |coding| {
Ok(operation(coding))
})
.map_err(ServiceError::from_runtime)?
}
fn authorize(&self, context: &AccessContext, family: &WebFamily) -> Result<bool, String> {
authorize_workspace(context, &self.inner.groups, family)
}
fn require_authorized(&self, context: &AccessContext, family: &WebFamily) -> ServiceResult<()> {
match self.authorize(context, family) {
Ok(true) => Ok(()),
Ok(false) => Err(ServiceError::new(
ServiceFailure::WorkspaceDenied,
"workspace is denied",
)),
Err(error) => Err(ServiceError::new(ServiceFailure::Authorization, error)),
}
}
}
fn authority_bytes(authority: Authority) -> [u8; 12] {
match authority {
Authority::User(value) => *value.as_tx_id().as_bytes(),
Authority::Group(value) => *value.txid().as_bytes(),
}
}
fn document_error(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
}
fn dependency(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::Dependency, error.to_string())
}
fn workspace_error(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::Dependency, error.to_string())
}