Skip to content

Commit db7886d

Browse files
feat: Re-genericize tokens (#318)
* feat: Remake tokens generic re: #221 #203 Turns out you can actually get around candid's limitation on `IDLValue`. It just takes some careful rejiggering. First, use `Reserved` to match anything. This seems like a valid use of `Reserved`, and should basically always be supported. Second, use `deserialize_ignored_any` to actually get all the content with `IDLValueVisitor`. This is a little bit of a hack, but it works.
1 parent 784c460 commit db7886d

File tree

3 files changed

+200
-46
lines changed

3 files changed

+200
-46
lines changed

Cargo.lock

+6-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ic-utils/Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ic-utils"
3-
version = "0.12.1"
3+
version = "0.12.2"
44
authors = ["DFINITY Stiftung <[email protected]>"]
55
edition = "2018"
66
description = "Collection of utilities for Rust, on top of ic-agent, to communicate with the Internet Computer, following the Public Specification."
@@ -16,14 +16,17 @@ include = ["src", "Cargo.toml", "../LICENSE", "README.md"]
1616

1717
[dependencies]
1818
async-trait = "0.1.40"
19-
candid = "0.7.10"
19+
candid = "0.7.12"
2020
garcon = { version = "0.2", features = ["async"] }
2121
ic-agent = { path = "../ic-agent", version = "0.12" }
2222
serde = "1.0.115"
2323
serde_bytes = "0.11"
2424
strum = "0.23"
2525
strum_macros = "0.23"
2626
thiserror = "1.0.29"
27+
paste = "1"
28+
num-bigint = "0.4"
29+
leb128 = "0.2"
2730

2831
[dev-dependencies]
2932
ring = "0.16.11"

ic-utils/src/interfaces/http_request.rs

+189-41
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
//! The canister interface for canisters that implement HTTP requests.
22
3-
use crate::{call::AsyncCall, call::SyncCall, canister::CanisterBuilder, Canister};
4-
use candid::{CandidType, Deserialize, Func, Nat};
3+
use crate::{
4+
call::{AsyncCall, SyncCall},
5+
canister::CanisterBuilder,
6+
Canister,
7+
};
8+
use candid::{
9+
parser::value::{IDLValue, IDLValueVisitor},
10+
types::{Serializer, Type},
11+
CandidType, Deserialize, Func,
12+
};
513
use ic_agent::{export::Principal, Agent};
6-
use serde_bytes::ByteBuf;
714
use std::fmt::Debug;
815

916
/// A canister that can serve a HTTP request.
@@ -28,59 +35,72 @@ pub struct HttpRequest<'body> {
2835
pub body: &'body [u8],
2936
}
3037

