kcode_k1_web_code_source_selection/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use kcode_k1_web_code_document::CodeDocument;
5use kcode_k1_web_code_session::OpenedSource;
6pub use kcode_k1_web_code_workspace::TxId as WorkspaceId;
7use kcode_k1_web_code_workspace::{K1WebCodeWorkspace, Workspace};
8use kcode_k1_web_package::{AuthorityId, WebFamily, WebId};
9use kcode_k1_web_projection::K1WebProjection;
10use semver::Version;
11use std::sync::Arc;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum VersionChoice {
15 Published(Version),
16 Unpublished(WorkspaceId),
17}
18
19pub struct ResolvedSource {
20 document: CodeDocument,
21 source: OpenedSource,
22}
23
24impl ResolvedSource {
25 pub fn document(&self) -> &CodeDocument {
26 &self.document
27 }
28
29 pub fn source(&self) -> &OpenedSource {
30 &self.source
31 }
32
33 pub fn into_parts(self) -> (CodeDocument, OpenedSource) {
34 (self.document, self.source)
35 }
36}
37
38pub struct SourceSelection {
39 public: Arc<K1WebProjection>,
40 workspaces: Arc<K1WebCodeWorkspace>,
41}
42
43impl SourceSelection {
44 pub fn new(public: Arc<K1WebProjection>, workspaces: Arc<K1WebCodeWorkspace>) -> Self {
45 Self { public, workspaces }
46 }
47
48 pub fn family(authority: [u8; 12], library: &str) -> Result<WebFamily, String> {
49 WebFamily::new(
50 AuthorityId::new(WorkspaceId::from_bytes(authority)),
51 library,
52 )
53 .map_err(|error| error.to_string())
54 }
55
56 pub fn family_exists(&self, family: &WebFamily) -> Result<bool, String> {
57 let published = !self.public.snapshot()?.versions(family).is_empty();
58 if published {
59 return Ok(true);
60 }
61 Ok(self.workspaces.latest(family)?.is_some())
62 }
63
64 pub fn exact(
65 &self,
66 family: &WebFamily,
67 choice: &VersionChoice,
68 ) -> Result<Option<ResolvedSource>, String> {
69 match choice {
70 VersionChoice::Published(version) => self.published(family, version),
71 VersionChoice::Unpublished(id) => self.workspace(family, *id),
72 }
73 }
74
75 pub fn default_open(&self, family: &WebFamily) -> Result<Option<ResolvedSource>, String> {
76 if let Some(workspace) = self.workspaces.latest(family)?
77 && !self.is_published(workspace.document())?
78 {
79 return Ok(Some(from_workspace(workspace)));
80 }
81 self.latest_published(family)
82 }
83
84 pub fn latest_published(&self, family: &WebFamily) -> Result<Option<ResolvedSource>, String> {
85 let snapshot = self.public.snapshot()?;
86 let Some(version) = snapshot.versions(family).pop() else {
87 return Ok(None);
88 };
89 let id = WebId::new(family.clone(), version.clone()).map_err(|error| error.to_string())?;
90 snapshot
91 .load(&id)
92 .map(|source| from_public(source.as_ref(), version))
93 .transpose()
94 }
95
96 pub fn published(
97 &self,
98 family: &WebFamily,
99 version: &Version,
100 ) -> Result<Option<ResolvedSource>, String> {
101 let id = WebId::new(family.clone(), version.clone()).map_err(|error| error.to_string())?;
102 self.public
103 .load(&id)?
104 .map(|source| from_public(source.as_ref(), version.clone()))
105 .transpose()
106 }
107
108 pub fn workspace(
109 &self,
110 family: &WebFamily,
111 id: WorkspaceId,
112 ) -> Result<Option<ResolvedSource>, String> {
113 Ok(self
114 .workspaces
115 .get(id)?
116 .filter(|value| value.document().family() == family)
117 .map(from_workspace))
118 }
119
120 pub fn is_published(&self, document: &CodeDocument) -> Result<bool, String> {
121 let snapshot = self.public.snapshot()?;
122 for version in snapshot.versions(document.family()) {
123 let id = WebId::new(document.family().clone(), version)
124 .map_err(|error| error.to_string())?;
125 if let Some(source) = snapshot.load(&id)
126 && CodeDocument::from_source_package(source.as_ref())
127 .map_err(|error| error.to_string())?
128 == *document
129 {
130 return Ok(true);
131 }
132 }
133 Ok(false)
134 }
135}
136
137pub fn draft_check_version(source: &OpenedSource) -> Version {
138 match source {
139 OpenedSource::Published(version) => version.clone(),
140 OpenedSource::Unpublished { id, .. } => {
141 let bytes = id.as_bytes();
142 Version::new(
143 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64,
144 u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as u64,
145 u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as u64,
146 )
147 }
148 }
149}
150
151fn from_workspace(value: Workspace) -> ResolvedSource {
152 let source = OpenedSource::Unpublished {
153 id: value.id(),
154 revision: value.revision(),
155 };
156 ResolvedSource {
157 document: value.document().clone(),
158 source,
159 }
160}
161
162fn from_public(
163 source: &kcode_k1_web_package::SourcePackage,
164 version: Version,
165) -> Result<ResolvedSource, String> {
166 Ok(ResolvedSource {
167 document: CodeDocument::from_source_package(source).map_err(|error| error.to_string())?,
168 source: OpenedSource::Published(version),
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use kcode_k1_web_code_document::Language;
176
177 fn family() -> WebFamily {
178 SourceSelection::family([7; 12], "sample-lib").expect("family")
179 }
180
181 #[test]
182 fn draft_versions_preserve_published_and_map_every_workspace_byte() {
183 let published = Version::new(1, 2, 3);
184 assert_eq!(
185 draft_check_version(&OpenedSource::Published(published.clone())),
186 published
187 );
188 let id = WorkspaceId::from_bytes([0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3]);
189 assert_eq!(
190 draft_check_version(&OpenedSource::Unpublished { id, revision: id }),
191 Version::new(1, 2, 3)
192 );
193 }
194
195 #[test]
196 fn family_and_resolved_source_preserve_identity() {
197 let document = CodeDocument::new(
198 family(),
199 b"<!-- k1-web/v1\n{\"dependencies\":[]}\n-->\n".to_vec(),
200 Language::JavaScript,
201 Vec::new(),
202 )
203 .expect("document");
204 let source = OpenedSource::Published(Version::new(4, 5, 6));
205 let resolved = ResolvedSource { document, source };
206 assert_eq!(resolved.document().family(), &family());
207 assert!(matches!(
208 resolved.source(),
209 OpenedSource::Published(version) if *version == Version::new(4, 5, 6)
210 ));
211 }
212}