use kcode_k1_chat_boxes::BoxId;
use kcode_k1_transaction_id::TxId;
use kcode_k1_web_package::{AuthorityId, WebFamily, WebId};
use semver::Version;
use serde::{Deserialize, Deserializer};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
pub const WEB_CODE_DOCS: &str = "WebCodeDocs";
pub const WEB_CODE_OPEN: &str = "WebCodeOpen";
pub const WEB_CODE_OVERWRITE: &str = "WebCodeOverwrite";
pub const WEB_CODE_CHECK: &str = "WebCodeCheck";
pub const WEB_CODE_PUBLISH: &str = "WebCodePublish";
pub const WEB_CODE_DOCS_METADATA_TYPE: &str = "k1.web-code-docs/v1";
pub const WEB_CODE_OPEN_METADATA_TYPE: &str = "k1.web-code-open/v1";
pub const WEB_CODE_OVERWRITE_METADATA_TYPE: &str = "k1.web-code-overwrite/v1";
pub const WEB_CODE_CHECK_METADATA_TYPE: &str = "k1.web-code-check/v1";
pub const WEB_CODE_PUBLISH_METADATA_TYPE: &str = "k1.web-code-publish/v1";
#[derive(Debug, Eq, PartialEq)]
pub enum Call {
Docs(IdentityRequest),
Open(OpenRequest),
Overwrite(OverwriteRequest),
Check(IdentityRequest),
Publish(IdentityRequest),
}
#[derive(Debug, Eq, PartialEq)]
pub struct Identity(WebId);
impl Identity {
pub fn authority(&self) -> AuthorityId {
self.0.family().authority()
}
pub fn name(&self) -> &str {
self.0.family().logical_name()
}
pub fn version(&self) -> &Version {
self.0.version()
}
pub fn web_id(&self) -> &WebId {
&self.0
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct IdentityRequest(Identity);
impl IdentityRequest {
pub fn identity(&self) -> &Identity {
&self.0
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct OpenRequest {
identity: Identity,
language: Option<Language>,
}
impl OpenRequest {
pub fn identity(&self) -> &Identity {
&self.identity
}
pub const fn language(&self) -> Option<Language> {
self.language
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct OverwriteRequest {
box_id: BoxId,
contents: String,
}
impl OverwriteRequest {
pub const fn box_id(&self) -> BoxId {
self.box_id
}
pub fn contents(&self) -> &str {
&self.contents
}
pub fn into_contents(self) -> String {
self.contents
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Language {
Javascript,
Html,
Css,
}
impl Language {
pub const fn as_str(self) -> &'static str {
match self {
Self::Javascript => "javascript",
Self::Html => "html",
Self::Css => "css",
}
}
}
impl Display for Language {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParseError {
UnknownTool,
InvalidArguments,
}
impl Display for ParseError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownTool => formatter.write_str("unknown Web code Ktool"),
Self::InvalidArguments => formatter.write_str("invalid Web code Ktool arguments"),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct IdentityWire {
authority: String,
name: String,
version: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OpenWire {
authority: String,
name: String,
version: String,
#[serde(default, deserialize_with = "optional_string")]
language: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OverwriteWire {
box_id: String,
contents: String,
}
pub fn parse(name: &str, arguments: &str) -> Result<Call, ParseError> {
match name {
WEB_CODE_DOCS => parse_identity(arguments).map(Call::Docs),
WEB_CODE_OPEN => parse_open(arguments).map(Call::Open),
WEB_CODE_OVERWRITE => parse_overwrite(arguments).map(Call::Overwrite),
WEB_CODE_CHECK => parse_identity(arguments).map(Call::Check),
WEB_CODE_PUBLISH => parse_identity(arguments).map(Call::Publish),
_ => Err(ParseError::UnknownTool),
}
}
fn parse_identity(arguments: &str) -> Result<IdentityRequest, ParseError> {
let wire: IdentityWire = serde_json::from_str(arguments).map_err(invalid)?;
Ok(IdentityRequest(identity(
wire.authority,
wire.name,
wire.version,
)?))
}
fn parse_open(arguments: &str) -> Result<OpenRequest, ParseError> {
let wire: OpenWire = serde_json::from_str(arguments).map_err(invalid)?;
let language = wire
.language
.map(|value| match value.as_str() {
"javascript" => Ok(Language::Javascript),
"html" => Ok(Language::Html),
"css" => Ok(Language::Css),
_ => Err(ParseError::InvalidArguments),
})
.transpose()?;
Ok(OpenRequest {
identity: identity(wire.authority, wire.name, wire.version)?,
language,
})
}
fn parse_overwrite(arguments: &str) -> Result<OverwriteRequest, ParseError> {
let wire: OverwriteWire = serde_json::from_str(arguments).map_err(invalid)?;
if wire.box_id.is_empty() || !wire.box_id.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(ParseError::InvalidArguments);
}
let value = wire.box_id.parse::<u64>().map_err(invalid)?;
Ok(OverwriteRequest {
box_id: BoxId::new(value),
contents: wire.contents,
})
}
fn identity(authority: String, name: String, version: String) -> Result<Identity, ParseError> {
let tx = TxId::from_str(&authority).map_err(invalid)?;
if tx.to_string() != authority {
return Err(ParseError::InvalidArguments);
}
let family = WebFamily::new(AuthorityId::new(tx), name).map_err(invalid)?;
let parsed = Version::parse(&version).map_err(invalid)?;
if parsed.to_string() != version {
return Err(ParseError::InvalidArguments);
}
WebId::new(family, parsed).map(Identity).map_err(invalid)
}
fn optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Some)
}
fn invalid<T>(_: T) -> ParseError {
ParseError::InvalidArguments
}
#[cfg(test)]
mod tests {
use super::*;
const AUTHORITY: &str = "010101010101010101010101";
fn identity_json() -> String {
format!(r#"{{"authority":"{AUTHORITY}","name":"alpha-2","version":"1.2.3"}}"#)
}
#[test]
fn freezes_names_metadata_and_valid_calls() {
assert_eq!(
[
WEB_CODE_DOCS,
WEB_CODE_OPEN,
WEB_CODE_OVERWRITE,
WEB_CODE_CHECK,
WEB_CODE_PUBLISH
],
[
"WebCodeDocs",
"WebCodeOpen",
"WebCodeOverwrite",
"WebCodeCheck",
"WebCodePublish"
]
);
assert_eq!(
[
WEB_CODE_DOCS_METADATA_TYPE,
WEB_CODE_OPEN_METADATA_TYPE,
WEB_CODE_OVERWRITE_METADATA_TYPE,
WEB_CODE_CHECK_METADATA_TYPE,
WEB_CODE_PUBLISH_METADATA_TYPE
],
[
"k1.web-code-docs/v1",
"k1.web-code-open/v1",
"k1.web-code-overwrite/v1",
"k1.web-code-check/v1",
"k1.web-code-publish/v1"
]
);
for name in [WEB_CODE_DOCS, WEB_CODE_CHECK, WEB_CODE_PUBLISH] {
let call = parse(name, &identity_json()).unwrap();
let identity = match call {
Call::Docs(request) | Call::Check(request) | Call::Publish(request) => request.0,
_ => unreachable!(),
};
assert_eq!(identity.name(), "alpha-2");
assert_eq!(identity.version(), &Version::new(1, 2, 3));
assert_eq!(identity.authority().to_string(), AUTHORITY);
}
for (suffix, expected) in [("", None), (r#","language":"html""#, Some(Language::Html))] {
let arguments = format!(
r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"0.1.0"{suffix}}}"#
);
let call = parse(WEB_CODE_OPEN, &arguments).unwrap();
assert!(matches!(call, Call::Open(request) if request.language() == expected));
}
let overwrite = parse(WEB_CODE_OVERWRITE, r#"{"box_id":"000","contents":"a\nā"}"#).unwrap();
assert!(
matches!(overwrite, Call::Overwrite(request) if request.box_id().get() == 0 && request.contents() == "a\nā")
);
}
#[test]
fn rejects_unknown_or_structurally_invalid_calls() {
assert_eq!(parse("webcodedocs", "{}"), Err(ParseError::UnknownTool));
let invalid_identity = [
"[]".to_owned(),
"{}".to_owned(),
r#"{"authority":null,"name":"a","version":"1.0.0"}"#.to_owned(),
format!(r#"{{"authority":"{AUTHORITY}","name":"a","version":"1.0.0","extra":1}}"#),
format!(
r#"{{"authority":"{AUTHORITY}","authority":"{AUTHORITY}","name":"a","version":"1.0.0"}}"#
),
format!(r#"{{"authority":"{AUTHORITY}","name":"a","version":"1.0.0"}} trailing"#),
];
for arguments in invalid_identity {
assert_eq!(
parse(WEB_CODE_DOCS, &arguments),
Err(ParseError::InvalidArguments)
);
}
for arguments in [
r#"{"box_id":0,"contents":""}"#,
r#"{"box_id":"","contents":""}"#,
r#"{"box_id":"1x","contents":""}"#,
r#"{"box_id":"18446744073709551616","contents":""}"#,
r#"{"box_id":"1","contents":null}"#,
r#"{"box_id":"1","contents":"","extra":true}"#,
] {
assert_eq!(
parse(WEB_CODE_OVERWRITE, arguments),
Err(ParseError::InvalidArguments)
);
}
let null_language = format!(
r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"1.2.3","language":null}}"#
);
assert_eq!(
parse(WEB_CODE_OPEN, &null_language),
Err(ParseError::InvalidArguments)
);
}
#[test]
fn rejects_noncanonical_identity_and_language_values() {
for (authority, name, version) in [
("01010101010101010101010A", "alpha", "1.2.3"),
("0101", "alpha", "1.2.3"),
(AUTHORITY, "Alpha", "1.2.3"),
(AUTHORITY, "a--b", "1.2.3"),
(AUTHORITY, "alpha", "01.2.3"),
(AUTHORITY, "alpha", "1.2.3-pre"),
(AUTHORITY, "alpha", "1.2.3+build"),
] {
let arguments =
format!(r#"{{"authority":"{authority}","name":"{name}","version":"{version}"}}"#);
assert_eq!(
parse(WEB_CODE_CHECK, &arguments),
Err(ParseError::InvalidArguments)
);
}
for language in ["JavaScript", "js", "", "javascript "] {
let arguments = format!(
r#"{{"authority":"{AUTHORITY}","name":"alpha","version":"1.2.3","language":"{language}"}}"#
);
assert_eq!(
parse(WEB_CODE_OPEN, &arguments),
Err(ParseError::InvalidArguments)
);
}
}
}