1use kcode_k1_access_types::{AccessContext, Authority, GroupId, TxId as AccessTxId, UserId};
2use kcode_k1_groups::K1Groups;
3use kcode_k1_objects::K1Objects;
4use kcode_k1_rust_code_document::{RustCodeDocument, RustCodeDocumentError};
5use kcode_k1_rust_coding::{
6 BinaryBuild, K1RustCoding, RustCodingConfig, RustCodingError, RustPublishAuthorization,
7 VerifiedCheck,
8};
9use kcode_k1_rust_package::{LibraryFamily, SourcePackage};
10use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
11use kcode_k1_rust_transaction::{RustSourceTransaction, TransactionError, encode};
12use kcode_k1_rust_worktree::{OpenedSource, SourceSelector, publication_source};
13use kcode_k1_transaction_id::TxId as RustTxId;
14use semver::Version;
15use std::collections::HashMap;
16use std::error::Error;
17use std::fmt::{Debug, Display, Formatter};
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Mutex, OnceLock};
20
21const OBJECT_FILENAME: &str = "rust-publication.k1rust";
22const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-transaction.v1";
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct OpenedDocument {
26 selector: SourceSelector,
27 revision: Option<RustTxId>,
28 document: RustCodeDocument,
29}
30
31impl OpenedDocument {
32 pub fn selector(&self) -> &SourceSelector {
33 &self.selector
34 }
35
36 pub const fn revision(&self) -> Option<RustTxId> {
37 self.revision
38 }
39
40 pub fn document(&self) -> &RustCodeDocument {
41 &self.document
42 }
43
44 fn from_source(source: OpenedSource) -> Result<Self, RustCodeKtoolServiceError> {
45 let selector = source.selector().clone();
46 let revision = source.revision();
47 let document = RustCodeDocument::from_source_package(source.into_source())
48 .map_err(RustCodeKtoolServiceError::Unsupported)?;
49 Ok(Self {
50 selector,
51 revision,
52 document,
53 })
54 }
55}
56
57#[derive(Debug)]
58pub enum RustCodeKtoolServiceError {
59 State(String),
60 Authorization(String),
61 AccessDenied,
62 LibraryExists,
63 LibraryAbsent,
64 NoPublishedVersion,
65 WorktreeChanged,
66 VersionUsed,
67 Unsupported(RustCodeDocumentError),
68 CodingInitialization(String),
69 Coding(RustCodingError),
70 SourceEncoding(TransactionError),
71 Object(String),
72 CheckMismatch,
73 Projection(String),
74}
75
76impl Display for RustCodeKtoolServiceError {
77 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
78 Debug::fmt(self, formatter)
79 }
80}
81
82impl Error for RustCodeKtoolServiceError {}
83
84pub struct ServiceCheck {
85 user: UserId,
86 package: SourcePackage,
87 verified: VerifiedCheck,
88}
89
90#[derive(Default)]
91struct UserCoding {
92 value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
93}
94
95pub struct RustCodeKtoolService {
96 cache_root: PathBuf,
97 config: RustCodingConfig,
98 projection: Arc<K1RustProjection>,
99 objects: Arc<K1Objects>,
100 groups: Arc<K1Groups>,
101 users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
102}
103
104impl RustCodeKtoolService {
105 pub fn new(
106 cache_root: impl AsRef<Path>,
107 config: RustCodingConfig,
108 projection: Arc<K1RustProjection>,
109 objects: Arc<K1Objects>,
110 groups: Arc<K1Groups>,
111 ) -> Self {
112 Self {
113 cache_root: cache_root.as_ref().to_path_buf(),
114 config,
115 projection,
116 objects,
117 groups,
118 users: Mutex::new(HashMap::new()),
119 }
120 }
121
122 pub fn create(
123 &self,
124 authenticated: &AccessContext,
125 family: LibraryFamily,
126 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
127 self.authorize(authenticated, &family)?;
128 if self
129 .projection
130 .family_exists(&family)
131 .map_err(RustCodeKtoolServiceError::Projection)?
132 {
133 return Err(RustCodeKtoolServiceError::LibraryExists);
134 }
135 let identity = kcode_k1_rust_package::LibraryId::new(family, Version::new(0, 0, 0))
136 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
137 let document = RustCodeDocument::approved_default(identity)
138 .map_err(RustCodeKtoolServiceError::Unsupported)?;
139 let worktree = self
140 .projection
141 .create(&document.clone().into_source_package())
142 .map_err(map_write_error)?;
143 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
144 }
145
146 pub fn open(
147 &self,
148 authenticated: &AccessContext,
149 family: &LibraryFamily,
150 selector: Option<&SourceSelector>,
151 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
152 let source = self
153 .projection
154 .open_source(family, selector)
155 .map_err(RustCodeKtoolServiceError::Projection)?
156 .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
157 if matches!(source.selector(), SourceSelector::Unpublished(_)) {
158 self.authorize(authenticated, family)?;
159 }
160 OpenedDocument::from_source(source)
161 }
162
163 pub fn latest_published(
164 &self,
165 family: &LibraryFamily,
166 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
167 if let Some(source) = self
168 .projection
169 .latest_published(family)
170 .map_err(RustCodeKtoolServiceError::Projection)?
171 {
172 return OpenedDocument::from_source(source);
173 }
174 if self
175 .projection
176 .family_exists(family)
177 .map_err(RustCodeKtoolServiceError::Projection)?
178 {
179 Err(RustCodeKtoolServiceError::NoPublishedVersion)
180 } else {
181 Err(RustCodeKtoolServiceError::LibraryAbsent)
182 }
183 }
184
185 pub fn build_published(
186 &self,
187 authenticated: &AccessContext,
188 family: &LibraryFamily,
189 version: &Version,
190 ) -> Result<BinaryBuild, RustCodeKtoolServiceError> {
191 self.authorize(authenticated, family)?;
192 let selector = SourceSelector::Published(version.clone());
193 let source = self
194 .projection
195 .open_source(family, Some(&selector))
196 .map_err(RustCodeKtoolServiceError::Projection)?
197 .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
198 let package = source.into_source();
199 let coding = self.coding(authenticated)?;
200 let mut coding = coding
201 .lock()
202 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
203 coding
204 .build_binary(&package, family.logical_name())
205 .map_err(RustCodeKtoolServiceError::Coding)
206 }
207
208 pub fn overwrite(
209 &self,
210 authenticated: &AccessContext,
211 opened: &OpenedDocument,
212 next: RustCodeDocument,
213 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
214 let family = opened.document.identity().family();
215 self.authorize(authenticated, family)?;
216 if next.identity() != opened.document.identity() {
217 return Err(RustCodeKtoolServiceError::WorktreeChanged);
218 }
219 let source = next.into_source_package();
220 let worktree = match opened.selector() {
221 SourceSelector::Published(version) => {
222 if opened.revision().is_some() {
223 return Err(RustCodeKtoolServiceError::WorktreeChanged);
224 }
225 let published = self
226 .projection
227 .open_source(family, Some(&SourceSelector::Published(version.clone())))
228 .map_err(RustCodeKtoolServiceError::Projection)?
229 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
230 if published.source() != &opened.document.clone().into_source_package() {
231 return Err(RustCodeKtoolServiceError::WorktreeChanged);
232 }
233 self.projection.fork(&source).map_err(map_write_error)?
234 }
235 SourceSelector::Unpublished(id) => {
236 let expected_revision = opened
237 .revision()
238 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
239 let current = self
240 .projection
241 .load_unpublished(family, *id)
242 .map_err(RustCodeKtoolServiceError::Projection)?
243 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
244 if current.revision() != expected_revision
245 || current.source() != &opened.document.clone().into_source_package()
246 {
247 return Err(RustCodeKtoolServiceError::WorktreeChanged);
248 }
249 self.projection
250 .overwrite(*id, expected_revision, &source)
251 .map_err(map_write_error)?
252 }
253 };
254 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
255 }
256
257 pub fn versioned(
258 &self,
259 document: &RustCodeDocument,
260 version: Version,
261 ) -> Result<RustCodeDocument, RustCodeKtoolServiceError> {
262 let package = publication_source(&document.clone().into_source_package(), version)
263 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
264 RustCodeDocument::from_source_package(package)
265 .map_err(RustCodeKtoolServiceError::Unsupported)
266 }
267
268 pub fn check_fresh(
269 &self,
270 authenticated: &AccessContext,
271 document: &RustCodeDocument,
272 ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
273 let package = document.clone().into_source_package();
274 let coding = self.coding(authenticated)?;
275 let mut coding = coding
276 .lock()
277 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
278 let verified = coding
279 .check_fresh(&package)
280 .map_err(RustCodeKtoolServiceError::Coding)?;
281 Ok(ServiceCheck {
282 user: authenticated.user(),
283 package,
284 verified,
285 })
286 }
287
288 pub fn current_check(
289 &self,
290 authenticated: &AccessContext,
291 document: &RustCodeDocument,
292 ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
293 let package = document.clone().into_source_package();
294 let coding = self.coding(authenticated)?;
295 let mut coding = coding
296 .lock()
297 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
298 let verified = coding
299 .current_check(&package)
300 .map_err(RustCodeKtoolServiceError::Coding)?;
301 Ok(verified.map(|verified| ServiceCheck {
302 user: authenticated.user(),
303 package,
304 verified,
305 }))
306 }
307
308 pub fn publish_checked(
309 &self,
310 authenticated: &AccessContext,
311 document: &RustCodeDocument,
312 checked: ServiceCheck,
313 ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
314 let package = document.clone().into_source_package();
315 if checked.user != authenticated.user() || checked.package != package {
316 return Err(RustCodeKtoolServiceError::CheckMismatch);
317 }
318 if self
319 .projection
320 .version_exists(package.id())
321 .map_err(RustCodeKtoolServiceError::Projection)?
322 {
323 return Err(RustCodeKtoolServiceError::VersionUsed);
324 }
325 let coding = self.coding(authenticated)?;
326 let mut coding = coding
327 .lock()
328 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
329 let bytes = encode(&RustSourceTransaction::Publish(package.clone()))
330 .map_err(RustCodeKtoolServiceError::SourceEncoding)?;
331 self.objects
332 .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
333 .map_err(RustCodeKtoolServiceError::Object)?;
334 let gate = AuthorityGate {
335 authenticated,
336 groups: &self.groups,
337 };
338 coding
339 .publish_checked(&package, &checked.verified, &gate)
340 .map_err(RustCodeKtoolServiceError::Coding)
341 }
342
343 fn authorize(
344 &self,
345 authenticated: &AccessContext,
346 family: &LibraryFamily,
347 ) -> Result<(), RustCodeKtoolServiceError> {
348 match (AuthorityGate {
349 authenticated,
350 groups: &self.groups,
351 })
352 .authorize_publish(family)
353 {
354 Ok(true) => Ok(()),
355 Ok(false) => Err(RustCodeKtoolServiceError::AccessDenied),
356 Err(error) => Err(RustCodeKtoolServiceError::Authorization(error)),
357 }
358 }
359
360 fn coding(
361 &self,
362 authenticated: &AccessContext,
363 ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
364 let slot = {
365 let mut users = self.users.lock().map_err(|_| {
366 RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
367 })?;
368 users
369 .entry(authenticated.user())
370 .or_insert_with(|| Arc::new(UserCoding::default()))
371 .clone()
372 };
373 match slot.value.get_or_init(|| {
374 let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
375 K1RustCoding::open(
376 &self.cache_root,
377 user,
378 self.config.clone(),
379 self.projection.clone(),
380 )
381 .map(|coding| Arc::new(Mutex::new(coding)))
382 .map_err(|error| error.to_string())
383 }) {
384 Ok(coding) => Ok(coding.clone()),
385 Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
386 error.clone(),
387 )),
388 }
389 }
390}
391
392fn map_write_error(error: String) -> RustCodeKtoolServiceError {
393 if error == "unpublished version changed" || error == "source transaction was rejected" {
394 RustCodeKtoolServiceError::WorktreeChanged
395 } else if error == "library family already exists" {
396 RustCodeKtoolServiceError::LibraryExists
397 } else {
398 RustCodeKtoolServiceError::Projection(error)
399 }
400}
401
402struct AuthorityGate<'a> {
403 authenticated: &'a AccessContext,
404 groups: &'a K1Groups,
405}
406
407impl RustPublishAuthorization for AuthorityGate<'_> {
408 fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
409 let authority = family.authority();
410 let authority = authority.transaction_id();
411 if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
412 return Ok(true);
413 }
414 let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
415 if group.sentinel().is_some()
416 || self
417 .authenticated
418 .filter()
419 .contains(Authority::Group(group))
420 {
421 return Ok(false);
422 }
423 let memberships = self
424 .groups
425 .memberships(self.authenticated.user(), self.authenticated.model())?;
426 Ok(memberships.shared_groups().contains(&group))
427 }
428}