31-
/// A token for continuing a callback streaming strategy.
32-
#[derive(Debug, Clone, CandidType, Deserialize)]
33-
pub struct Token {
34-
key: String,
35-
content_encoding: String,
36-
index: Nat,
37-
// The sha ensures that a client doesn't stream part of one version of an asset
38-
// followed by part of a different asset, even if not checking the certificate.
39-
sha256: Option<ByteBuf>,
40-
}
41-
42-
/// A callback-token pair for a callback streaming strategy.
43-
#[derive(Debug, Clone, CandidType, Deserialize)]
44-
pub struct CallbackStrategy {
45-
/// The callback function to be called to continue the stream.
46-
pub callback: Func,
47-
/// The token to pass to the function.
48-
pub token: Token,
49-
}
50-
51-
/// Possible strategies for a streaming response.
52-
#[derive(Debug, Clone, CandidType, Deserialize)]
53-
pub enum StreamingStrategy {
54-
/// A callback-based streaming strategy, where a callback function is provided for continuing the stream.
55-
Callback(CallbackStrategy),
56-
}
57-
5838
/// A HTTP response.
5939
#[derive(Debug, Clone, CandidType, Deserialize)]
60-
pub struct HttpResponse {
40+
pub struct HttpResponse<Token = self::Token> {
6141
/// The HTTP status code.
6242
pub status_code: u16,
6343
/// The response header map.
6444
pub headers: Vec<HeaderField>,
65-
#[serde(with = "serde_bytes")]
6645
/// The response body.
46+
#[serde(with = "serde_bytes")]
6747
pub body: Vec<u8>,
6848
/// The strategy for streaming the rest of the data, if the full response is to be streamed.
69-
pub streaming_strategy: Option<StreamingStrategy>,
49+
pub streaming_strategy: Option<StreamingStrategy<Token>>,
7050
/// Whether the query call should be upgraded to an update call.
7151
pub upgrade: Option<bool>,
7252
}
7353

54+
/// Possible strategies for a streaming response.
55+
#[derive(Debug, Clone, CandidType, Deserialize)]
56+
pub enum StreamingStrategy<Token = self::Token> {
57+
/// A callback-based streaming strategy, where a callback function is provided for continuing the stream.
58+
Callback(CallbackStrategy<Token>),
59+
}
60+
61+
/// A callback-token pair for a callback streaming strategy.
62+
#[derive(Debug, Clone, CandidType, Deserialize)]
63+
pub struct CallbackStrategy<Token = self::Token> {
64+
/// The callback function to be called to continue the stream.
65+
pub callback: Func,
66+
/// The token to pass to the function.
67+
pub token: Token,
68+
}
69+
7470
/// The next chunk of a streaming HTTP response.
7571
#[derive(Debug, Clone, CandidType, Deserialize)]
76-
pub struct StreamingCallbackHttpResponse {
72+
pub struct StreamingCallbackHttpResponse<Token = self::Token> {
7773
/// The body of the stream chunk.
7874
#[serde(with = "serde_bytes")]
7975
pub body: Vec<u8>,
8076
/// The new stream continuation token.
8177
pub token: Option<Token>,
8278
}
8379

80+
/// A token for continuing a callback streaming strategy.
81+
#[derive(Debug, Clone, PartialEq)]
82+
pub struct Token(pub IDLValue);
83+
84+
impl CandidType for Token {
85+
fn _ty() -> Type {
86+
Type::Reserved
87+
}
88+
fn idl_serialize<S: Serializer>(&self, _serializer: S) -> Result<(), S::Error> {
89+
// We cannot implement serialize, since our type must be `Reserved` in order to accept anything.
90+
// Attempting to serialize this type is always an error and should be regarded as a compile time error.
91+
unimplemented!("Token is not serializable")
92+
}
93+
}
94+
95+
impl<'de> Deserialize<'de> for Token {
96+
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
97+
// Ya know it says `ignored`, but what if we just didn't ignore it.
98+
deserializer
99+
.deserialize_ignored_any(IDLValueVisitor)
100+
.map(Token)
101+
}
102+
}
103+
84104
impl HttpRequestCanister {
85105
/// Create an instance of a [Canister] implementing the [HttpRequestCanister] interface
86106
/// and pointing to the right Canister ID.
@@ -151,25 +171,30 @@ impl<'agent> Canister<'agent, HttpRequestCanister> {
151171
method: M,
152172
token: Token,
153173
) -> impl 'agent + SyncCall<(StreamingCallbackHttpResponse,)> {
154-
self.query_(&method.into()).with_arg(token).build()
174+
self.query_(&method.into()).with_value_arg(token.0).build()
155175
}
156176
}
157177

