-
Notifications
You must be signed in to change notification settings - Fork 4
WIP: feat: constraint params to valid JSONRPC params #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
@vorner I wanted to show you this before I finished it, there are still a few You will also see a comment on the |
I know not everything is valid there. I'm not sure however if enforcing the restriction is worth the added code and complexity. Yes, it prevents sending or receiving some invalid RPCs, but then, if someone sends them by a mistake, they get an error anyway (this is especially true on the receiver side). Also, what error do you generate if the received message has the wrong type of parameters? My guess (without any attempt to verify it) is that it would be syntax error. Furthermore, I don't like the panicking at runtime if the wrong type of parameters is passed. I think it could be enforced by the macro itself ( That being said, I'm not completely against the idea, I'm just not convinced. As for the id, I'm reluctant to enforce it on the receiver side and the library never generates anything but strings (I'd have to think of a way to create a message manually with something else than the string, it might be possible, but I don't think it is easy). So the benefit is probably even lower. Also, please run the code through rustfmt (or look at the travis output). Currently, the used version of rustfmt is 0.8.0 ‒ I'll probably migrate to the newest one soon, though. I know rustfmt sometimes has suboptimal ideas about what is the best formatting, but it at least enforces some consistency through the code. |
src/message.rs
Outdated
/// Wether or not there are any params | ||
pub fn is_empty(&self) -> bool { | ||
match *self { | ||
Params::Positional(ref params) => params.len() == 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't vec
have is_empty
as well? Also, what about an empty map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pilot error, fixed
src/message.rs
Outdated
} | ||
|
||
// Helper fuction for skipping the params field serialization when there are no params | ||
fn params_empty(x: &Option<Params>) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This'll need to be public if used from outside in the macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
I tend the prefer preventing bad things in the earliest stage possible. If we could prevent the sender from creating malformed messages, then sending something we could have known was wrong, have the server try to process it, fail and send an error back, seems unnecessary runtime work. It's a compile error vs a runtime error.
We chose to deny it and our current non-tokio jsonrpc returns:
You can choose to warn or allow in the server side and interpret
We are in complete agreement, I was looking on how to get something equivalent to Let me know if you have changed your mind about this or you think its best to scratch it. |
I put a lot of effort into the parsing mostly for comfort of use and because I need to do some validation no matter what the client side of my library does. The server-side validation is mandatory (security and sanity wise), the client side is nice, but kind of optional. Anyway, about changing mind and such. Let me put it differently. I was a bit worried if this was because you wanted to have some code to hack on, or if you have a specific need that this solves. If there's a need and it is worth your time to write it, I'm not really against that. I'll leave the decision up to you, I just wanted to make sure you thought about the other side (the cost). Especially because I think this might be a bigger task than it looks. For example, it is possible to receive the whole json object as one structure if the structure implements So, if you want to spend the time on this feature, I won't stop you and I'm OK with including the changes, but there might be some (even yet unknown and unexpected) problems like the above. |
It's not a personal hack, it is for production use.
I wasn't sure how you were consuming the API, if through raw json or with structs through Take a struct with two fields,
You can't convert a The boilerplate can be taken care of:
In our case, we didn't have macros 1.1 and we originally had to build this on top of command!{
name = "test";
response = ();
#[derive(Debug, PartialEq)]
struct TestCommand {
pub field1: u8,
pub field2: Option<String>,
}
} The macro implements the Wether the macro deserializes from I'm not attached to the code we have or the way we did things. jsonrpc-client -s "localhost:1234" test 8 "Hello world"
jsonrpc-client -s "localhost:1234" test 8
jsonrpc-client -s "localhost:1234" test { "field1": 8 }
jsonrpc-client -s "localhost:1234" test { "field1": 8, "field2": null }
jsonrpc-client -s "localhost:1234" test { "field1": 8, "field2": "foo" } |
@vorner I rebased and cleaned up the params!([1,2,3]);
params!({"x": 7, "y": 8}); I also added support for top level literals: params!(true); // this will create `"params": [true]`
params!(8); // this will create `"params": [8]` Additionally you can use #[derive(Serialize)]
struct TestParam {
x: usize,
y: usize,
}
// by default they get serialized by name
params!(TestParam {x: 1, y:2}); // this will create `"params": {"x": 1, "y": 2}`
// we can serialize structs by position too
params!(pos TestParam {x: 1, y:2}); // this will create `"params": [1, 2]` You will still find an If you want to get rid of it, we can write a
I still need to fix |
Just to let you know, I don't know when I'll have the time to look through this. I'm not ignoring it, it's in the TODO list, but it may take few days before I get to it. |
Yea, that's totally understandable. |
- new `parse_params!` macro - new `try_parse_params!` macro
a97dc7b
to
6b815a8
Compare
pub fn into_positional(self) -> Self { | ||
match self { | ||
Params::Named(x) => { | ||
let values = x.into_iter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think serde_json::Map
guarantees preserving the order. Therefore, turning the map into array may result in arbitrary order. What is the use case for this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn... I figured thats why they had a Map instead of using HashMap. I'll have to find another solution for it.
The use is for sending the rpc params in positional form instead of named.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a feature that forces the Map
to preserve order (I don't remember the name), but enabling it would enable it for whatever other use the program might have for the serde_json
crate. Otherwise, Map
sorts them (that is why your tests with {x: …, y: …}
didn't catch the problem, if you reverse them to {y: …, x: …}
, then you might).
However, serde itself has two ways to decode structs, one is by a name and another is by the order, so it may be possible to place some kind of intermediate layer that converts it somehow. I don't know the details, only that some formats serde can decode/encode don't provide names, only the order (I think CBOR is one such format).
|
||
([ $($tt:tt)+ ]) => { | ||
{ | ||
match $crate::macro_exports::Value::Array(json_internal!(@array [] $($tt)+)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't match json!([ $($tt)+ ] {
be easier? Or just directly $crate::Params::Positional(json_internal!(...))
? This construct array and then deconstruct it right away seems a bit strange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, fixed
let mut xs = Vec::with_capacity(x.len()); | ||
for (_,v) in x { | ||
xs.push(v); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I prefer something like x.map(|(_, v)| v).collect()
. I even believe this might be more idiomatic. But maybe that's just a personal preference, so just pointing out the possibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, x.into_iter().map(|(_, v)| v).collect())
// if the object serializes to `Null` map it to `None` | ||
$crate::macro_exports::Value::Null => None, | ||
// encode any other value as an array of 1 item | ||
value => Some($crate::Params::Positional(vec![value])), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about the semantics here, how this will work out. What if I want to send single positional vec? So, the result should be [[42]]
.
Maybe if I see some examples, it'll be clearer, but currently I'm afraid about this heuristic being too surprising in some cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You use params!([[7]])
, I added it to the test cases.
$crate::message::RpcError::parse_error(format!("{}", err)))) | ||
} | ||
None => { | ||
// Review: should we try to parse an object where all fields are missing? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? Being able to parse unit structs, for example, from empty objects? I think that is legal. But parsing it from no params is probably not...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That comment is from before the derive. It's referring to this code:
// Try to parse it as if we received no arguments, if we fail DO NOT error
let value = ::tokio_jsonrpc::macro_exports::Value::Null;
match ::tokio_jsonrpc::macro_exports::from_value(value) {
Ok(x) => Some(Ok(x)),
Err(_) => None,
}
That code will work on the macro too for unit structs but unfortunatly we can't ask if T: Default
on the macro to pull exactly same trick I have on the derive.
We could add an option for it, or just tell people to define the struct and use #[derive(Params)
for complex use cases.
//! | ||
//! A server listening on localhost:2345. It reponds to the „now“ method, returning the current | ||
//! unix timestamp (number of seconds since 1.1. 1970). You can also subscribe to periodic time | ||
//! updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy-paste? O:-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm.... xD removed
I know this is still work in progress (so I just skipped commenting on some parts that are yet obviously in flux), but I wanted to give it a quick read rather sooner than later. Generally, I like your interface more than that of the monstrous
I know this is a long comment, but there was a lot of code to comment on. As I said, I like the direction it is taking and I hope this'll be just somewhere on the level of fine-tuning it and synchronizing different views of the ideas. |
I didn't want to be assuming and replace your
I have a branch waiting on this PR that gets rid of that snaffu. fn rpc(&self, _ctl: &ServerCtl, _method: &str, _params: Option<Params>) That way we avoid unnecessary cloning.
I will look into it
Go ahead
Will do
Good catch derive magic
I am using serde for the heavy lifting.
Marking an object as Params gives you the ability to deserialize structs and enums from json objects or arrays. Serde will only let you do one of the two depending on what it is you are deserializing. All the derive magic is doing is to map whatever json we have into something serde will deserialize correctly.
We peek at two places for two very different reasons:
I understand your concern, but there is no need for it. We are not adding/removing any attribute or messing with them in any way. Whatever attributes were used by the user will remain there and serde itself is the one processing them not us. Once I code support for tagged enums, our only dependency on the attributes will be default which is unlikely to get removed from serde. We can always code our own |
I was thinking about that. But this is a bit in a conflict with another feature, composability of the server implementations. If one returns But that could probably be handled in some way. Like refusing to handle a method not by |
This went somehow silent... do you expect to continue work on this, or did you postpone it? Or, do you wait for an answer about something from me? (if so, I haven't noticed the question) Thanks |
@vorner yea, I expect to finish it. A few higher priority things showed up. I'm about a week or so away from being able to focus again on the transition to tokio. |
This is a work in progress
Motivation
Not any json is valid in the params field.
source: jsonrpc2.0 specification
The parsing of
"params": null
seems to be left to the readers discreation, given that not passing the field is valid, the PR assumes params being present butnull
is the the same as not being present and will be deserialized asNone
.Pending:
fix jsonrpc_params~>parse_params!
foobar_anonymous_structs
into proper testsparse_params!
'ish on existing structs#[derive(Params)]
support for tagged enums