Skip to content

Commit

Permalink
Add eval operation implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sasha committed May 17, 2020
1 parent 9b72fb3 commit 9ffcc60
Showing 1 changed file with 131 additions and 26 deletions.
157 changes: 131 additions & 26 deletions src/nrepl_server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Creates and maintain a simple NREPL server.

use std::{
borrow::Cow,
collections::HashMap,
dbg,
io::{BufReader, Read, Result as IResult, Write},
Expand All @@ -15,10 +16,25 @@ use bendy::{
encoding::{Error as EError, SingleItemEncoder, ToBencode},
};

use crate::repl::Repl;
use crate::{repl::Repl, value::Value};

const SERVER_PORT: u16 = 5555;

// TODO: implement the required code so that this hack is not required anymore.
const INITIAL_LEIN_REQUEST: &[u8] = b"(clojure.core/binding [clojure.core/*ns* (clojure.core/or (clojure.core/find-ns (clojure.core/symbol \"reply.eval-modes.nrepl\")) clojure.core/*ns*)] (set! *print-length* nil))";

// TODO: implement the required code so that this hack is not required anymore.
/// Recognize the initial lein request when it connects itself, and returns the
/// expected value (ie: `nil`).
fn try_handle_initial_lein_request(req: &[u8]) -> Option<Value> {
if req == INITIAL_LEIN_REQUEST {
println!("Initial lein request fixed with `Nil`");
Some(Value::Nil)
} else {
None
}
}

/// Returns the address of the server.
fn get_address(port: u16) -> SocketAddrV4 {
let lh = Ipv4Addr::LOCALHOST;
Expand All @@ -34,9 +50,7 @@ fn nrepl_default_address() -> SocketAddrV4 {
// TODO: switch to something better, perhaps five usize or something else.
type SessionId = String;

pub struct Server {
sessions: HashMap<SessionId, Repl>,
}
pub struct Server(Repl);

impl Server {
/// Creates a new server and runs it.
Expand Down Expand Up @@ -68,66 +82,130 @@ impl Server {
let dec = Decoder::new(to_decode);
let req = decode_request(dec).expect("Request decoding failed");

let to_send = server
.run_request(req)
.expect("Request execution failed")
.to_bencode()
.expect("Response encoding failed");
let responses = server.run_request(req).expect("Request execution failed");

stream.write(to_send.as_slice())?;
responses.into_iter().try_for_each(|rep| {
let to_send = rep.to_bencode().expect("Response encoding failed");
stream.write(to_send.as_slice()).map(|_| ())
})?;
}

Ok(())
}

/// Creates a new server.
fn new() -> Server {
Server {
sessions: HashMap::new(),
}
Server(Repl::default())
}

/// Runs a request, updates the inner state, and returns the data that
/// should be returned to the client.
fn run_request(&mut self, r: Request) -> Result<Response, RequestError> {
fn run_request(&mut self, r: Request) -> Result<Vec<Response>, RequestError> {
// TODO: split this match arm
match r {
Request::Clone(id) => {
self.sessions.insert(id.clone(), Repl::default());
let session = random_uuid();
let new_session = random_uuid();
let status = "done";
Ok(Response::Cloned {
Ok(vec![Response::Cloned {
id,
new_session,
status,
})
session,
}])
}
Request::Eval { code, id, session } => {
let repl = &self.0;

let code_bytes = code.as_bytes();
let ns = String::from("user");

if let Some(hardcoded_response) = try_handle_initial_lein_request(code_bytes) {
let value = hardcoded_response;
return Ok(vec![
Response::Evaled1 {
id: id.clone(),
value,
ns,
session: session.clone(),
},
Response::Evaled2 { id, session },
]);
}

let mut input = BufReader::new(code_bytes);

let next = Repl::read(&mut input);
let evaled_next = repl.eval(&next);

let value = evaled_next;
Ok(vec![
Response::Evaled1 {
id: id.clone(),
value,
ns,
session: session.clone(),
},
Response::Evaled2 { id, session },
])
}
}
}
}

// TODO: instead of owning session and id, borrow it from client input
/// A response generated by the server.
enum Response {
/// Emitted when a `Clone` was requested
Cloned {
id: SessionId,
new_session: SessionId,
status: &'static str,
session: SessionId,
},
/// First response emitted when an `Eval` is requested.
Evaled1 {
id: SessionId,
session: SessionId,
value: Value,
ns: String,
},
/// Second response emitted when an `Eval` is requested.
Evaled2 { id: SessionId, session: SessionId },
}

impl ToBencode for Response {
const MAX_DEPTH: usize = 1;
const MAX_DEPTH: usize = 2;

fn encode(&self, enc: SingleItemEncoder) -> Result<(), EError> {
// TODO: split each match arm into smaller functions
// TODO: create a generic "emit_response" function, which will allow to
// declaratively specify what is emitted.
match self {
Response::Cloned {
id,
new_session,
status,
session,
} => enc.emit_dict(|ref mut dict_encoder| {
dict_encoder.emit_pair(b"id", id.as_str())?;
dict_encoder.emit_pair(b"new-session", new_session.as_str())?;
dict_encoder.emit_pair(b"status", status)
dict_encoder.emit_pair(b"session", session.as_str())?;
dict_encoder.emit_pair(b"status", &["done"] as &[_])
}),
Response::Evaled1 {
id,
value,
ns,
session,
} => enc.emit_dict(|ref mut dict_encoder| {
dict_encoder.emit_pair(b"id", id.as_str())?;
dict_encoder.emit_pair(b"ns", ns.as_str())?;
dict_encoder.emit_pair(b"session", session.as_str())?;
// TODO: use ToString instead of format
let value = format!("{}", value);
dict_encoder.emit_pair(b"value", value.as_str())
}),
Response::Evaled2 { id, session } => enc.emit_dict(|ref mut dict_encoder| {
dict_encoder.emit_pair(b"id", id.as_str())?;
dict_encoder.emit_pair(b"session", session.as_str())?;
dict_encoder.emit_pair(b"status", &["done"] as &[_])
}),
}
}
Expand All @@ -137,6 +215,12 @@ impl ToBencode for Response {
enum Request {
/// When the client wants to clone a session.
Clone(String),
/// When the user wants the server to evaluate something
Eval {
code: String,
session: SessionId,
id: SessionId,
},
}

/// An error raised by the server when it couldn't handle a request from a
Expand All @@ -157,6 +241,8 @@ enum RequestError {
UnknownOp,
/// Raised when a key should be present, but was not supplied by the client.
KeyNotFound(&'static str),
/// Raised when a client wants to do something with a non-declared session.
UnknownSession(String),
}

impl From<DError> for RequestError {
Expand All @@ -173,7 +259,7 @@ fn decode_request(mut input: Decoder) -> Result<Request, RequestError> {
_ => Err(RequestError::UnexpectedObject),
}?;

let mut op = request_dict
let op = request_dict
.get("op")
.map(String::as_str)
.ok_or(RequestError::Noop)?;
Expand All @@ -183,6 +269,24 @@ fn decode_request(mut input: Decoder) -> Result<Request, RequestError> {
.get("id")
.map(|i| Request::Clone(i.into()))
.ok_or(RequestError::KeyNotFound("id")),
"eval" => {
// TODO: instead of cloning each key, remove them from op, with
// HashMap::insert
let session = request_dict
.get("session")
.map(String::from)
.ok_or(RequestError::KeyNotFound("session"))?;
let code = request_dict
.get("code")
.map(String::from)
.ok_or(RequestError::KeyNotFound("code"))?;
let id = request_dict
.get("session")
.map(String::from)
.ok_or(RequestError::KeyNotFound("session"))?;

Ok(Request::Eval { session, code, id })
}
_ => {
dbg!(request_dict);
Err(RequestError::UnknownOp)
Expand Down Expand Up @@ -217,6 +321,7 @@ fn decode_dict(mut d: DictDecoder) -> Result<HashMap<String, String>, RequestErr
}

// Adapted from random-uuid from clojurescript
/// Returns a random uuid, as defined somewhere in the java documentation.
fn random_uuid() -> String {
let base = 16_usize;

Expand All @@ -230,7 +335,7 @@ fn random_uuid() -> String {
let part_3_to_add = 4 * base.pow(3);
let part_3 = part_3_to_add + hex_num(3);

let rhex = 8 | (3 & hex_num(1));
let rhex = (8 | (3 & hex_num(1))) * base.pow(3);
let part_4 = rhex + hex_num(3);

let part_5 = hex_num(12);
Expand All @@ -241,7 +346,7 @@ fn random_uuid() -> String {
// This function may be refactored in order to avoid its use, but it is
// not strictly necessary as it is not a hot path.
format!(
"{:x}-{:x}-{:x}-{:x}-{:x}",
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
part_1, part_2, part_3, part_4, part_5
)
}

0 comments on commit 9ffcc60

Please sign in to comment.