kcode_k1_web_code_ktool_protocol/
lib.rs1#![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}