Skip to main content

kcode_k1_web_code_ktool_protocol/
lib.rs

1#![forbid(unsafe_code)]
2
3use kcode_k1_chat_boxes::BoxId;
4use kcode_k1_transaction_id::TxId;
5pub use kcode_k1_web_code_document::Language;
6use kcode_k1_web_package::{AuthorityId, WebFamily};
7use semver::Version;
8use serde::Deserialize;
9use std::{
10    fmt::{Display, Formatter},
11    str::FromStr,
12};
13
14pub const WEB_CODE_CREATE: &str = "WebCodeCreate";
15pub const WEB_CODE_DOCS: &str = "WebCodeDocs";
16pub const WEB_CODE_OPEN: &str = "WebCodeOpen";
17pub const WEB_CODE_OVERWRITE: &str = "WebCodeOverwrite";
18pub const WEB_CODE_CHECK: &str = "WebCodeCheck";
19pub const WEB_CODE_PUBLISH: &str = "WebCodePublish";
20
21pub const WEB_CODE_CREATE_METADATA_TYPE: &str = "k1.web-code-create/v1";
22pub const WEB_CODE_DOCS_METADATA_TYPE: &str = "k1.web-code-docs/v2";
23pub const WEB_CODE_OPEN_METADATA_TYPE: &str = "k1.web-code-open/v2";
24pub const WEB_CODE_OVERWRITE_METADATA_TYPE: &str = "k1.web-code-overwrite/v2";
25pub const WEB_CODE_CHECK_METADATA_TYPE: &str = "k1.web-code-check/v2";
26pub const WEB_CODE_PUBLISH_METADATA_TYPE: &str = "k1.web-code-publish/v2";
27
28#[derive(Debug, Eq, PartialEq)]
29pub enum Call {
30    Create(CreateRequest),
31    Docs(LookupRequest),
32    Open(LookupRequest),
33    Overwrite(OverwriteRequest),
34    Check(CheckRequest),
35    Publish(PublishRequest),
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub enum VersionSelector {
40    Published(Version),
41    Unpublished(TxId),
42}
43impl VersionSelector {
44    pub fn text(&self) -> String {
45        match self {
46            Self::Published(value) => value.to_string(),
47            Self::Unpublished(value) => value.to_string(),
48        }
49    }
50}
51
52#[derive(Debug, Eq, PartialEq)]
53pub struct CreateRequest {
54    library: String,
55    language: Language,
56}
57impl CreateRequest {
58    pub fn library(&self) -> &str {
59        &self.library
60    }
61    pub const fn language(&self) -> Language {
62        self.language
63    }
64}
65
66#[derive(Debug, Eq, PartialEq)]
67pub struct LookupRequest {
68    library: String,
69    version: Option<VersionSelector>,
70}
71impl LookupRequest {
72    pub fn library(&self) -> &str {
73        &self.library
74    }
75    pub fn version(&self) -> Option<&VersionSelector> {
76        self.version.as_ref()
77    }
78}
79
80#[derive(Debug, Eq, PartialEq)]
81pub struct OverwriteRequest {
82    box_id: BoxId,
83    contents: String,
84}
85impl OverwriteRequest {
86    pub const fn box_id(&self) -> BoxId {
87        self.box_id
88    }
89    pub fn contents(&self) -> &str {
90        &self.contents
91    }
92    pub fn into_contents(self) -> String {
93        self.contents
94    }
95}
96
97#[derive(Debug, Eq, PartialEq)]
98pub struct CheckRequest {
99    box_id: BoxId,
100}
101impl CheckRequest {
102    pub const fn box_id(&self) -> BoxId {
103        self.box_id
104    }
105}
106
107#[derive(Debug, Eq, PartialEq)]
108pub struct PublishRequest {
109    box_id: BoxId,
110    version: Version,
111}
112impl PublishRequest {
113    pub const fn box_id(&self) -> BoxId {
114        self.box_id
115    }
116    pub fn version(&self) -> &Version {
117        &self.version
118    }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum ParseError {
123    UnknownTool,
124    InvalidArguments,
125}
126impl Display for ParseError {
127    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128        f.write_str(match self {
129            Self::UnknownTool => "unknown Web code Ktool",
130            Self::InvalidArguments => "invalid Web code Ktool arguments",
131        })
132    }
133}
134impl std::error::Error for ParseError {}
135
136#[derive(Deserialize)]
137#[serde(deny_unknown_fields)]
138struct CreateWire {
139    library: String,
140    language: String,
141}
142#[derive(Deserialize)]
143#[serde(deny_unknown_fields)]
144struct LookupWire {
145    library: String,
146    #[serde(default, deserialize_with = "optional_string")]
147    version: Option<String>,
148}
149#[derive(Deserialize)]
150#[serde(deny_unknown_fields)]
151struct OverwriteWire {
152    box_id: u64,
153    contents: String,
154}
155#[derive(Deserialize)]
156#[serde(deny_unknown_fields)]
157struct CheckWire {
158    box_id: u64,
159}
160#[derive(Deserialize)]
161#[serde(deny_unknown_fields)]
162struct PublishWire {
163    box_id: u64,
164    version: String,
165}
166
167pub fn parse(name: &str, arguments: &str) -> Result<Call, ParseError> {
168    match name {
169        WEB_CODE_CREATE => parse_create(arguments).map(Call::Create),
170        WEB_CODE_DOCS => parse_lookup(arguments).map(Call::Docs),
171        WEB_CODE_OPEN => parse_lookup(arguments).map(Call::Open),
172        WEB_CODE_OVERWRITE => parse_overwrite(arguments).map(Call::Overwrite),
173        WEB_CODE_CHECK => parse_check(arguments).map(Call::Check),
174        WEB_CODE_PUBLISH => parse_publish(arguments).map(Call::Publish),
175        _ => Err(ParseError::UnknownTool),
176    }
177}
178
179fn parse_create(arguments: &str) -> Result<CreateRequest, ParseError> {
180    let wire: CreateWire = serde_json::from_str(arguments).map_err(invalid)?;
181    validate_library(&wire.library)?;
182    let language = language(&wire.language)?;
183    Ok(CreateRequest {
184        library: wire.library,
185        language,
186    })
187}
188fn parse_lookup(arguments: &str) -> Result<LookupRequest, ParseError> {
189    let wire: LookupWire = serde_json::from_str(arguments).map_err(invalid)?;
190    validate_library(&wire.library)?;
191    let version = wire.version.map(|value| selector(&value)).transpose()?;
192    Ok(LookupRequest {
193        library: wire.library,
194        version,
195    })
196}
197fn parse_overwrite(arguments: &str) -> Result<OverwriteRequest, ParseError> {
198    let wire: OverwriteWire = serde_json::from_str(arguments).map_err(invalid)?;
199    positive(wire.box_id)?;
200    Ok(OverwriteRequest {
201        box_id: BoxId::new(wire.box_id),
202        contents: wire.contents,
203    })
204}
205fn parse_check(arguments: &str) -> Result<CheckRequest, ParseError> {
206    let wire: CheckWire = serde_json::from_str(arguments).map_err(invalid)?;
207    positive(wire.box_id)?;
208    Ok(CheckRequest {
209        box_id: BoxId::new(wire.box_id),
210    })
211}
212fn parse_publish(arguments: &str) -> Result<PublishRequest, ParseError> {
213    let wire: PublishWire = serde_json::from_str(arguments).map_err(invalid)?;
214    positive(wire.box_id)?;
215    Ok(PublishRequest {
216        box_id: BoxId::new(wire.box_id),
217        version: stable(&wire.version)?,
218    })
219}
220fn selector(text: &str) -> Result<VersionSelector, ParseError> {
221    stable(text).map(VersionSelector::Published).or_else(|_| {
222        let value = TxId::from_str(text).map_err(invalid)?;
223        (value.to_string() == text)
224            .then_some(VersionSelector::Unpublished(value))
225            .ok_or(ParseError::InvalidArguments)
226    })
227}
228fn stable(text: &str) -> Result<Version, ParseError> {
229    let value = Version::parse(text).map_err(invalid)?;
230    (value.to_string() == text && value.pre.is_empty() && value.build.is_empty())
231        .then_some(value)
232        .ok_or(ParseError::InvalidArguments)
233}
234fn validate_library(value: &str) -> Result<(), ParseError> {
235    WebFamily::new(AuthorityId::new(TxId::from_bytes([0; 12])), value)
236        .map(|_| ())
237        .map_err(invalid)
238}
239fn language(value: &str) -> Result<Language, ParseError> {
240    match value {
241        "javascript" => Ok(Language::JavaScript),
242        "html" => Ok(Language::Html),
243        "css" => Ok(Language::Css),
244        _ => Err(ParseError::InvalidArguments),
245    }
246}
247fn positive(value: u64) -> Result<(), ParseError> {
248    (value > 0)
249        .then_some(())
250        .ok_or(ParseError::InvalidArguments)
251}
252fn optional_string<'de, D: serde::Deserializer<'de>>(
253    deserializer: D,
254) -> Result<Option<String>, D::Error> {
255    String::deserialize(deserializer).map(Some)
256}
257fn invalid<T>(_: T) -> ParseError {
258    ParseError::InvalidArguments
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    #[test]
265    fn accepts_exact_contracts() {
266        assert!(matches!(
267            parse(WEB_CODE_CREATE, r#"{"library":"alpha","language":"html"}"#),
268            Ok(Call::Create(_))
269        ));
270        assert!(matches!(
271            parse(WEB_CODE_OPEN, r#"{"library":"alpha"}"#),
272            Ok(Call::Open(_))
273        ));
274        assert!(matches!(
275            parse(WEB_CODE_DOCS, r#"{"library":"alpha","version":"1.2.3"}"#),
276            Ok(Call::Docs(_))
277        ));
278        assert!(matches!(
279            parse(
280                WEB_CODE_OPEN,
281                r#"{"library":"alpha","version":"010101010101010101010101"}"#
282            ),
283            Ok(Call::Open(_))
284        ));
285        assert!(matches!(
286            parse(WEB_CODE_OVERWRITE, r#"{"box_id":1,"contents":"x"}"#),
287            Ok(Call::Overwrite(_))
288        ));
289        assert!(matches!(
290            parse(WEB_CODE_CHECK, r#"{"box_id":2}"#),
291            Ok(Call::Check(_))
292        ));
293        assert!(matches!(
294            parse(WEB_CODE_PUBLISH, r#"{"box_id":3,"version":"0.1.0"}"#),
295            Ok(Call::Publish(_))
296        ));
297    }
298    #[test]
299    fn rejects_old_or_noncanonical_shapes() {
300        for input in [
301            r#"{"library":"alpha","version":null}"#,
302            r#"{"library":"alpha","authority":"010101010101010101010101"}"#,
303            r#"{"box_id":0}"#,
304        ] {
305            assert_eq!(
306                parse(WEB_CODE_OPEN, input),
307                Err(ParseError::InvalidArguments)
308            );
309        }
310        assert_eq!(
311            parse(WEB_CODE_CHECK, r#"{"box_id":"1"}"#),
312            Err(ParseError::InvalidArguments)
313        );
314        assert_eq!(
315            parse(WEB_CODE_PUBLISH, r#"{"box_id":1,"version":"1.0.0-pre"}"#),
316            Err(ParseError::InvalidArguments)
317        );
318    }
319}