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}