158178
#[cfg(test)]
159179
mod test {
160-
use super::HttpResponse;
161-
use candid::{Decode, Encode};
180+
use super::{
181+
CallbackStrategy, HttpResponse, StreamingCallbackHttpResponse, StreamingStrategy, Token,
182+
};
183+
use candid::{
184+
parser::value::{IDLField, IDLValue},
185+
Decode, Encode,
186+
};
162187

163188
mod pre_update_legacy {
164189
use candid::{CandidType, Deserialize, Func, Nat};
165190
use serde_bytes::ByteBuf;
166191

167192
#[derive(CandidType, Deserialize)]
168193
pub struct Token {
169-
key: String,
170-
content_encoding: String,
171-
index: Nat,
172-
sha256: Option<ByteBuf>,
194+
pub key: String,
195+
pub content_encoding: String,
196+
pub index: Nat,
197+
pub sha256: Option<ByteBuf>,
173198
}
174199

175200
#[derive(CandidType, Deserialize)]
@@ -208,4 +233,127 @@ mod test {
208233

209234
let _response = Decode!(&bytes, HttpResponse).unwrap();
210235
}
236+
237+
#[test]
238+
fn deserialize_response_with_token() {
239+
use candid::{types::Label, Func, Principal};
240+
241+
let bytes: Vec<u8> = Encode!(&HttpResponse {
242+
status_code: 100,
243+
headers: Vec::new(),
244+
body: Vec::new(),
245+
streaming_strategy: Some(StreamingStrategy::Callback(CallbackStrategy {
246+
callback: Func {
247+
principal: Principal::from_text("2chl6-4hpzw-vqaaa-aaaaa-c").unwrap(),
248+
method: "callback".into()
249+
},
250+
token: pre_update_legacy::Token {
251+
key: "foo".into(),
252+
content_encoding: "bar".into(),
253+
index: 42.into(),
254+
sha256: None,
255+
},
256+
})),
257+
upgrade: None,
258+
})
259+
.unwrap();
260+
261+
let response = Decode!(&bytes, HttpResponse).unwrap();
262+
assert_eq!(response.status_code, 100);
263+
let token = match response.streaming_strategy {
264+
Some(StreamingStrategy::Callback(CallbackStrategy { token, .. })) => token,
265+
_ => panic!("streaming_strategy was missing"),
266+
};
267+
let fields = match token {
268+
Token(IDLValue::Record(fields)) => fields,
269+
_ => panic!("token type mismatched {:?}", token),
270+
};
271+
assert!(fields.contains(&IDLField {
272+
id: Label::Named("key".into()),
273+
val: IDLValue::Text("foo".into())
274+
}));
275+
assert!(fields.contains(&IDLField {
276+
id: Label::Named("content_encoding".into()),
277+
val: IDLValue::Text("bar".into())
278+
}));
279+
assert!(fields.contains(&IDLField {
280+
id: Label::Named("index".into()),
281+
val: IDLValue::Nat(42.into())
282+
}));
283+
assert!(fields.contains(&IDLField {
284+
id: Label::Named("sha256".into()),
285+
val: IDLValue::None
286+
}));
287+
}
288+
289+
#[test]
290+
fn deserialize_streaming_response_with_token() {
291+
use candid::types::Label;
292+
293+
let bytes: Vec<u8> = Encode!(&StreamingCallbackHttpResponse {
294+
body: b"this is a body".as_ref().into(),
295+
token: Some(pre_update_legacy::Token {
296+
key: "foo".into(),
297+
content_encoding: "bar".into(),
298+
index: 42.into(),
299+
sha256: None,
300+
}),
301+
})
302+
.unwrap();
303+
304+
let response = Decode!(&bytes, StreamingCallbackHttpResponse).unwrap();
305+
assert_eq!(response.body, b"this is a body");
306+
let fields = match response.token {
307+
Some(Token(IDLValue::Record(fields))) => fields,
308+
_ => panic!("token type mismatched {:?}", response.token),
309+
};
310+
assert!(fields.contains(&IDLField {
311+
id: Label::Named("key".into()),
312+
val: IDLValue::Text("foo".into())
313+
}));
314+
assert!(fields.contains(&IDLField {
315+
id: Label::Named("content_encoding".into()),
316+
val: IDLValue::Text("bar".into())
317+
}));
318+
assert!(fields.contains(&IDLField {
319+
id: Label::Named("index".into()),
320+
val: IDLValue::Nat(42.into())
321+
}));
322+
assert!(fields.contains(&IDLField {
323+
id: Label::Named("sha256".into()),
324+
val: IDLValue::None
325+
}));
326+
}
327+
328+
#[test]
329+
fn deserialize_streaming_response_without_token() {
330+
mod missing_token {
331+
use candid::{CandidType, Deserialize};
332+
/// The next chunk of a streaming HTTP response.
333+
#[derive(Debug, Clone, CandidType, Deserialize)]
334+
pub struct StreamingCallbackHttpResponse {
335+
/// The body of the stream chunk.
336+
#[serde(with = "serde_bytes")]
337+
pub body: Vec<u8>,
338+
}
339+
}
340+
let bytes: Vec<u8> = Encode!(&missing_token::StreamingCallbackHttpResponse {
341+
body: b"this is a body".as_ref().into(),
342+
})
343+
.unwrap();
344+
345+
let response = Decode!(&bytes, StreamingCallbackHttpResponse).unwrap();
346+
assert_eq!(response.body, b"this is a body");
347+
assert_eq!(response.token, None);
348+
349+
let bytes: Vec<u8> = Encode!(&StreamingCallbackHttpResponse {
350+
body: b"this is a body".as_ref().into(),
351+
token: Option::<pre_update_legacy::Token>::None,
352+
})
353+
.unwrap();
354+
355+
let response = Decode!(&bytes, StreamingCallbackHttpResponse).unwrap();
356+
assert_eq!(response.body, b"this is a body");
357+
assert_eq!(response.token, None);
358+
}
211359
}

0 commit comments

Comments
 (0)