deserialization boilerplate
This commit is contained in:
parent
0ced95dee7
commit
85ae3d601c
|
@ -5,8 +5,13 @@ edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "example-client"
|
||||||
|
path = "examples/client.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
chrono = "0.4"
|
||||||
confy = "0.4"
|
confy = "0.4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
pub mod config;
|
use epp_client::{epp::request, connection, epp::xml::EppXml, epp::response::EppResponse};
|
||||||
pub mod connection;
|
|
||||||
pub mod epp;
|
|
||||||
pub mod error;
|
|
||||||
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
use crate::{epp::request};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let mut client = match connection::connect("hexonet").await {
|
let mut client = match connection::connect("hexonet").await {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
println!("{}", client.greeting());
|
let greeting = client.greeting();
|
||||||
|
let greeting_object = EppResponse::deserialize(&greeting).unwrap();
|
||||||
|
println!("{:?}", greeting_object);
|
||||||
client
|
client
|
||||||
},
|
},
|
||||||
Err(e) => panic!("Error: {}", e)
|
Err(e) => panic!("Error: {}", e)
|
|
@ -2,3 +2,4 @@ pub mod object;
|
||||||
pub mod quick_xml;
|
pub mod quick_xml;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod xml;
|
pub mod xml;
|
||||||
|
pub mod response;
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::epp::xml::{EPP_XMLNS, EPP_XMLNS_XSI, EPP_XSI_SCHEMA_LOCATION};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct StringValue(String);
|
||||||
|
|
||||||
|
impl Default for StringValue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(String::from(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StringValueTrait {
|
||||||
|
fn to_string_value(&self) -> StringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringValueTrait for &str {
|
||||||
|
fn to_string_value(&self) -> StringValue {
|
||||||
|
StringValue(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename = "epp")]
|
#[serde(rename = "epp")]
|
||||||
pub struct EppObject<T> {
|
pub struct EppObject<T> {
|
||||||
|
@ -8,5 +29,48 @@ pub struct EppObject<T> {
|
||||||
pub xmlns_xsi: String,
|
pub xmlns_xsi: String,
|
||||||
#[serde(rename = "xsi:schemaLocation")]
|
#[serde(rename = "xsi:schemaLocation")]
|
||||||
pub xsi_schema_location: String,
|
pub xsi_schema_location: String,
|
||||||
|
#[serde(alias = "greeting")]
|
||||||
pub data: T,
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename = "options")]
|
||||||
|
pub struct Options {
|
||||||
|
pub version: StringValue,
|
||||||
|
pub lang: StringValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
pub fn build(version: &str, lang: &str) -> Options {
|
||||||
|
Options {
|
||||||
|
version: version.to_string_value(),
|
||||||
|
lang: lang.to_string_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename = "svcExtension")]
|
||||||
|
pub struct ServiceExtension {
|
||||||
|
#[serde(rename = "extURI")]
|
||||||
|
pub ext_uris: Option<Vec<StringValue>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Services {
|
||||||
|
#[serde(rename = "objURI")]
|
||||||
|
pub obj_uris: Vec<StringValue>,
|
||||||
|
#[serde(rename = "svcExtension")]
|
||||||
|
pub svc_ext: Option<ServiceExtension>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EppObject<T> {
|
||||||
|
pub fn new(data: T) -> EppObject<T> {
|
||||||
|
EppObject {
|
||||||
|
data: data,
|
||||||
|
xmlns: EPP_XMLNS.to_string(),
|
||||||
|
xmlns_xsi: EPP_XMLNS_XSI.to_string(),
|
||||||
|
xsi_schema_location: EPP_XSI_SCHEMA_LOCATION.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
|
use quick_xml::de::from_str;
|
||||||
use quick_xml::se;
|
use quick_xml::se;
|
||||||
use serde::Serialize;
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use crate::epp::object::EppObject;
|
use crate::epp::object::EppObject;
|
||||||
use crate::epp::xml::{EppXml, EPP_XML_HEADER};
|
use crate::epp::xml::{EppXml, EPP_XML_HEADER};
|
||||||
|
|
||||||
impl<T: Serialize> EppXml for EppObject<T> {
|
impl<T: Serialize + DeserializeOwned> EppXml for EppObject<T> {
|
||||||
|
type Object = EppObject<T>;
|
||||||
|
|
||||||
fn serialize(&self) -> Result<String, Box<dyn Error>> {
|
fn serialize(&self) -> Result<String, Box<dyn Error>> {
|
||||||
let epp_xml = format!("{}\r\n{}", EPP_XML_HEADER, se::to_string(self)?);
|
let epp_xml = format!("{}\r\n{}", EPP_XML_HEADER, se::to_string(self)?);
|
||||||
|
|
||||||
Ok(epp_xml)
|
Ok(epp_xml)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize(epp_xml: &str) -> Result<Self::Object, Box<dyn Error>> {
|
||||||
|
match from_str(epp_xml) {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(e) => Err(format!("epp-client Deserialization Error: {}", e).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,39 +2,17 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use crate::epp::object::EppObject;
|
use crate::epp::object::{
|
||||||
|
EppObject, Options, ServiceExtension, Services, StringValue, StringValueTrait,
|
||||||
const EPP_XMLNS: &str = "urn:ietf:params:xml:ns:epp-1.0";
|
};
|
||||||
const EPP_XMLNS_XSI: &str = "http://www.w3.org/2001/XMLSchema-instance";
|
use crate::epp::xml::{EPP_LANG, EPP_VERSION, EPP_XMLNS, EPP_XMLNS_XSI, EPP_XSI_SCHEMA_LOCATION};
|
||||||
const EPP_XSI_SCHEMA_LOCATION: &str = "urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd";
|
|
||||||
|
|
||||||
const EPP_VERSION: &str = "1.0";
|
|
||||||
const EPP_LANG: &str = "en";
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct StringValue(String);
|
|
||||||
|
|
||||||
impl Default for StringValue {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(String::from(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait StringValueTrait {
|
|
||||||
fn to_string_value(&self) -> StringValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StringValueTrait for &str {
|
|
||||||
fn to_string_value(&self) -> StringValue {
|
|
||||||
StringValue(self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum RequestType {
|
pub enum RequestType {
|
||||||
Hello,
|
Hello,
|
||||||
Command {
|
#[serde(rename = "command")]
|
||||||
|
CommandLogin {
|
||||||
login: Login,
|
login: Login,
|
||||||
#[serde(rename = "clTRID")]
|
#[serde(rename = "clTRID")]
|
||||||
client_tr_id: StringValue,
|
client_tr_id: StringValue,
|
||||||
|
@ -48,15 +26,6 @@ pub enum RequestType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<RequestType> EppObject<RequestType> {
|
impl<RequestType> EppObject<RequestType> {
|
||||||
pub fn new(data: RequestType) -> EppObject<RequestType> {
|
|
||||||
EppObject {
|
|
||||||
data: data,
|
|
||||||
xmlns: EPP_XMLNS.to_string(),
|
|
||||||
xmlns_xsi: EPP_XMLNS_XSI.to_string(),
|
|
||||||
xsi_schema_location: EPP_XSI_SCHEMA_LOCATION.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_client_tr_id(username: &str) -> Result<String, Box<dyn Error>> {
|
pub fn generate_client_tr_id(username: &str) -> Result<String, Box<dyn Error>> {
|
||||||
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
|
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
|
||||||
Ok(format!("{}:{}", username, timestamp.as_secs()))
|
Ok(format!("{}:{}", username, timestamp.as_secs()))
|
||||||
|
@ -75,37 +44,6 @@ impl Hello {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
#[serde(rename = "options")]
|
|
||||||
pub struct LoginOptions {
|
|
||||||
version: StringValue,
|
|
||||||
lang: StringValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginOptions {
|
|
||||||
pub fn build(version: &str, lang: &str) -> LoginOptions {
|
|
||||||
LoginOptions {
|
|
||||||
version: version.to_string_value(),
|
|
||||||
lang: lang.to_string_value(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
#[serde(rename = "svcExtension")]
|
|
||||||
pub struct ServiceExtension {
|
|
||||||
#[serde(rename = "extURI")]
|
|
||||||
ext_uris: Option<Vec<StringValue>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub struct Services {
|
|
||||||
#[serde(rename = "objURI")]
|
|
||||||
obj_uris: Vec<StringValue>,
|
|
||||||
#[serde(rename = "svcExtension")]
|
|
||||||
svc_ext: Option<ServiceExtension>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
login: Login,
|
login: Login,
|
||||||
|
@ -120,7 +58,7 @@ pub struct Login {
|
||||||
username: StringValue,
|
username: StringValue,
|
||||||
#[serde(rename = "pw", default)]
|
#[serde(rename = "pw", default)]
|
||||||
password: StringValue,
|
password: StringValue,
|
||||||
options: LoginOptions,
|
options: Options,
|
||||||
#[serde(rename = "svcs")]
|
#[serde(rename = "svcs")]
|
||||||
services: Services,
|
services: Services,
|
||||||
}
|
}
|
||||||
|
@ -130,7 +68,7 @@ impl Login {
|
||||||
let login = Login {
|
let login = Login {
|
||||||
username: username.to_string_value(),
|
username: username.to_string_value(),
|
||||||
password: password.to_string_value(),
|
password: password.to_string_value(),
|
||||||
options: LoginOptions {
|
options: Options {
|
||||||
version: EPP_VERSION.to_string_value(),
|
version: EPP_VERSION.to_string_value(),
|
||||||
lang: EPP_LANG.to_string_value(),
|
lang: EPP_LANG.to_string_value(),
|
||||||
},
|
},
|
||||||
|
@ -148,13 +86,13 @@ impl Login {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
EppRequest::new(RequestType::Command {
|
EppRequest::new(RequestType::CommandLogin {
|
||||||
login: login,
|
login: login,
|
||||||
client_tr_id: client_tr_id.to_string_value(),
|
client_tr_id: client_tr_id.to_string_value(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_options(&mut self, options: LoginOptions) {
|
pub fn set_options(&mut self, options: Options) {
|
||||||
self.options = options;
|
self.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,118 @@
|
||||||
|
// use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::epp::object::{EppObject, Options, ServiceExtension, Services, StringValue};
|
||||||
|
use crate::epp::xml::{EPP_XMLNS, EPP_XMLNS_XSI, EPP_XSI_SCHEMA_LOCATION};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ResponseType {
|
||||||
|
#[serde(rename = "greeting")]
|
||||||
|
Greeting(Greeting),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type EppResponse = EppObject<ResponseType>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, PartialEq)]
|
||||||
|
pub struct ServiceMenu {
|
||||||
|
pub options: Options,
|
||||||
|
pub services: Services,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
struct FlattenedServiceMenu {
|
||||||
|
pub version: StringValue,
|
||||||
|
pub lang: StringValue,
|
||||||
|
#[serde(rename = "objURI")]
|
||||||
|
pub obj_uris: Vec<StringValue>,
|
||||||
|
#[serde(rename = "svcExtension")]
|
||||||
|
pub svc_ext: Option<ServiceExtension>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ServiceMenu {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let flattened_svc_menu = FlattenedServiceMenu::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let svc_menu = ServiceMenu {
|
||||||
|
options: Options {
|
||||||
|
version: flattened_svc_menu.version,
|
||||||
|
lang: flattened_svc_menu.lang,
|
||||||
|
},
|
||||||
|
services: Services {
|
||||||
|
obj_uris: flattened_svc_menu.obj_uris,
|
||||||
|
svc_ext: flattened_svc_menu.svc_ext,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(svc_menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct All;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Access {
|
||||||
|
all: All,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Admin;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Prov;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Purpose {
|
||||||
|
admin: Admin,
|
||||||
|
prov: Prov,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Ours;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Public;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Recipient {
|
||||||
|
ours: Ours,
|
||||||
|
public: Public,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Stated;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Retention {
|
||||||
|
stated: Stated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Statement {
|
||||||
|
purpose: Purpose,
|
||||||
|
recipient: Recipient,
|
||||||
|
retention: Retention,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Dcp {
|
||||||
|
access: Access,
|
||||||
|
statement: Statement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct Greeting {
|
||||||
|
#[serde(rename = "svID")]
|
||||||
|
service_id: String,
|
||||||
|
#[serde(rename = "svDate")]
|
||||||
|
service_date: String,
|
||||||
|
#[serde(rename = "svcMenu")]
|
||||||
|
svc_menu: ServiceMenu,
|
||||||
|
dcp: Dcp,
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
// use crate::epp::object::EppObject;
|
||||||
|
|
||||||
pub const EPP_XML_HEADER: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#;
|
pub const EPP_XML_HEADER: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#;
|
||||||
|
pub const EPP_XMLNS: &str = "urn:ietf:params:xml:ns:epp-1.0";
|
||||||
|
pub const EPP_XMLNS_XSI: &str = "http://www.w3.org/2001/XMLSchema-instance";
|
||||||
|
pub const EPP_XSI_SCHEMA_LOCATION: &str = "urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd";
|
||||||
|
|
||||||
|
pub const EPP_VERSION: &str = "1.0";
|
||||||
|
pub const EPP_LANG: &str = "en";
|
||||||
|
|
||||||
pub trait EppXml {
|
pub trait EppXml {
|
||||||
|
type Object;
|
||||||
|
|
||||||
fn serialize(&self) -> Result<String, Box<dyn Error>>;
|
fn serialize(&self) -> Result<String, Box<dyn Error>>;
|
||||||
// fn deserialize(&self) -> Result<Self, Box<dyn Error>>;
|
fn deserialize(epp_xml: &str) -> Result<Self::Object, Box<dyn Error>>;
|
||||||
}
|
}
|
||||||
|
|
58
src/lib.rs
58
src/lib.rs
|
@ -1,34 +1,34 @@
|
||||||
// pub mod config;
|
pub mod config;
|
||||||
// pub mod connection;
|
pub mod connection;
|
||||||
// pub mod epp;
|
pub mod epp;
|
||||||
// pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod tests {
|
mod tests {
|
||||||
// use super::config;
|
use super::config;
|
||||||
// use super::connection;
|
use super::connection;
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn config() {
|
fn config() {
|
||||||
// let servers = &config::CONFIG.servers;
|
let servers = &config::CONFIG.servers;
|
||||||
|
|
||||||
// ()
|
()
|
||||||
// }
|
}
|
||||||
|
|
||||||
// macro_rules! aw {
|
macro_rules! aw {
|
||||||
// ($e:expr) => {
|
($e:expr) => {
|
||||||
// tokio_test::block_on($e)
|
tokio_test::block_on($e)
|
||||||
// };
|
};
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn connect() {
|
fn connect() {
|
||||||
// let mut client = match aw!(connection::connect("hexonet")) {
|
let mut client = match aw!(connection::connect("hexonet")) {
|
||||||
// Ok(client) => {
|
Ok(client) => {
|
||||||
// println!("{}", client.greeting());
|
println!("{}", client.greeting());
|
||||||
// client
|
client
|
||||||
// },
|
},
|
||||||
// Err(e) => panic!("Error: {}", e)
|
Err(e) => panic!("Error: {}", e)
|
||||||
// };
|
};
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
Loading…
Reference in New Issue