use core::str;
use std::{ops::Deref, sync::Arc};
#[cfg(feature = "http")]
use web_time::Duration;
#[cfg(feature = "http")]
use std::str::FromStr;
#[cfg(feature = "http")]
use std::ops::DerefMut;
#[cfg(feature = "http")]
use bytes::Bytes;
#[cfg(feature = "http")]
use imbl::OrdMap;
#[cfg(feature = "http")]
use lazy_static::lazy_static;
#[cfg(feature = "http")]
use reqwest::{header::{HeaderMap, HeaderName, CONTENT_TYPE}, Method};
#[cfg(feature = "http")]
use rustc_hash::FxHashMap;
#[cfg(feature = "http")]
use crate::runtime::{wake, WakeRef, NumT};
use arcstr::{literal, ArcStr};
use imbl::vector;
use serde::{Deserialize, Serialize};
use crate::{model::{Graph, LibFunc, Param, Profile}, runtime::{Error, Num, Type, Units, Val, ValRef, Variable, instruction::{Instruction, Instructions}, instructions::Base, proc::ProcEnv}};
#[cfg(all(feature = "http", feature = "tokio"))]
use reqwest::Client;
#[cfg(all(feature = "http", not(feature = "tokio")))]
use reqwest::blocking::Client;
#[cfg(feature = "tokio")]
use tokio::sync::Semaphore;
#[cfg(feature = "http")]
lazy_static! {
static ref HTTP_CLIENT: Arc<Client> = Arc::new(Client::new());
}
#[cfg(feature = "tokio")]
lazy_static! {
static ref HTTP_BACKPRESSURE_SEMAPHORE: Arc<Semaphore> = Arc::new(Semaphore::new(100));
}
pub(self) const HTTP_LIB: ArcStr = literal!("Http");
pub fn insert_http_lib(graph: &mut Graph) {
#[cfg(feature = "http")]
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "fetch".into(),
is_async: true,
docs: r#"# async Http.fetch(url: str, method: str = "get", body: str | blob = null, headers: map = null, timeout: seconds = null, query: map = null, bearer: str = null) -> Promise<map>
Make an HTTP request, using the thread pool in the background so that other Stof processes can continue running.
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
assert(resp.get('text').len() > 100);
```"#.into(),
params: vector![
Param { name: "url".into(), param_type: Type::Str, default: None },
Param { name: "method".into(), param_type: Type::Str, default: Some(Arc::new(Base::Literal(Val::Str("get".into())))) },
Param { name: "body".into(), param_type: Type::Union(vector![Type::Blob, Type::Str]), default: Some(Arc::new(Base::Literal(Val::Null))) },
Param { name: "headers".into(), param_type: Type::Map, default: Some(Arc::new(Base::Literal(Val::Null))) },
Param { name: "timeout".into(), param_type: Type::Num(NumT::Float), default: Some(Arc::new(Base::Literal(Val::Null))) },
Param { name: "query".into(), param_type: Type::Map, default: Some(Arc::new(Base::Literal(Val::Null))) },
Param { name: "bearer".into(), param_type: Type::Str, default: Some(Arc::new(Base::Literal(Val::Null))) },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let wake_ref = WakeRef::default();
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::SendRequest(wake_ref.clone())));
instructions.push(Arc::new(Base::CtrlSleepRef(wake_ref)));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "parse".into(),
is_async: false,
docs: r#"# Http.parse(response: map, context: obj = self) -> obj
Parse an HTTP response into the context object (also the return value), using the response "Content-Type" header as a Stof format (binary import). Default content type if not found in response headers is "stof". Will throw an error if the format isn't accepted by this graph, or if the body doesn't exist.
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
const body = new {};
try Http.parse(resp, body);
catch { /* didn't work out.. */ }
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
Param { name: "context".into(), param_type: Type::Void, default: Some(Arc::new(Base::Literal(Val::Null))) },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::ParseResponse));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "text".into(),
is_async: false,
docs: r#"# Http.text(response: map) -> str
Extract a UTF-8 text body from this response map (Equivalent to Http.blob(response) as str).
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
const body = Http.text(resp);
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::TextBody));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "size".into(),
is_async: false,
docs: r#"# Http.size(response: map) -> bytes
Extract the response body size in bytes.
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
const mib_body_size = Http.size(resp) as MiB;
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::BodySize));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "blob".into(),
is_async: false,
docs: r#"# Http.blob(response: map) -> blob
Extract the body of this response as bytes.
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
const body = Http.blob(resp);
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::BlobBody));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "success".into(),
is_async: false,
docs: r#"# Http.success(response: map) -> bool
Was the request successful? Meaning, is the response 'status' between [200, 299]?
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
assert(Http.success(resp));
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::ResponseSuccess));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "client_error".into(),
is_async: false,
docs: r#"# Http.client_error(response: map) -> bool
Was the request a client error? Meaning, is the response 'status' between [400, 499]?
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
assert_not(Http.client_error(resp));
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::ClientError));
Ok(instructions)
})
});
graph.insert_libfunc(LibFunc {
library: HTTP_LIB.clone(),
name: "server_error".into(),
is_async: false,
docs: r#"# Http.server_error(response: map) -> bool
Was the request a server error? Meaning, is the response 'status' between [500, 599]?
```rust
const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe");
assert_not(Http.server_error(resp));
```"#.into(),
params: vector![
Param { name: "response".into(), param_type: Type::Map, default: None },
],
return_type: None,
unbounded_args: false,
args_to_symbol_table: false,
func: Arc::new(move |_as_ref, _arg_count, _env, _graph| {
let mut instructions = Instructions::default();
instructions.push(Arc::new(HttpIns::ServerError));
Ok(instructions)
})
});
}
#[cfg(feature = "http")]
pub(self) struct HTTPRequest {
pub url: String,
pub method: Method,
pub headers: HeaderMap,
pub bearer: Option<String>,
pub body: Option<Bytes>,
pub timeout: Option<Duration>,
pub query: Option<FxHashMap<String, String>>,
pub results: ValRef<Val>,
pub waker: WakeRef,
}
#[cfg(feature = "http")]
impl HTTPRequest {
#[allow(unused)]
pub fn send(self, env: &mut ProcEnv) {
#[cfg(feature = "tokio")]
{
if let Some(handle) = &env.tokio_runtime {
handle.spawn(async move {
let _permit = HTTP_BACKPRESSURE_SEMAPHORE.acquire().await;
let client = &HTTP_CLIENT;
let mut builder = client.request(self.method, self.url);
builder = builder.headers(self.headers);
if let Some(bearer) = self.bearer {
builder = builder.bearer_auth(bearer);
}
if let Some(body) = self.body {
builder = builder.body(body);
}
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
if let Some(query) = self.query {
builder = builder.query(&query);
}
if let Ok(request) = builder.build() {
match client.execute(request).await {
Ok(response) => {
let status = response.status();
let headers = response.headers().clone();
let response_body = response.bytes().await;
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("status".into())), ValRef::new(Val::Num(Num::Int(status.as_u16() as i64))));
results.insert(ValRef::new(Val::Str("ok".into())), ValRef::new(Val::Bool(status.is_success())));
let mut result_headers = OrdMap::default();
for (k, v) in &headers {
if let Ok(val) = v.to_str() {
result_headers.insert(ValRef::new(Val::Str(k.as_str().into())), ValRef::new(Val::Str(val.into())));
}
}
results.insert(ValRef::new(Val::Str("headers".into())), ValRef::new(Val::Map(result_headers)));
if let Some(ctype) = headers.get(CONTENT_TYPE) {
if let Ok(val) = ctype.to_str() {
results.insert(ValRef::new(Val::Str("content_type".into())), ValRef::new(Val::Str(val.into())));
}
}
if let Ok(bytes) = response_body {
results.insert(ValRef::new(Val::Str("bytes".into())), ValRef::new(Val::Blob(bytes)));
}
},
_ => {},
}
},
Err(error) => {
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("error".into())), ValRef::new(Val::Str(error.to_string().into())));
},
_ => {},
}
}
}
}
wake(&self.waker); });
} else {
let client = reqwest::blocking::Client::new();
let mut builder = client.request(self.method, self.url);
builder = builder.headers(self.headers);
if let Some(bearer) = self.bearer {
builder = builder.bearer_auth(bearer);
}
if let Some(body) = self.body {
builder = builder.body(body);
}
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
if let Some(query) = self.query {
builder = builder.query(&query);
}
if let Ok(request) = builder.build() {
match client.execute(request) {
Ok(response) => {
let status = response.status();
let headers = response.headers().clone();
let response_body = response.bytes();
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("status".into())), ValRef::new(Val::Num(Num::Int(status.as_u16() as i64))));
results.insert(ValRef::new(Val::Str("ok".into())), ValRef::new(Val::Bool(status.is_success())));
let mut result_headers = OrdMap::default();
for (k, v) in &headers {
if let Ok(val) = v.to_str() {
result_headers.insert(ValRef::new(Val::Str(k.as_str().into())), ValRef::new(Val::Str(val.into())));
}
}
results.insert(ValRef::new(Val::Str("headers".into())), ValRef::new(Val::Map(result_headers)));
if let Some(ctype) = headers.get(CONTENT_TYPE) {
if let Ok(val) = ctype.to_str() {
results.insert(ValRef::new(Val::Str("content_type".into())), ValRef::new(Val::Str(val.into())));
}
}
if let Ok(bytes) = response_body {
results.insert(ValRef::new(Val::Str("bytes".into())), ValRef::new(Val::Blob(bytes)));
}
},
_ => {},
}
},
Err(error) => {
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("error".into())), ValRef::new(Val::Str(error.to_string().into())));
},
_ => {},
}
}
}
}
wake(&self.waker); }
}
#[cfg(not(feature = "tokio"))]
{
let client = &HTTP_CLIENT;
let mut builder = client.request(self.method, self.url);
builder = builder.headers(self.headers);
if let Some(bearer) = self.bearer {
builder = builder.bearer_auth(bearer);
}
if let Some(body) = self.body {
builder = builder.body(body);
}
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
if let Some(query) = self.query {
builder = builder.query(&query);
}
if let Ok(request) = builder.build() {
match client.execute(request) {
Ok(response) => {
let status = response.status();
let headers = response.headers().clone();
let response_body = response.bytes();
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("status".into())), ValRef::new(Val::Num(Num::Int(status.as_u16() as i64))));
results.insert(ValRef::new(Val::Str("ok".into())), ValRef::new(Val::Bool(status.is_success())));
let mut result_headers = OrdMap::default();
for (k, v) in &headers {
if let Ok(val) = v.to_str() {
result_headers.insert(ValRef::new(Val::Str(k.as_str().into())), ValRef::new(Val::Str(val.into())));
}
}
results.insert(ValRef::new(Val::Str("headers".into())), ValRef::new(Val::Map(result_headers)));
if let Some(ctype) = headers.get(CONTENT_TYPE) {
if let Ok(val) = ctype.to_str() {
results.insert(ValRef::new(Val::Str("content_type".into())), ValRef::new(Val::Str(val.into())));
}
}
if let Ok(bytes) = response_body {
results.insert(ValRef::new(Val::Str("bytes".into())), ValRef::new(Val::Blob(bytes)));
}
},
_ => {},
}
},
Err(error) => {
let mut results = self.results.write();
match results.deref_mut() {
Val::Map(results) => {
results.insert(ValRef::new(Val::Str("error".into())), ValRef::new(Val::Str(error.to_string().into())));
},
_ => {},
}
}
}
}
wake(&self.waker); }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(self) enum HttpIns {
#[cfg(feature = "http")]
SendRequest(WakeRef),
ParseResponse,
TextBody,
BodySize,
BlobBody,
ResponseSuccess,
ClientError,
ServerError,
}
#[typetag::serde(name = "HttpIns")]
impl Instruction for HttpIns {
fn exec(&self, env: &mut ProcEnv, graph: &mut Graph) -> Result<Option<Instructions>, Error> {
match self {
#[cfg(feature = "http")]
Self::SendRequest(waker) => {
let mut url = String::default();
let mut method = Method::GET;
let mut headers = HeaderMap::default();
let mut bearer = None;
let mut body = None;
let mut timeout = None;
let mut query = None;
if let Some(bearer_var) = env.stack.pop() {
match bearer_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Str(token) => {
bearer = Some(token.to_string());
},
_ => {
return Err(Error::HttpArgs(format!("bearer argument must be a string")));
}
}
}
if let Some(query_var) = env.stack.pop() {
match query_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Map(query_map) => {
let mut qu = FxHashMap::default();
for (k, v) in query_map {
let key = k.read().print(&graph);
let value = v.read().print(&graph);
qu.insert(key, value);
}
query = Some(qu);
},
_ => {
return Err(Error::HttpArgs(format!("query argument must be a map")));
}
}
}
if let Some(timeout_var) = env.stack.pop() {
match timeout_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Num(num) => {
let seconds = num.float(Some(Units::Seconds));
timeout = Some(Duration::from_secs(seconds as u64));
},
_ => {
return Err(Error::HttpArgs(format!("timeout must be a number")));
}
}
}
if let Some(headers_var) = env.stack.pop() {
match headers_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Map(header_map) => {
for (k, v) in header_map {
let key = k.read().print(&graph);
if let Ok(name) = HeaderName::from_str(&key) {
headers.insert(name, v.read().print(&graph).parse().unwrap());
}
}
},
_ => {
return Err(Error::HttpArgs(format!("headers must be a map")));
}
}
}
if let Some(body_var) = env.stack.pop() {
match body_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Str(str) => {
body = Some(Bytes::from(str.to_string()));
},
Val::Blob(blob) => {
body = Some(Bytes::from(blob.clone()));
},
_ => {
return Err(Error::HttpArgs(format!("body must be either a string or a blob")));
}
}
}
if let Some(method_var) = env.stack.pop() {
match method_var.val.read().deref() {
Val::Void |
Val::Null => {
},
Val::Str(method_val) => {
match method_val.to_lowercase().as_str() {
"get" => method = Method::GET,
"post" => method = Method::POST,
"put" => method = Method::PUT,
"patch" => method = Method::PATCH,
"delete" => method = Method::DELETE,
"head" => method = Method::HEAD,
"options" => method = Method::OPTIONS,
"trace" => method = Method::TRACE,
"connect" => method = Method::CONNECT,
_ => {
return Err(Error::HttpArgs(format!("method must be one of 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace', or 'connect'")));
}
}
},
_ => {
return Err(Error::HttpArgs(format!("method must be one of 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace', or 'connect'")));
}
}
}
if let Some(url_var) = env.stack.pop() {
match url_var.val.read().deref() {
Val::Str(url_val) => {
url = url_val.to_string();
},
_ => {
return Err(Error::HttpArgs(format!("URL must be a string")));
}
}
}
let map = ValRef::new(Val::Map(OrdMap::default()));
let request = HTTPRequest {
url,
method,
headers,
bearer,
body,
timeout,
query,
results: map.clone(),
waker: waker.clone(),
};
env.stack.push(Variable::refval(map));
request.send(env);
},
Self::ParseResponse => {
let mut context = env.self_ptr();
if let Some(context_var) = env.stack.pop() {
if let Some(obj) = context_var.try_obj() {
context = obj;
}
}
if let Some(response) = env.stack.pop() {
match response.val.read().deref() {
Val::Map(response) => {
let mut content_type = "stof".to_string();
if let Some(ctype) = response.get(&ValRef::new(Val::Str("content_type".into()))) {
content_type = ctype.read().to_string();
}
if let Some(body) = response.get(&ValRef::new(Val::Str("bytes".into()))) {
match body.read().deref() {
Val::Blob(bytes) => {
graph.binary_import(&content_type, bytes.clone(), Some(context.clone()), &Profile::default())?;
env.stack.push(Variable::val(Val::Obj(context)));
},
_ => {
return Err(Error::HttpSendError(format!("parse response map 'bytes' body must be a blob")));
}
}
} else {
return Err(Error::HttpSendError(format!("parse response map must have a 'bytes' key-value blob body")));
}
},
_ => {
return Err(Error::HttpSendError(format!("parse response must be a map")))
}
}
}
},
Self::TextBody => {
if let Some(response) = env.stack.pop() {
match response.val.read().deref() {
Val::Map(response) => {
if let Some(body) = response.get(&ValRef::new(Val::Str("bytes".into()))) {
match body.read().deref() {
Val::Blob(bytes) => {
if let Ok(res) = str::from_utf8(&bytes) {
env.stack.push(Variable::val(Val::Str(res.into())));
} else {
env.stack.push(Variable::val(Val::Null));
}
},
_ => {
return Err(Error::HttpSendError(format!("text body response map 'bytes' value must be a blob")));
}
}
} else {
return Err(Error::HttpSendError(format!("text body must have a 'bytes' key-value blob body")));
}
},
_ => {
return Err(Error::HttpSendError(format!("text body response must be a map")))
}
}
}
},
Self::BodySize => {
if let Some(response) = env.stack.pop() {
match response.val.read().deref() {
Val::Map(response) => {
if let Some(body) = response.get(&ValRef::new(Val::Str("bytes".into()))) {
match body.read().deref() {
Val::Blob(bytes) => {
env.stack.push(Variable::val(Val::Num(Num::Units(bytes.len() as f64, Units::Bytes))));
},
_ => {
return Err(Error::HttpSendError(format!("blob body size response map 'bytes' value must be a blob")));
}
}
} else {
return Err(Error::HttpSendError(format!("blob body size must have a 'bytes' key-value blob body")));
}
},
_ => {
return Err(Error::HttpSendError(format!("blob body size response must be a map")))
}
}
}
},
Self::BlobBody => {
if let Some(response) = env.stack.pop() {
match response.val.read().deref() {
Val::Map(response) => {
if let Some(body) = response.get(&ValRef::new(Val::Str("bytes".into()))) {
match body.read().deref() {
Val::Blob(bytes) => {
env.stack.push(Variable::val(Val::Blob(bytes.clone())));
},
_ => {
return Err(Error::HttpSendError(format!("blob body response map 'bytes' value must be a blob")));
}
}
} else {
return Err(Error::HttpSendError(format!("blob body must have a 'bytes' key-value blob body")));
}
},
_ => {
return Err(Error::HttpSendError(format!("blob body response must be a map")))
}
}
}
},
Self::ResponseSuccess => {
if let Some(response_var) = env.stack.pop() {
match response_var.val.read().deref() {
Val::Map(response) => {
if let Some(status) = response.get(&ValRef::new(Val::Str("status".into()))) {
match status.read().deref() {
Val::Num(num) => {
let v = num.int();
env.stack.push(Variable::val(Val::Bool(v >= 200 && v < 300)));
},
_ => {
env.stack.push(Variable::val(Val::Bool(false)));
}
}
} else {
env.stack.push(Variable::val(Val::Bool(false)));
}
},
_ => {}
}
}
},
Self::ClientError => {
if let Some(response_var) = env.stack.pop() {
match response_var.val.read().deref() {
Val::Map(response) => {
if let Some(status) = response.get(&ValRef::new(Val::Str("status".into()))) {
match status.read().deref() {
Val::Num(num) => {
let v = num.int();
env.stack.push(Variable::val(Val::Bool(v >= 400 && v < 500)));
},
_ => {
env.stack.push(Variable::val(Val::Bool(false)));
}
}
} else {
env.stack.push(Variable::val(Val::Bool(false)));
}
},
_ => {}
}
}
},
Self::ServerError => {
if let Some(response_var) = env.stack.pop() {
match response_var.val.read().deref() {
Val::Map(response) => {
if let Some(status) = response.get(&ValRef::new(Val::Str("status".into()))) {
match status.read().deref() {
Val::Num(num) => {
let v = num.int();
env.stack.push(Variable::val(Val::Bool(v >= 500 && v < 600)));
},
_ => {
env.stack.push(Variable::val(Val::Bool(false)));
}
}
} else {
env.stack.push(Variable::val(Val::Bool(false)));
}
},
_ => {}
}
}
},
}
Ok(None)
}
}