Skip to content

Commit 0a18a6b

Browse files
feat: impl json-rpc types, routing, deserializers
Introduce foundational JSON-RPC types, deserialization, and an initial swing at a POC method API for the project and package.
1 parent 189a470 commit 0a18a6b

File tree

4 files changed

+260
-0
lines changed

4 files changed

+260
-0
lines changed

src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod error;
2929
mod game;
3030
mod package;
3131
mod project;
32+
mod server;
3233
mod ts;
3334
mod ui;
3435
mod util;

src/server/method.rs

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use std::fmt::Display;
2+
use std::str::FromStr;
3+
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
7+
pub enum Error {
8+
#[error("The method '{0}' is malformed or otherwise invalid.")]
9+
BadMethod(String),
10+
#[error("The method '{0}' is invalid.")]
11+
InvalidMethodName(String),
12+
}
13+
14+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
15+
pub enum Method {
16+
Project(ProjectMethod),
17+
Package(PackageMethod),
18+
}
19+
20+
impl Display for Method {
21+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22+
panic!("Invalid use, Method cannot be serialized (despite it being possible to do so).");
23+
}
24+
}
25+
26+
/// Method namespace registration.
27+
impl FromStr for Method {
28+
type Err = Error;
29+
30+
fn from_str(value: &str) -> Result<Self, Self::Err> {
31+
let mut split = value.split('/');
32+
let (namespace, name) = (
33+
split.next().ok_or_else(|| Error::BadMethod(value.into()))?,
34+
split.next().ok_or_else(|| Error::BadMethod(value.into()))?,
35+
);
36+
37+
// Route namespaces to the appropriate enum variants for construction.
38+
Ok(match namespace {
39+
"project" => Self::Project(ProjectMethod::from_str(&name)?),
40+
"package" => Self::Package(PackageMethod::from_str(&name)?),
41+
x => Err(Error::InvalidMethodName(x.into()))?,
42+
})
43+
}
44+
}
45+
46+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
47+
pub enum ProjectMethod {
48+
/// Set the project directory context. This locks the project, if it exists.
49+
SetContext,
50+
/// Release the project directory context. This unlocks the project, if a lock exists.
51+
ReleaseContext,
52+
/// Get project metadata.
53+
GetMetadata,
54+
/// Add one or more packages to the project.
55+
AddPackages,
56+
/// Remove one or more packages from the project.
57+
RemovePackages,
58+
/// Get a list of currently installed packages.
59+
GetPackages,
60+
/// Determine if the current context is a valid project.
61+
IsValid,
62+
}
63+
64+
impl FromStr for ProjectMethod {
65+
type Err = Error;
66+
67+
fn from_str(value: &str) -> Result<Self, Self::Err> {
68+
Ok(match value {
69+
"set_context" => Self::SetContext,
70+
"release_context" => Self::ReleaseContext,
71+
"get_metadata" => Self::GetMetadata,
72+
"add_packages" => Self::AddPackages,
73+
"remove_packages" => Self::RemovePackages,
74+
"get_packages" => Self::GetPackages,
75+
"is_valid" => Self::IsValid,
76+
x => Err(Error::InvalidMethodName(x.into()))?,
77+
})
78+
}
79+
}
80+
81+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
82+
pub enum PackageMethod {
83+
/// Get metadata about this package.
84+
GetMetadata,
85+
/// Determine if the package exists within the cache.
86+
IsCached,
87+
}
88+
89+
impl FromStr for PackageMethod {
90+
type Err = Error;
91+
92+
fn from_str(value: &str) -> Result<Self, Self::Err> {
93+
Ok(match value {
94+
"get_metadata" => Self::GetMetadata,
95+
"is_cached" => Self::IsCached,
96+
x => Err(Error::InvalidMethodName(x.into()))?,
97+
})
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod test {
103+
use super::*;
104+
105+
#[test]
106+
fn test_namespace_resolve() {
107+
let method = "project/set_context";
108+
let resolved = Method::from_str(method).unwrap();
109+
assert_eq!(resolved, Method::Project(ProjectMethod::SetContext));
110+
111+
let method = "project/release_context";
112+
let resolved = Method::from_str(method).unwrap();
113+
assert_eq!(resolved, Method::Project(ProjectMethod::ReleaseContext));
114+
115+
// Assert that methods with invalid structure are caught.
116+
let method = "null";
117+
let resolved = Method::from_str(method);
118+
assert!(resolved.is_err());
119+
assert!(matches!(resolved.err().unwrap(), Error::BadMethod(..)));
120+
121+
// Assert that invalid methods with correct structure are caught.
122+
let method = "null/null";
123+
let resolved = Method::from_str(method);
124+
assert!(resolved.is_err());
125+
assert!(matches!(
126+
resolved.err().unwrap(),
127+
Error::InvalidMethodName(..),
128+
));
129+
130+
// Assert that name resolution can handle bad names.
131+
let name = "null";
132+
let resolved = ProjectMethod::from_str(name);
133+
assert!(resolved.is_err());
134+
assert!(matches!(
135+
resolved.err().unwrap(),
136+
Error::InvalidMethodName(..),
137+
));
138+
139+
let name = "null";
140+
let resolved = PackageMethod::from_str(name);
141+
assert!(resolved.is_err());
142+
assert!(matches!(
143+
resolved.err().unwrap(),
144+
Error::InvalidMethodName(..),
145+
));
146+
}
147+
}

src/server/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
mod method;
2+
mod proto;
3+
4+
/// The daemon's entrypoint. This is a psuedo event loop which does the following in step:
5+
/// 1. Read JSON-RPC input(s) from stdin.
6+
/// 2. Route each input.
7+
/// 3. Serialize the output and write to stdout.
8+
pub async fn start() {}

src/server/proto.rs

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::str::FromStr;
2+
3+
use serde::{Deserialize, Serialize};
4+
use serde_json::Value;
5+
use serde_with::{serde_as, DisplayFromStr};
6+
7+
use crate::server::method::Method;
8+
9+
#[derive(thiserror::Error, Debug)]
10+
pub enum Error {
11+
#[error("The provided message '{0}' could not be parsed as JSON.")]
12+
InvalidMessage(String),
13+
}
14+
15+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
16+
#[serde(untagged)]
17+
pub enum Message {
18+
Request(Request),
19+
Response(Response),
20+
}
21+
22+
impl Message {
23+
pub fn from_json(json: &str) -> Result<Self, Error> {
24+
serde_json::from_str::<Message>(json).map_err(|e| Error::InvalidMessage(e.to_string()))
25+
}
26+
}
27+
28+
/// This FromStr wrapper is here specifically for serde_with deserialization.
29+
impl FromStr for Message {
30+
type Err = Error;
31+
32+
fn from_str(s: &str) -> Result<Self, Self::Err> {
33+
Message::from_json(s)
34+
}
35+
}
36+
37+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
38+
#[serde(untagged)]
39+
pub enum Id {
40+
Int(isize),
41+
String(String),
42+
}
43+
44+
#[serde_as]
45+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
46+
pub struct Request {
47+
/// An identifier which, as per the JSON-RPC spec, can either be an integer
48+
/// or string. We use an untagged enum to allow serde to transparenly parse these types.
49+
pub id: Id,
50+
51+
/// This field is deserialized into a Method enum variant via Method::from_str.
52+
/// Unfortunately this means that errors returned from Method::from_str are lost.
53+
#[serde_as(as = "DisplayFromStr")]
54+
pub method: Method,
55+
56+
/// This field is null for notifications.
57+
#[serde(default = "Value::default")]
58+
#[serde(skip_serializing_if = "Value::is_null")]
59+
pub params: Value,
60+
}
61+
62+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
63+
pub struct Response {
64+
pub id: Id,
65+
pub content: Option<Value>,
66+
}
67+
68+
#[derive(Serialize, Deserialize, Debug)]
69+
pub struct ErrorMessage {
70+
pub id: Id,
71+
}
72+
73+
#[cfg(test)]
74+
mod test {
75+
use super::*;
76+
use crate::server::method::{PackageMethod, ProjectMethod};
77+
78+
#[test]
79+
fn test_request_deserialize() {
80+
let request = "{ \"id\": 1, \"method\": \"project/set_context\" }";
81+
let de = Message::from_str(request).unwrap();
82+
let cmp = Request {
83+
id: Id::Int(1),
84+
method: Method::Project(ProjectMethod::SetContext),
85+
params: Value::Null,
86+
};
87+
assert_eq!(de, Message::Request(cmp));
88+
89+
let request = "{ \"id\": \"oksamies\", \"method\": \"package/get_metadata\" }";
90+
let de = Message::from_str(request).unwrap();
91+
let cmp = Request {
92+
id: Id::String(String::from("oksamies")),
93+
method: Method::Package(PackageMethod::GetMetadata),
94+
params: Value::Null,
95+
};
96+
assert_eq!(de, Message::Request(cmp));
97+
98+
// At this point the error is pretty obfuscated behind serde_json::Error, so we just
99+
// check to see if an error was returned.
100+
let request = "{ \"id\": \"oksamies\", \"method\": \"null/null\" }";
101+
let de: Result<Request, serde_json::Error> = serde_json::from_str(request);
102+
assert!(de.is_err());
103+
}
104+
}

0 commit comments

Comments
 (0)