Skip to main content

kcode_k1_web_code_source_selection/
lib.rs

1#![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}