Simplify ruma-api-macros codegen
This commit is contained in:
parent
3696516679
commit
81207890b6
@ -108,7 +108,7 @@ impl ToTokens for Api {
|
|||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (set_request_path, parse_request_path) = if self.request.has_path_fields() {
|
let (url_set_path, parse_request_path) = if self.request.has_path_fields() {
|
||||||
let path_str = path.value();
|
let path_str = path.value();
|
||||||
|
|
||||||
assert!(path_str.starts_with('/'), "path needs to start with '/'");
|
assert!(path_str.starts_with('/'), "path needs to start with '/'");
|
||||||
@ -182,7 +182,7 @@ impl ToTokens for Api {
|
|||||||
(set_tokens, parse_tokens)
|
(set_tokens, parse_tokens)
|
||||||
};
|
};
|
||||||
|
|
||||||
let set_request_query = if let Some(field) = self.request.query_map_field() {
|
let url_set_querystring = if let Some(field) = self.request.query_map_field() {
|
||||||
let field_name = field.ident.as_ref().expect("expected field to have identifier");
|
let field_name = field.ident.as_ref().expect("expected field to have identifier");
|
||||||
let field_type = &field.ty;
|
let field_type = &field.ty;
|
||||||
|
|
||||||
@ -252,10 +252,8 @@ impl ToTokens for Api {
|
|||||||
quote! {
|
quote! {
|
||||||
#field_name: request_query
|
#field_name: request_query
|
||||||
}
|
}
|
||||||
} else if self.request.has_query_fields() {
|
|
||||||
self.request.request_init_query_fields()
|
|
||||||
} else {
|
} else {
|
||||||
TokenStream::new()
|
self.request.request_init_query_fields()
|
||||||
};
|
};
|
||||||
|
|
||||||
let add_headers_to_request = if self.request.has_header_fields() {
|
let add_headers_to_request = if self.request.has_header_fields() {
|
||||||
@ -282,76 +280,22 @@ impl ToTokens for Api {
|
|||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let create_http_request = if let Some(field) = self.request.newtype_body_field() {
|
let request_body_initializers = if let Some(field) = self.request.newtype_body_field() {
|
||||||
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
||||||
|
quote! { (request.#field_name) }
|
||||||
quote! {
|
|
||||||
let request_body = RequestBody(request.#field_name);
|
|
||||||
|
|
||||||
let mut http_request = ruma_api::exports::http::Request::new(
|
|
||||||
ruma_api::exports::serde_json::to_vec(&request_body)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if self.request.has_body_fields() {
|
|
||||||
let request_body_init_fields = self.request.request_body_init_fields();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let request_body = RequestBody {
|
|
||||||
#request_body_init_fields
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut http_request = ruma_api::exports::http::Request::new(
|
|
||||||
ruma_api::exports::serde_json::to_vec(&request_body)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
quote! {
|
let initializers = self.request.request_body_init_fields();
|
||||||
let mut http_request = ruma_api::exports::http::Request::new(Vec::new());
|
quote! { { #initializers } }
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let extract_request_body = if self.request.newtype_body_field().is_some() {
|
|
||||||
quote! {
|
|
||||||
let request_body =
|
|
||||||
ruma_api::exports::serde_json::from_slice(request.body().as_slice())?;
|
|
||||||
}
|
|
||||||
} else if self.request.has_body_fields() {
|
|
||||||
quote! {
|
|
||||||
let request_body: <RequestBody as ruma_api::Outgoing>::Incoming =
|
|
||||||
ruma_api::exports::serde_json::from_slice(request.body().as_slice())?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TokenStream::new()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let parse_request_body = if let Some(field) = self.request.newtype_body_field() {
|
let parse_request_body = if let Some(field) = self.request.newtype_body_field() {
|
||||||
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#field_name: request_body,
|
#field_name: request_body.0,
|
||||||
}
|
}
|
||||||
} else if self.request.has_body_fields() {
|
} else {
|
||||||
self.request.request_init_body_fields()
|
self.request.request_init_body_fields()
|
||||||
} else {
|
|
||||||
TokenStream::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_body_type_annotation = if self.response.has_body_fields() {
|
|
||||||
quote!(: <ResponseBody as ruma_api::Outgoing>::Incoming)
|
|
||||||
} else {
|
|
||||||
TokenStream::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let try_deserialize_response_body = if self.response.has_body() {
|
|
||||||
quote! {
|
|
||||||
ruma_api::exports::serde_json::from_slice(
|
|
||||||
http_response.into_body().as_slice(),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let extract_response_headers = if self.response.has_header_fields() {
|
let extract_response_headers = if self.response.has_header_fields() {
|
||||||
@ -362,24 +306,11 @@ impl ToTokens for Api {
|
|||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let response_init_fields = if self.response.has_fields() {
|
let response_init_fields = self.response.init_fields();
|
||||||
self.response.init_fields()
|
|
||||||
} else {
|
|
||||||
TokenStream::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialize_response_headers = self.response.apply_header_fields();
|
let serialize_response_headers = self.response.apply_header_fields();
|
||||||
|
|
||||||
let try_serialize_response_body = if self.response.has_body() {
|
let body = self.response.to_body();
|
||||||
let body = self.response.to_body();
|
|
||||||
quote! {
|
|
||||||
ruma_api::exports::serde_json::to_vec(&#body)?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {
|
|
||||||
"{}".as_bytes().to_vec()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_doc = format!(
|
let request_doc = format!(
|
||||||
"Data for a request to the `{}` API endpoint.\n\n{}",
|
"Data for a request to the `{}` API endpoint.\n\n{}",
|
||||||
@ -406,7 +337,9 @@ impl ToTokens for Api {
|
|||||||
#extract_request_path
|
#extract_request_path
|
||||||
#extract_request_query
|
#extract_request_query
|
||||||
#extract_request_headers
|
#extract_request_headers
|
||||||
#extract_request_body
|
|
||||||
|
let request_body: <RequestBody as ruma_api::Outgoing>::Incoming =
|
||||||
|
ruma_api::exports::serde_json::from_slice(request.body().as_slice())?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
#parse_request_path
|
#parse_request_path
|
||||||
@ -431,10 +364,14 @@ impl ToTokens for Api {
|
|||||||
ruma_api::exports::url::Url::parse("http://invalid-host-please-change/")
|
ruma_api::exports::url::Url::parse("http://invalid-host-please-change/")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
{ #set_request_path }
|
{ #url_set_path }
|
||||||
{ #set_request_query }
|
{ #url_set_querystring }
|
||||||
|
|
||||||
#create_http_request
|
let request_body = RequestBody #request_body_initializers;
|
||||||
|
|
||||||
|
let mut http_request = ruma_api::exports::http::Request::new(
|
||||||
|
ruma_api::exports::serde_json::to_vec(&request_body)?,
|
||||||
|
);
|
||||||
|
|
||||||
*http_request.method_mut() = ruma_api::exports::http::Method::#method;
|
*http_request.method_mut() = ruma_api::exports::http::Method::#method;
|
||||||
*http_request.uri_mut() = url.into_string().parse().unwrap();
|
*http_request.uri_mut() = url.into_string().parse().unwrap();
|
||||||
@ -456,7 +393,7 @@ impl ToTokens for Api {
|
|||||||
let response = ruma_api::exports::http::Response::builder()
|
let response = ruma_api::exports::http::Response::builder()
|
||||||
.header(ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
|
.header(ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
|
||||||
#serialize_response_headers
|
#serialize_response_headers
|
||||||
.body(#try_serialize_response_body)
|
.body(ruma_api::exports::serde_json::to_vec(&#body)?)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
@ -472,8 +409,10 @@ impl ToTokens for Api {
|
|||||||
if http_response.status().is_success() {
|
if http_response.status().is_success() {
|
||||||
#extract_response_headers
|
#extract_response_headers
|
||||||
|
|
||||||
let response_body #response_body_type_annotation =
|
let response_body: <ResponseBody as ruma_api::Outgoing>::Incoming =
|
||||||
#try_deserialize_response_body;
|
ruma_api::exports::serde_json::from_slice(
|
||||||
|
http_response.into_body().as_slice(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
#response_init_fields
|
#response_init_fields
|
||||||
|
@ -168,9 +168,7 @@ impl Request {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! { #(#fields,)* }
|
||||||
#(#fields,)*
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,46 +291,24 @@ impl TryFrom<RawRequest> for Request {
|
|||||||
|
|
||||||
impl ToTokens for Request {
|
impl ToTokens for Request {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let request_struct_header = quote! {
|
let request_def = if self.fields.is_empty() {
|
||||||
#[derive(Debug, Clone, ruma_api::Outgoing)]
|
|
||||||
#[incoming_no_deserialize]
|
|
||||||
pub struct Request
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_struct_body = if self.fields.is_empty() {
|
|
||||||
quote!(;)
|
quote!(;)
|
||||||
} else {
|
} else {
|
||||||
let fields =
|
let fields =
|
||||||
self.fields.iter().map(|request_field| strip_serde_attrs(request_field.field()));
|
self.fields.iter().map(|request_field| strip_serde_attrs(request_field.field()));
|
||||||
|
quote! { { #(#fields),* } }
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
#(#fields),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_body_struct =
|
let (derive_deserialize, request_body_def) =
|
||||||
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
|
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
|
||||||
let field = body_field.field();
|
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
|
||||||
let ty = &field.ty;
|
|
||||||
let span = field.span();
|
|
||||||
let derive_deserialize = if body_field.has_wrap_incoming_attr() {
|
let derive_deserialize = if body_field.has_wrap_incoming_attr() {
|
||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
} else {
|
} else {
|
||||||
quote!(ruma_api::exports::serde::Deserialize)
|
quote!(ruma_api::exports::serde::Deserialize)
|
||||||
};
|
};
|
||||||
|
|
||||||
quote_spanned! {span=>
|
(derive_deserialize, quote! { (#field); })
|
||||||
/// Data in the request body.
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
ruma_api::Outgoing,
|
|
||||||
ruma_api::exports::serde::Serialize,
|
|
||||||
#derive_deserialize
|
|
||||||
)]
|
|
||||||
struct RequestBody(#ty);
|
|
||||||
}
|
|
||||||
} else if self.has_body_fields() {
|
} else if self.has_body_fields() {
|
||||||
let fields = self.fields.iter().filter(|f| f.is_body());
|
let fields = self.fields.iter().filter(|f| f.is_body());
|
||||||
let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) {
|
let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) {
|
||||||
@ -342,20 +318,9 @@ impl ToTokens for Request {
|
|||||||
};
|
};
|
||||||
let fields = fields.map(RequestField::field);
|
let fields = fields.map(RequestField::field);
|
||||||
|
|
||||||
quote! {
|
(derive_deserialize, quote! { { #(#fields),* } })
|
||||||
/// Data in the request body.
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
ruma_api::Outgoing,
|
|
||||||
ruma_api::exports::serde::Serialize,
|
|
||||||
#derive_deserialize
|
|
||||||
)]
|
|
||||||
struct RequestBody {
|
|
||||||
#(#fields),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
TokenStream::new()
|
(quote!(ruma_api::exports::serde::Deserialize), quote!(;))
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_path_struct = if self.has_path_fields() {
|
let request_path_struct = if self.has_path_fields() {
|
||||||
@ -376,18 +341,17 @@ impl ToTokens for Request {
|
|||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_query_struct = if let Some(field) = self.query_map_field() {
|
let request_query_struct = if let Some(f) = self.query_map_field() {
|
||||||
let ty = &field.ty;
|
let field = Field { ident: None, colon_token: None, ..f.clone() };
|
||||||
let span = field.span();
|
|
||||||
|
|
||||||
quote_spanned! {span=>
|
quote! {
|
||||||
/// Data in the request's query string.
|
/// Data in the request's query string.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
ruma_api::exports::serde::Deserialize,
|
ruma_api::exports::serde::Deserialize,
|
||||||
ruma_api::exports::serde::Serialize,
|
ruma_api::exports::serde::Serialize,
|
||||||
)]
|
)]
|
||||||
struct RequestQuery(#ty);
|
struct RequestQuery(#field);
|
||||||
}
|
}
|
||||||
} else if self.has_query_fields() {
|
} else if self.has_query_fields() {
|
||||||
let fields = self.fields.iter().filter_map(RequestField::as_query_field);
|
let fields = self.fields.iter().filter_map(RequestField::as_query_field);
|
||||||
@ -408,9 +372,19 @@ impl ToTokens for Request {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let request = quote! {
|
let request = quote! {
|
||||||
#request_struct_header
|
#[derive(Debug, Clone, ruma_api::Outgoing)]
|
||||||
#request_struct_body
|
#[incoming_no_deserialize]
|
||||||
#request_body_struct
|
pub struct Request #request_def
|
||||||
|
|
||||||
|
/// Data in the request body.
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
ruma_api::Outgoing,
|
||||||
|
ruma_api::exports::serde::Serialize,
|
||||||
|
#derive_deserialize
|
||||||
|
)]
|
||||||
|
struct RequestBody #request_body_def
|
||||||
|
|
||||||
#request_path_struct
|
#request_path_struct
|
||||||
#request_query_struct
|
#request_query_struct
|
||||||
};
|
};
|
||||||
|
@ -23,21 +23,11 @@ impl Response {
|
|||||||
self.fields.iter().any(|field| field.is_body())
|
self.fields.iter().any(|field| field.is_body())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not this response has any fields.
|
|
||||||
pub fn has_fields(&self) -> bool {
|
|
||||||
!self.fields.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not this response has any data in HTTP headers.
|
/// Whether or not this response has any data in HTTP headers.
|
||||||
pub fn has_header_fields(&self) -> bool {
|
pub fn has_header_fields(&self) -> bool {
|
||||||
self.fields.iter().any(|field| field.is_header())
|
self.fields.iter().any(|field| field.is_header())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not this response has any data in the HTTP body.
|
|
||||||
pub fn has_body(&self) -> bool {
|
|
||||||
self.fields.iter().any(|field| !field.is_header())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether any field has a #[wrap_incoming] attribute.
|
/// Whether any field has a #[wrap_incoming] attribute.
|
||||||
pub fn uses_wrap_incoming(&self) -> bool {
|
pub fn uses_wrap_incoming(&self) -> bool {
|
||||||
self.fields.iter().any(|f| f.has_wrap_incoming_attr())
|
self.fields.iter().any(|f| f.has_wrap_incoming_attr())
|
||||||
@ -74,7 +64,7 @@ impl Response {
|
|||||||
let span = field.span();
|
let span = field.span();
|
||||||
|
|
||||||
quote_spanned! {span=>
|
quote_spanned! {span=>
|
||||||
#field_name: response_body
|
#field_name: response_body.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -100,9 +90,7 @@ impl Response {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! { #(#header_calls)* }
|
||||||
#(#header_calls)*
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces code to initialize the struct that will be used to create the response body.
|
/// Produces code to initialize the struct that will be used to create the response body.
|
||||||
@ -127,9 +115,7 @@ impl Response {
|
|||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
ResponseBody {
|
ResponseBody { #(#fields),* }
|
||||||
#(#fields),*
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,46 +217,25 @@ impl TryFrom<RawResponse> for Response {
|
|||||||
|
|
||||||
impl ToTokens for Response {
|
impl ToTokens for Response {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let response_struct_header = quote! {
|
let response_def = if self.fields.is_empty() {
|
||||||
#[derive(Debug, Clone, ruma_api::Outgoing)]
|
|
||||||
#[incoming_no_deserialize]
|
|
||||||
pub struct Response
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_struct_body = if self.fields.is_empty() {
|
|
||||||
quote!(;)
|
quote!(;)
|
||||||
} else {
|
} else {
|
||||||
let fields =
|
let fields =
|
||||||
self.fields.iter().map(|response_field| strip_serde_attrs(response_field.field()));
|
self.fields.iter().map(|response_field| strip_serde_attrs(response_field.field()));
|
||||||
|
|
||||||
quote! {
|
quote! { { #(#fields),* } }
|
||||||
{
|
|
||||||
#(#fields),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let response_body_struct =
|
let (derive_deserialize, response_body_def) =
|
||||||
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
|
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
|
||||||
let field = body_field.field();
|
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
|
||||||
let ty = &field.ty;
|
|
||||||
let span = field.span();
|
|
||||||
let derive_deserialize = if body_field.has_wrap_incoming_attr() {
|
let derive_deserialize = if body_field.has_wrap_incoming_attr() {
|
||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
} else {
|
} else {
|
||||||
quote!(ruma_api::exports::serde::Deserialize)
|
quote!(ruma_api::exports::serde::Deserialize)
|
||||||
};
|
};
|
||||||
|
|
||||||
quote_spanned! {span=>
|
(derive_deserialize, quote! { (#field); })
|
||||||
/// Data in the response body.
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
ruma_api::Outgoing,
|
|
||||||
ruma_api::exports::serde::Serialize,
|
|
||||||
#derive_deserialize
|
|
||||||
)]
|
|
||||||
struct ResponseBody(#ty);
|
|
||||||
}
|
|
||||||
} else if self.has_body_fields() {
|
} else if self.has_body_fields() {
|
||||||
let fields = self.fields.iter().filter(|f| f.is_body());
|
let fields = self.fields.iter().filter(|f| f.is_body());
|
||||||
let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) {
|
let derive_deserialize = if fields.clone().any(|f| f.has_wrap_incoming_attr()) {
|
||||||
@ -280,26 +245,24 @@ impl ToTokens for Response {
|
|||||||
};
|
};
|
||||||
let fields = fields.map(ResponseField::field);
|
let fields = fields.map(ResponseField::field);
|
||||||
|
|
||||||
quote! {
|
(derive_deserialize, quote!({ #(#fields),* }))
|
||||||
/// Data in the response body.
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
ruma_api::Outgoing,
|
|
||||||
ruma_api::exports::serde::Serialize,
|
|
||||||
#derive_deserialize
|
|
||||||
)]
|
|
||||||
struct ResponseBody {
|
|
||||||
#(#fields),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
TokenStream::new()
|
(quote!(ruma_api::exports::serde::Deserialize), quote!(;))
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = quote! {
|
let response = quote! {
|
||||||
#response_struct_header
|
#[derive(Debug, Clone, ruma_api::Outgoing)]
|
||||||
#response_struct_body
|
#[incoming_no_deserialize]
|
||||||
#response_body_struct
|
pub struct Response #response_def
|
||||||
|
|
||||||
|
/// Data in the response body.
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
ruma_api::Outgoing,
|
||||||
|
ruma_api::exports::serde::Serialize,
|
||||||
|
#derive_deserialize
|
||||||
|
)]
|
||||||
|
struct ResponseBody #response_body_def
|
||||||
};
|
};
|
||||||
|
|
||||||
response.to_tokens(tokens);
|
response.to_tokens(tokens);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user