Skip to main content

kcode_k1_web_code_ktool_protocol/
lib.rs

1use kcode_k1_chat_boxes::BoxId;
2use kcode_k1_transaction_id::TxId;
3use kcode_k1_web_package::{AuthorityId, WebFamily, WebId};
4use semver::Version;
5use serde::{Deserialize, Deserializer};
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8
9pub const WEB_CODE_DOCS: &str = "WebCodeDocs";
10pub const WEB_CODE_OPEN: &str = "WebCodeOpen";
11pub const WEB_CODE_OVERWRITE: &str = "WebCodeOverwrite";
12pub const WEB_CODE_CHECK: &str = "WebCodeCheck";
13pub const WEB_CODE_PUBLISH: &str = "WebCodePublish";
14
15pub const WEB_CODE_DOCS_METADATA_TYPE: &str = "k1.web-code-docs/v1";
16pub const WEB_CODE_OPEN_METADATA_TYPE: &str = "k1.web-code-open/v1";
17pub const WEB_CODE_OVERWRITE_METADATA_TYPE: &str = "k1.web-code-overwrite/v1";
18pub const WEB_CODE_CHECK_METADATA_TYPE: &str = "k1.web-code-check/v1";
19pub const WEB_CODE_PUBLISH_METADATA_TYPE: &str = "k1.web-code-publish/v1";
20
21#[derive(Debug, Eq, PartialEq)]
22pub enum Call {
23    Docs(IdentityRequest),
24    Open(OpenRequest),
25    Overwrite(OverwriteRequest),
26    Check(IdentityRequest),
27    Publish(IdentityRequest),
28}
29
30#[derive(Debug, Eq, PartialEq)]
31pub struct Identity(WebId);
32
33impl Identity {
34    pub fn authority(&self) -> AuthorityId {
35        self.0.family().authority()
36    }
37
38    pub fn name(&self) -> &str {
39        self.0.family().logical_name()
40    }
41
42    pub fn version(&self) -> &Version {
43        self.0.version()
44    }
45
46    pub fn web_id(&self) -> &WebId {
47        &self.0
48    }
49}
50
51#[derive(Debug, Eq, PartialEq)]
52pub struct IdentityRequest(Identity);
53
54impl IdentityRequest {
55    pub fn identity(&self) -> &Identity {
56        &self.0
57    }
58}
59
60#[derive(Debug, Eq, PartialEq)]
61pub struct OpenRequest {
62    identity: Identity,
63    language: Option<Language>,
64}
65
66impl OpenRequest {
67    pub fn identity(&self) -> &Identity {
68        &self.identity
69    }
70
71    pub const fn language(&self) -> Option<Language> {
72        self.language
73    }
74}
75
76#[derive(Debug, Eq, PartialEq)]
77pub struct OverwriteRequest {
78    box_id: BoxId,
79    contents: String,
80}
81
82impl OverwriteRequest {
83    pub const fn box_id(&self) -> BoxId {
84        self.box_id
85    }
86
87    pub fn contents(&self) -> &str {
88        &self.contents
89    }
90
91    pub fn into_contents(self) -> String {
92        self.contents
93    }
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum Language {
98    Javascript,
99    Html,
100    Css,
101}
102
103impl Language {
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::Javascript => "javascript",
107            Self::Html => "html",
108            Self::Css => "css",
109        }
110    }
111}
112
113impl Display for Language {
114    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
115        formatter.write_str(self.as_str())
116    }
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum ParseError {
121    UnknownTool,
122    InvalidArguments,
123}
124
125impl Display for ParseError {
126    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
127        match self {
128            Self::UnknownTool => formatter.write_str("unknown Web code Ktool"),
129            Self::InvalidArguments => formatter.write_str("invalid Web code Ktool arguments"),
130        }
131    }
132}
133
134impl std::error::Error for ParseError {}
135
136#[derive(Deserialize)]
137#[serde(deny_unknown_fields)]
138struct IdentityWire {
139    authority: String,
140    name: String,
141    version: String,
142}
143
144#[derive(Deserialize)]
145#[serde(deny_unknown_fields)]
146struct OpenWire {
147    authority: String,
148    name: String,
149    version: String,
150    #[serde(default, deserialize_with = "optional_string")]
151    language: Option<String>,
152}
153
154#[derive(Deserialize)]
155#[serde(deny_unknown_fields)]
156struct OverwriteWire {
157    box_id: String,
158    contents: String,
159}
160
161pub fn parse(name: &str, arguments: &str) -> Result<Call, ParseError> {
162    match name {
163        WEB_CODE_DOCS => parse_identity(arguments).map(Call::Docs),
164        WEB_CODE_OPEN => parse_open(arguments).map(Call::Open),
165        WEB_CODE_OVERWRITE => parse_overwrite(arguments).map(Call::Overwrite),
166        WEB_CODE_CHECK => parse_identity(arguments).map(Call::Check),
167        WEB_CODE_PUBLISH => parse_identity(arguments).map(Call::Publish),
168        _ => Err(ParseError::UnknownTool),
169    }
170}
171
172fn parse_identity(arguments: &str) -> Result<IdentityRequest, ParseError> {
173    let wire: IdentityWire = serde_json::from_str(arguments).map_err(invalid)?;
174    Ok(IdentityRequest(identity(
175        wire.authority,
176        wire.name,
177        wire.version,
178    )?))
179}
180
181fn parse_open(arguments: &str) -> Result<OpenRequest, ParseError> {
182    let wire: OpenWire = serde_json::from_str(arguments).map_err(invalid)?;
183    let language = wire
184        .language
185        .map(|value| match value.as_str() {
186            "javascript" => Ok(Language::Javascript),
187            "html" => Ok(Language::Html),
188            "css" => Ok(Language::Css),
189            _ => Err(ParseError::InvalidArguments),
190        })
191        .transpose()?;
192    Ok(OpenRequest {
193        identity: identity(wire.authority, wire.name, wire.version)?,
194        language,
195    })
196}
197
198fn parse_overwrite(arguments: &str) -> Result<OverwriteRequest, ParseError> {
199    let wire: OverwriteWire = serde_json::from_str(arguments).map_err(invalid)?;
200    if wire.box_id.is_empty() || !wire.box_id.bytes().all(|byte| byte.is_ascii_digit()) {
201        return Err(ParseError::InvalidArguments);
202    }
203    let value = wire.box_id.parse::<u64>().map_err(invalid)?;
204    Ok(OverwriteRequest {
205        box_id: BoxId::new(value),
206        contents: wire.contents,
207    })
208}
209
210fn identity(authority: String, name: String, version: String) -> Result<Identity, ParseError> {
211    let tx = TxId::from_str(&authority).map_err(invalid)?;
212    if tx.to_string() != authority {
213        return Err(ParseError::InvalidArguments);
214    }
215    let family = WebFamily::new(AuthorityId::new(tx), name).map_err(invalid)?;
216    let parsed = Version::parse(&version).map_err(invalid)?;
217    if parsed.to_string() != version {
218        return Err(ParseError::InvalidArguments);
219    }
220    WebId::new(family, parsed).map(Identity).map_err(invalid)
221}
222
223fn optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
224where
225    D: Deserializer<'de>,
226{
227    String::deserialize(deserializer).map(Some)
228}
229
230fn invalid<T>(_: T) -> ParseError {
231    ParseError::InvalidArguments
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    const AUTHORITY: &str = "010101010101010101010101";
239
240    fn identity_json() -> String {
241        format!(r#"{{"authority":"{AUTHORITY}","name":"alpha-2","version":"1.2.3"}}"#)
242    }
243
244    #[test]
245    fn freezes_names_metadata_and_valid_calls() {
246        assert_eq!(
247            [
248                WEB_CODE_DOCS,
249                WEB_CODE_OPEN,
250                WEB_CODE_OVERWRITE,
251                WEB_CODE_CHECK,
252                WEB_CODE_PUBLISH
253            ],
254            [
255                "WebCodeDocs",
256                "WebCodeOpen",
257                "WebCodeOverwrite",
258                "WebCodeCheck",
259                "WebCodePublish"
260            ]
261        );
262        assert_eq!(
263            [
264                WEB_CODE_DOCS_METADATA_TYPE,
265                WEB_CODE_OPEN_METADATA_TYPE,
266                WEB_CODE_OVERWRITE_METADATA_TYPE,
267                WEB_CODE_CHECK_METADATA_TYPE,
268                WEB_CODE_PUBLISH_METADATA_TYPE
269            ],
270            [
271                "k1.web-code-docs/v1",
272                "k1.web-code-open/v1",
273                "k1.web-code-overwrite/v1",
274                "k1.web-code-check/v1",
275                "k1.web-code-publish/v1"
276            ]
277        );
278        for name in [WEB_CODE_DOCS, WEB_CODE_CHECK, WEB_CODE_PUBLISH] {
279            let call = parse(name, &identity_json()).unwrap();
280            let identity = match call {
281                Call::Docs(request) | Call::Check(request) | Call::Publish(request) => request.0,
282                _ => unreachable!(),
283            };
284            assert_eq!(identity.name(), "alpha-2");
285            assert_eq!(identity.version(), &Version::new(1, 2, 3));
286            assert_eq!(identity.authority().to_string(), AUTHORITY);
287        }
288        for (suffix, expected) in [("", None), (r#","language":"html""#, Some(Language::Html))] {
289            let arguments = format!(
290                r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"0.1.0"{suffix}}}"#
291            );
292            let call = parse(WEB_CODE_OPEN, &arguments).unwrap();
293            assert!(matches!(call, Call::Open(request) if request.language() == expected));
294        }
295        let overwrite = parse(WEB_CODE_OVERWRITE, r#"{"box_id":"000","contents":"a\n✓"}"#).unwrap();
296        assert!(
297            matches!(overwrite, Call::Overwrite(request) if request.box_id().get() == 0 && request.contents() == "a\n✓")
298        );
299    }
300
301    #[test]
302    fn rejects_unknown_or_structurally_invalid_calls() {
303        assert_eq!(parse("webcodedocs", "{}"), Err(ParseError::UnknownTool));
304        let invalid_identity = [
305            "[]".to_owned(),
306            "{}".to_owned(),
307            r#"{"authority":null,"name":"a","version":"1.0.0"}"#.to_owned(),
308            format!(r#"{{"authority":"{AUTHORITY}","name":"a","version":"1.0.0","extra":1}}"#),
309            format!(
310                r#"{{"authority":"{AUTHORITY}","authority":"{AUTHORITY}","name":"a","version":"1.0.0"}}"#
311            ),
312            format!(r#"{{"authority":"{AUTHORITY}","name":"a","version":"1.0.0"}} trailing"#),
313        ];
314        for arguments in invalid_identity {
315            assert_eq!(
316                parse(WEB_CODE_DOCS, &arguments),
317                Err(ParseError::InvalidArguments)
318            );
319        }
320        for arguments in [
321            r#"{"box_id":0,"contents":""}"#,
322            r#"{"box_id":"","contents":""}"#,
323            r#"{"box_id":"1x","contents":""}"#,
324            r#"{"box_id":"18446744073709551616","contents":""}"#,
325            r#"{"box_id":"1","contents":null}"#,
326            r#"{"box_id":"1","contents":"","extra":true}"#,
327        ] {
328            assert_eq!(
329                parse(WEB_CODE_OVERWRITE, arguments),
330                Err(ParseError::InvalidArguments)
331            );
332        }
333        let null_language = format!(
334            r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"1.2.3","language":null}}"#
335        );
336        assert_eq!(
337            parse(WEB_CODE_OPEN, &null_language),
338            Err(ParseError::InvalidArguments)
339        );
340    }
341
342    #[test]
343    fn rejects_noncanonical_identity_and_language_values() {
344        for (authority, name, version) in [
345            ("01010101010101010101010A", "alpha", "1.2.3"),
346            ("0101", "alpha", "1.2.3"),
347            (AUTHORITY, "Alpha", "1.2.3"),
348            (AUTHORITY, "a--b", "1.2.3"),
349            (AUTHORITY, "alpha", "01.2.3"),
350            (AUTHORITY, "alpha", "1.2.3-pre"),
351            (AUTHORITY, "alpha", "1.2.3+build"),
352        ] {
353            let arguments =
354                format!(r#"{{"authority":"{authority}","name":"{name}","version":"{version}"}}"#);
355            assert_eq!(
356                parse(WEB_CODE_CHECK, &arguments),
357                Err(ParseError::InvalidArguments)
358            );
359        }
360        for language in ["JavaScript", "js", "", "javascript "] {
361            let arguments = format!(
362                r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"1.2.3","language":"{language}"}}"#
363            );
364            assert_eq!(
365                parse(WEB_CODE_OPEN, &arguments),
366                Err(ParseError::InvalidArguments)
367            );
368        }
369    }
370}