diff --git a/composer.lock b/composer.lock index 95ee19e49..abda99c4b 100644 --- a/composer.lock +++ b/composer.lock @@ -1834,6 +1834,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-11-30T07:30:19+00:00" }, { diff --git a/example.php b/example.php index ef65d4ec8..b16fec33a 100644 --- a/example.php +++ b/example.php @@ -18,6 +18,7 @@ use Appwrite\SDK\Language\SwiftClient; use Appwrite\SDK\Language\DotNet; use Appwrite\SDK\Language\Flutter; +use Appwrite\SDK\Language\Rust; use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Kotlin; @@ -395,8 +396,32 @@ function getSSLPage($url) { ; $sdk->generate(__DIR__ . '/examples/CLI'); - // Android + // Rust + $rust = new Rust(); + $rust->setPackageName('appwrite'); + + $sdk = new SDK($rust, new Swagger2($spec)); + $sdk + ->setName('NAME') + ->setDescription('Repo description goes here') + ->setShortDescription('Repo short description goes here') + ->setURL('https://example.com') + ->setLogo('https://appwrite.io/v1/images/console.png') + ->setLicenseContent('test test test') + ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') + ->setChangelog('**CHANGELOG**') + ->setVersion('0.0.1') + ->setGitUserName('repoowner') + ->setGitRepoName('reponame') + ->setDefaultHeaders([ + 'X-Appwrite-Response-Format' => '0.7.0', + ]) + ; + + $sdk->generate(__DIR__ . '/examples/rust'); + + // Android $sdk = new SDK(new Android(), new Swagger2($spec)); $sdk diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php new file mode 100644 index 000000000..fe4a699b2 --- /dev/null +++ b/src/SDK/Language/Rust.php @@ -0,0 +1,262 @@ + 'packageName', + ]; + + /** + * @param string $name + * @return $this + */ + public function setPackageName($name) + { + $this->setParam('packageName', $name); + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return 'Rust'; + } + + /** + * Get Language Keywords List + * + * @return array + */ + public function getKeywords() + { + return [ + "type", + "as", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + "async", + "await", + "dyn", + "abstract", + "become", + "box", + "do", + "final", + "macro", + "override", + "priv", + "typeof", + "unsized", + "virtual", + "yield", + "try" + ]; + } + + /** + * @param $type + * @return string + */ + public function getTypeName($type) + { + switch ($type) { + case self::TYPE_OBJECT: + return 'Option>'; + break; + case self::TYPE_INTEGER: + return 'i64'; + break; + case self::TYPE_STRING: + return '&str'; + break; + case self::TYPE_FILE: + return 'std::path::PathBuf'; + break; + case self::TYPE_BOOLEAN: + return 'bool'; + break; + case self::TYPE_ARRAY: + return '&[&str]'; + case self::TYPE_NUMBER: + return 'f64'; + break; + } + + return $type; + } + + /** + * @param array $param + * @return string + */ + public function getParamDefault(array $param) + { + return ""; + } + + /** + * @param array $param + * @return string + */ + public function getParamExample(array $param) + { + $type = $param['type'] ?? ''; + $example = $param['example'] ?? ''; + + $output = ''; + + if(empty($example) && $example !== 0 && $example !== false) { + switch ($type) { + case self::TYPE_FILE: + $output .= 'std::path::PathBuf::from("./path-to-files/image.jpg")'; + break; + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + $output .= '0'; + break; + case self::TYPE_BOOLEAN: + $output .= 'false'; + break; + case self::TYPE_STRING: + $output .= 'String::new()'; + break; + case self::TYPE_OBJECT: + $output .= 'new Object()'; + break; + case self::TYPE_ARRAY: + $output .= '&[]'; + break; + } + } + else { + switch ($type) { + case self::TYPE_OBJECT: + case self::TYPE_FILE: + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_ARRAY: + $output .= $example; + break; + case self::TYPE_BOOLEAN: + $output .= ($example) ? 'true' : 'false'; + break; + case self::TYPE_STRING: + $output .= sprintf('"%s"', $example); + break; + } + } + + return $output; + } + + /** + * @return array + */ + public function getFiles() + { + return [ + [ + 'scope' => 'default', + 'destination' => 'Cargo.toml', + 'template' => '/rust/Cargo.toml.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => '/rust/README.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => '/rust/CHANGELOG.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => '/rust/LICENSE.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => '/rust/docs/example.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'src/lib.rs', + 'template' => '/rust/src/lib.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'src/client.rs', + 'template' => '/rust/src/client.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'src/services/mod.rs', + 'template' => '/rust/src/services/mod.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'src/services/exception.rs', + 'template' => '/rust/src/services/exception.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'service', + 'destination' => 'src/services/{{service.name | caseDash}}.rs', + 'template' => '/rust/src/services/service.rs.twig', + 'minify' => false, + ], + ]; + } +} + diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index 3d96c5ed3..4253141fc 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -149,7 +149,7 @@ public function __construct(Language $language, Spec $spec) $this->twig->addFilter(new TwigFilter('dartComment', function ($value) { $value = explode("\n", $value); foreach ($value as $key => $line) { - $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); + $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); } return implode("\n", $value); }, ['is_safe' => ['html']])); diff --git a/templates/dart/lib/services/service.dart.twig b/templates/dart/lib/services/service.dart.twig index 1e77a6b8c..28d26c388 100644 --- a/templates/dart/lib/services/service.dart.twig +++ b/templates/dart/lib/services/service.dart.twig @@ -10,11 +10,11 @@ class {{ service.name | caseUcfirst }} extends Service { {{ service.name | caseUcfirst }}(Client client): super(client); {% for method in service.methods %} - /// {{ method.title }} + /// {{ method.title }} {% if method.description %} - /// + /// {{ method.description|dartComment }} - /// + /// {% endif %} {% if method.type == 'webAuth' %}Future{% elseif method.type == 'location' %} Future {% else %} {% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel }}({{ _self.method_parameters(method.parameters) }}) async { final String path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; diff --git a/templates/node/lib/services/service.js.twig b/templates/node/lib/services/service.js.twig index 26a8df85e..94b07bf09 100644 --- a/templates/node/lib/services/service.js.twig +++ b/templates/node/lib/services/service.js.twig @@ -53,4 +53,4 @@ class {{ service.name | caseUcfirst }} extends Service { {% endfor %} } -module.exports = {{ service.name | caseUcfirst }}; \ No newline at end of file +module.exports = {{ service.name | caseUcfirst }}; diff --git a/templates/rust/.gitignore b/templates/rust/.gitignore new file mode 100644 index 000000000..0eec03d61 --- /dev/null +++ b/templates/rust/.gitignore @@ -0,0 +1,11 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/templates/rust/CHANGELOG.md.twig b/templates/rust/CHANGELOG.md.twig new file mode 100644 index 000000000..a544d26c9 --- /dev/null +++ b/templates/rust/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{sdk.changelog}} \ No newline at end of file diff --git a/templates/rust/Cargo.toml.twig b/templates/rust/Cargo.toml.twig new file mode 100644 index 000000000..8aa7e034e --- /dev/null +++ b/templates/rust/Cargo.toml.twig @@ -0,0 +1,19 @@ +[package] +name = "{{ language.params.packageName }}" +version = "{{ sdk.version }}" +authors = ["Appwrite Team "] +edition = "2018" +description = "{{ sdk.shortDescription }}" +licence = "{{ sdk.licenseName }}" +repository = "{{ sdk.gitURL }}" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +url = "2.2.1" +reqwest = { version = "0.11", features = ["json", "blocking", "multipart"] } + +# Serde +serde = "1.0.124" +serde_derive = "1.0.124" +serde_json = "1.0.59" \ No newline at end of file diff --git a/templates/rust/LICENSE.twig b/templates/rust/LICENSE.twig new file mode 100644 index 000000000..0e8c361b4 --- /dev/null +++ b/templates/rust/LICENSE.twig @@ -0,0 +1 @@ +{{sdk.licenseContent}} \ No newline at end of file diff --git a/templates/rust/README.md.twig b/templates/rust/README.md.twig new file mode 100644 index 000000000..d6fe7557e --- /dev/null +++ b/templates/rust/README.md.twig @@ -0,0 +1,35 @@ +# {{ spec.title }} {{sdk.name}} SDK + +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?v=1) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?v=1) +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} + +{{ sdk.description }} + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +To install via [Crates.io](https://www.crates.io/) add the following to your Cargo.toml under dependencies: + +```toml +{{ language.params.packageName }} = "1" +``` + +{% if sdk.gettingStarted %} + +{{ sdk.gettingStarted|raw }} +{% endif %} + +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + +## License + +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. \ No newline at end of file diff --git a/templates/rust/docs/example.md.twig b/templates/rust/docs/example.md.twig new file mode 100644 index 000000000..fe5bd30b9 --- /dev/null +++ b/templates/rust/docs/example.md.twig @@ -0,0 +1,14 @@ +let mut client = {{ language.params.packageName }}::client::Client::new(); + +client.set_endpoint("https://[HOSTNAME_OR_IP]/v1"); // Your API Endpoint +{% for node in method.security %} +{% for key,header in node|keys %} +client.set_{{header|caseSnake}}("{{node[header]['x-appwrite']['demo']}}"); // {{node[header].description}} +{% endfor %} +{% endfor %} + +let {{ service.name | caseSnake }} = {{ language.params.packageName }}::services::{{ service.name | caseUcfirst }}::new(&client); + +let response = {{ service.name | caseSnake }}.{{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{% if parameter.required %}{% if not loop.first %}, {% endif %}{% if parameter | paramExample == "File.new()" %}{{spec.title | caseUcfirst}}::{{ parameter | paramExample }}{% else %}{{ parameter | paramExample }}{% endif %}{% endif %}{% endfor %}).unwrap(); + +println!("{}", response.text().unwrap()); \ No newline at end of file diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig new file mode 100644 index 000000000..5bf64f708 --- /dev/null +++ b/templates/rust/src/client.rs.twig @@ -0,0 +1,317 @@ +use reqwest::header::HeaderMap; +use std::{collections::HashMap, str::FromStr}; +use std::path::PathBuf; +use crate::services::{{spec.title | caseUcfirst}}Exception; + +#[derive(Clone)] +pub struct Client { + endpoint: url::Url, + headers: HeaderMap, + client: reqwest::blocking::Client, +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ParamType { + Bool(bool), + Number(i64), + String(String), + Array(Vec), + FilePath(PathBuf), + Object(HashMap), + Float(f64), + OptionalBool(Option), + OptionalNumber(Option), + OptionalArray(Option>), + OptionalFilePath(Option), + OptionalObject(Option>), + OptionalFloat(Option) +} + +// Converts optionals into normal ParamTypes +fn handleOptional(param: ParamType) -> Option { + match param { + ParamType::OptionalBool(value) => match value { + Some(data) => Some(ParamType::Bool(data)), + None => None + } + ParamType::OptionalNumber(value) => match value { + Some(data) => Some(ParamType::Number(data)), + None => None + } + ParamType::OptionalArray(value) => match value { + Some(data) => Some(ParamType::Array(data)), + None => None + } + ParamType::OptionalFilePath(value) => match value { + Some(data) => Some(ParamType::FilePath(data)), + None => None + } + ParamType::OptionalObject(value) => match value { + Some(data) => Some(ParamType::Object(data)), + None => None + } + ParamType::OptionalFloat(value) => match value { + Some(data) => Some(ParamType::Float(data)), + None => None + } + _ => Some(param) + } +} + +/// Example +/// ```rust +/// let mut client = {{ language.params.packageName }}::client::Client::new(); +/// +/// client.set_endpoint("Your Endpoint URL"); +/// client.set_project("Your Project ID"); +/// client.set_key("Your API Key"); +/// +/// // Create a user as a example +/// let userService = {{ language.params.packageName }}::services::Users::new(&client); +/// let response = userService.create("amadeus@example.com", "supersecurepassword", "Wolfgang Amadeus Mozart"); +/// +/// println!("{}", response.text().unwrap()); // Here you can also check the status code to see success +/// ``` +impl Client { + pub fn new() -> Self { + let mut new_headers = HeaderMap::new(); + + new_headers.insert("x-sdk-version", "{{spec.title | caseDash}}:{{ language.name | caseLower }}:{{ sdk.version }}".parse().unwrap()); + new_headers.insert("user-agent", format!("{}-rust-{}", std::env::consts::OS, "{{sdk.version}}").parse().unwrap()); +{% for key,header in spec.global.defaultHeaders %} + new_headers.insert("{{key}}", "{{header}}".parse().unwrap()); +{% endfor %} + + Self { + endpoint: "{{spec.endpoint}}".parse().unwrap(), + headers: new_headers, + client: reqwest::blocking::Client::builder() + .build().unwrap(), + } + } + + pub fn add_header(&mut self, key: String, value: String) { + self.headers.append( + reqwest::header::HeaderName::from_str(&key).unwrap(), + (&value.to_lowercase()).parse().unwrap(), + ); + } + + pub fn add_self_signed(&mut self, value: bool) { + self.client = reqwest::blocking::Client::builder().danger_accept_invalid_certs(value).build().unwrap(); + } + +{% for header in spec.global.headers %} +{% if header.description %} + /// Sets {{ header.description }} +{% endif %} + pub fn set_{{header.key | caseSnake}}(&mut self, value: &str) { + self.add_header("{{header.name}}".to_string(), value.to_string()) + } + +{% endfor %} + + pub fn set_endpoint(&mut self, endpoint: &str) { + self.endpoint = endpoint.parse().unwrap() + } + + pub fn call( + self, + method: &str, + path: &str, + headers: Option>, + params: Option>, + ) -> Result { + // If we have headers in the function call we combine them with the client headers. + + let mut content_type: String = "application/json".to_string(); + + let request_headers: HeaderMap = match headers { + Some(data) => { + let mut headers = self.headers.clone(); + + for (k, v) in data { + if k == "content-type" { + content_type = v.to_string() + } else { + headers.append( + reqwest::header::HeaderName::from_lowercase(k.as_bytes()).unwrap(), + (&v.to_lowercase()).parse().unwrap(), + ); + } + } + + headers + } + None => self.headers.clone(), + }; + + // Now start building request with reqwest + let method_type = match method { + "GET" => reqwest::Method::GET, + "POST" => reqwest::Method::POST, + "OPTIONS" => reqwest::Method::OPTIONS, + "PUT" => reqwest::Method::PUT, + "DELETE" => reqwest::Method::DELETE, + "HEAD" => reqwest::Method::HEAD, + "PATCH" => reqwest::Method::PATCH, + _ => reqwest::Method::GET, + }; + + let mut request = self + .client + .request(method_type, self.endpoint.join(&format!("{}{}", "v1", path)).unwrap()); + + match params { + Some(data) => { + let flattened_data = flatten(FlattenType::Normal(data.clone()), None); + + // Handle Optional Values + // Remove all optionals that result in None + // Turn all Optional____ into their non optional equivilants. + let mut buffer: Vec<(String, ParamType)> = Vec::new(); + for (k, v) in flattened_data { + match handleOptional(v) { + Some(data) => buffer.push((k, data)), + None => {} + } + } + let flattened_data = buffer; + + // First flatten the data and feed it into a FormData + if content_type.starts_with("multipart/form-data") { + let mut form = reqwest::blocking::multipart::Form::new(); + + for (k, v) in flattened_data.clone() { + match v { + ParamType::Bool(data) => { + form = form.text(k, data.to_string()); + } + ParamType::String(data) => form = form.text(k, data), + ParamType::FilePath(data) => form = form.file(k, data).unwrap(), + ParamType::Number(data) => form = form.text(k, data.to_string()), + ParamType::Float(data) => form = form.text(k, data.to_string()), + // This shouldn't be possible due to the flatten function, so we won't handle this for now + ParamType::Array(_data) => { + //todo: Feed this back into a flatten function if needed + }, + ParamType::Object(_data) => { + // Same for this + }, + _ => {} + } + } + request = request.multipart(form); + } + + if content_type.starts_with("application/json") && method != "GET" { + request = request.json(&data); + } + + if method == "GET" { + request = request.query(&queryize_data(flatten(FlattenType::Normal(data), None))); + } + } + None => {} + } + + request = request.headers(request_headers); + + match request.send() { + Ok(data) => { + if data.status().is_success() { + Ok(data) + } else { + let dataString = match data.text() { + Ok(data) => {data}, + Err(err) => { + // Last Resort. Called if string isn't even readable text. + return Err({{spec.title | caseUcfirst}}Exception::new(format!("A error occoured. ERR: {}, This could either be a connection error or an internal Appwrite error. Please check your Appwrite instance logs. ", err), 0, "".to_string())) + } + }; + + // Format error + Err(match serde_json::from_str(&dataString) { + Ok(data) => data, + Err(_err) => { + {{spec.title | caseUcfirst}}Exception::new(format!("{}", dataString), 0, "".to_string()) + } + }) + } + }, + Err(err) => { + // Throw {{spec.title | caseUcfirst}} Exception + Err({{spec.title | caseUcfirst}}Exception::new(format!("{}", err), 0, "".to_string())) + }, + } + } +} + +enum FlattenType { + Normal(HashMap), + Nested(Vec), +} + +fn queryize_data(data: Vec<(String, ParamType)>) -> Vec<(String, String)> { + let mut output: Vec<(String, String)> = Default::default(); + + for (k, v) in data { + match v { + ParamType::Bool(value) => output.push((k, value.to_string())), + ParamType::String(value) => output.push((k, value)), + ParamType::Number(value) => output.push((k, value.to_string())), + _ => {} + } + } + + output +} + +fn flatten(data: FlattenType, prefix: Option) -> Vec<(String, ParamType)> { + let mut output: Vec<(String, ParamType)> = Default::default(); + + match data { + FlattenType::Normal(data) => { + for (k, v) in data { + let final_key = match &prefix { + Some(current_prefix) => format!("{}[{}]", current_prefix, k), + None => k, + }; + + match v { + ParamType::Array(value) => { + output.append(&mut flatten(FlattenType::Nested(value), Some(final_key))); + } + ParamType::Object(value) => { + output.extend(flatten(FlattenType::Normal(value), Some(final_key)).into_iter()) + }, + value => { + output.push((final_key, value)); + } + } + } + } + + FlattenType::Nested(data) => { + for (k, v) in data.iter().enumerate() { + let final_key = match &prefix { + Some(current_prefix) => format!("{}[{}]", current_prefix, k), + None => k.to_string(), + }; + + match v { + ParamType::Array(value) => { + flatten(FlattenType::Nested(value.to_owned()), Some(final_key)) + .append(&mut output); + } + value => { + output.push((final_key, value.to_owned())); + } + } + } + } + } + + output +} diff --git a/templates/rust/src/lib.rs.twig b/templates/rust/src/lib.rs.twig new file mode 100644 index 000000000..2c9f10387 --- /dev/null +++ b/templates/rust/src/lib.rs.twig @@ -0,0 +1,26 @@ +/*! +This SDK is compatible with Appwrite server version 0.7.0. For older versions, please check previous releases. + +Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. +Appwrite aims to help you develop your apps faster and in a more secure way. Use the Rust SDK to integrate your app with the Appwrite server to easily start +interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to + +# Usage +This crate is [on crates.io](https://crates.io/crates/appwrite) and can be +used by adding `appwrite` to your dependencies in your project's `Cargo.toml`. + +```toml +[dependencies] +{{ language.params.packageName }} = "1" +``` + +# Contibution +This library is auto-generated by Appwrite custom SDK Generator. To learn more about how you can help us improve this SDK, please check the contribution guide before sending a pull-request. +*/ + +#![allow(non_snake_case)] +#[macro_use] +extern crate serde_derive; + +pub mod client; +pub mod services; diff --git a/templates/rust/src/services/exception.rs.twig b/templates/rust/src/services/exception.rs.twig new file mode 100644 index 000000000..2a4e148f6 --- /dev/null +++ b/templates/rust/src/services/exception.rs.twig @@ -0,0 +1,31 @@ +use std::fmt; +use std::error::Error; + +#[derive(Debug, Clone, Deserialize)] +pub struct {{spec.title | caseUcfirst}}Exception { + pub message: String, + pub code: i32, + pub version: String +} + +impl fmt::Display for {{spec.title | caseUcfirst}}Exception { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f,"ERROR: '{}' CODE: {}", self.message, self.code) + } +} + +impl Error for {{spec.title | caseUcfirst}}Exception { + fn description(&self) -> &str { + &self.message + } +} + +impl {{spec.title | caseUcfirst}}Exception { + pub fn new(message: String, code: i32, version: String) -> Self { + Self { + message: message, + code: code, + version: version + } + } +} \ No newline at end of file diff --git a/templates/rust/src/services/mod.rs.twig b/templates/rust/src/services/mod.rs.twig new file mode 100644 index 000000000..f9c3de964 --- /dev/null +++ b/templates/rust/src/services/mod.rs.twig @@ -0,0 +1,10 @@ +{% for service in spec.services %} +mod {{service.name}}; +{% endfor %} +mod exception; + +{% for service in spec.services %} +pub use self::{{service.name}}::{{service.name|caseUcfirst}}; +{% endfor %} + +pub use self::exception::{{spec.title | caseUcfirst}}Exception; \ No newline at end of file diff --git a/templates/rust/src/services/service.rs.twig b/templates/rust/src/services/service.rs.twig new file mode 100644 index 000000000..0062b6aa5 --- /dev/null +++ b/templates/rust/src/services/service.rs.twig @@ -0,0 +1,65 @@ +use crate::client::{Client, ParamType}; +use std::collections::HashMap; +use crate::services::{{spec.title | caseUcfirst}}Exception; + +#[derive(Clone)] +pub struct {{ service.name | caseUcfirst }} { + client: Client +} + +impl {{ service.name | caseUcfirst }} { + pub fn new(client: &Client) -> Self { + Self { + client: client.clone() + } + } +{% for method in service.methods %} + +{% if method.description %} +{{ method.description|dartComment }} +{% endif %} + pub fn {{ method.name | caseSnake }}(&self{% if method.parameters.all|length >= 1 %}, {% endif %}{% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword}}: {% if parameter.required != 1 %}Option<{% endif %}{{ parameter.type | typeName }}{% if parameter.required != 1%}>{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}) -> Result { + let path = "{{ method.path|replace({'{': '', '}': ""}) }}"{% for parameter in method.parameters.path %}.replace("{{ parameter.name | caseCamel }}", &{{ parameter.name | caseSnake }}){% endfor %}; + +{% if method.headers %} + let headers: HashMap = [ +{% for parameter in method.parameters.header %} + ("{{ parameter.name }}".to_string(), "{{ parameter.name | caseCamel }}".to_string()), +{% endfor %} +{% for key, header in method.headers %} + ("{{ key }}".to_string(), "{{ header }}".to_string()), +{% endfor %} + ].iter().cloned().collect(); +{% endif %} +{% for parameter in method.parameters.all %} +{% if parameter.required != 1 %} +{% if parameter.type == 'string' %} + + let {{ parameter.name | caseSnake | escapeKeyword}}:{{ parameter.type | typeName }} = match {{ parameter.name | caseSnake | escapeKeyword}} { + Some(data) => data, + None => "" + }; +{% endif %} +{% if parameter.type == 'array' %} + + let {{ parameter.name | caseSnake | escapeKeyword}}:{{ parameter.type | typeName }} = match {{ parameter.name | caseSnake | escapeKeyword}} { + Some(data) => data, + None => &[] + }; +{% endif %} +{% endif %} +{% endfor %} + + let params: HashMap = [ +{% for parameter in method.parameters.query %} + ("{{ parameter.name }}".to_string(), {% if parameter.type == 'number' %} ParamType::{% if parameter.required != 1 %}{% if parameter.type != 'string' %}Optional{% endif %}{% endif %}Float({% elseif parameter.type == 'integer' %} ParamType::{% if parameter.required != 1 %}Optional{% endif %}Number({% elseif parameter.type == 'string' %}ParamType::String({% elseif parameter.type == 'array' %}ParamType::Array({% elseif parameter.type == 'boolean' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}Bool({% elseif parameter.type == 'object' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}Object({% elseif parameter.type == 'file' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}FilePath({% endif %}{{ parameter.name | caseSnake | escapeKeyword }}{% if parameter.type == 'array' %}.into_iter().map(|x| ParamType::String(x.to_string())).collect(){% endif %}{% if parameter.type == 'string' %}.to_string(){% endif %}{% if parameter.type == 'object' %}.unwrap(){% endif %})), +{% endfor %} +{% for parameter in method.parameters.body %} + ("{{ parameter.name }}".to_string(), {% if parameter.type == 'number' %} ParamType::{% if parameter.required != 1 %}{% if parameter.type != 'string' %}Optional{% endif %}{% endif %}Float({% elseif parameter.type == 'integer' %} ParamType::{% if parameter.required != 1 %}Optional{% endif %}Number({% elseif parameter.type == 'string' %}ParamType::String({% elseif parameter.type == 'array' %}ParamType::Array({% elseif parameter.type == 'boolean' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}Bool({% elseif parameter.type == 'object' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}Object({% elseif parameter.type == 'file' %}ParamType::{% if parameter.required != 1 %}Optional{% endif %}FilePath({% endif %}{{ parameter.name | caseSnake | escapeKeyword }}{% if parameter.type == 'array' %}.into_iter().map(|x| ParamType::String(x.to_string())).collect(){% endif %}{% if parameter.type == 'string' %}.to_string(){% endif %}{% if parameter.type == 'object' %}.unwrap(){% endif %})), +{% endfor %} + ].iter().cloned().collect(); + + return self.client.clone().call("{{ method.method | caseUpper }}", &path, {% if method.headers %}Some(headers){% else %}None{% endif %}, Some(params) ); + } +{% endfor %} +} diff --git a/tests/SDKTest.php b/tests/SDKTest.php index 4a47cf7ec..46d94e45d 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -283,6 +283,19 @@ class SDKTest extends TestCase ...EXCEPTION_RESPONSES, ], ], + 'rust' => [ + 'class' => 'Appwrite\SDK\Language\Rust', + 'build' => [ + 'mkdir -p tests/sdks/rust/tests', + 'cp -R tests/languages/rust/* tests/sdks/rust/tests', + ], + 'envs' => [ + 'rust-1.50' => 'docker run --rm -v "$(pwd):/app" -w /app/tests/sdks/rust/tests rust:1.50 cargo run', + ], + 'supportRedirect' => true, + 'supportUpload' => true, + 'supportException' => true + ], 'python' => [ 'class' => 'Appwrite\SDK\Language\Python', @@ -332,7 +345,7 @@ public function testHTTPSuccess() throw new \Exception('Failed to fetch spec from Appwrite server'); } - $whitelist = ['php', 'cli', 'node', 'ruby', 'python', 'deno', 'dotnet', 'dart', 'flutter', 'web', 'android', 'kotlin', 'swift-server', 'swift-client']; + $whitelist = ['php', 'cli', 'node', 'ruby', 'python', 'deno', 'dotnet', 'dart', 'flutter', 'web', 'android', 'kotlin', 'swift-server', 'swift-client', 'rust']; foreach ($this->languages as $language => $options) { if (!empty($whitelist) && !in_array($language, $whitelist)) { diff --git a/tests/languages/rust/.gitignore b/tests/languages/rust/.gitignore new file mode 100644 index 000000000..9f970225a --- /dev/null +++ b/tests/languages/rust/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/tests/languages/rust/Cargo.toml b/tests/languages/rust/Cargo.toml new file mode 100644 index 000000000..c67c6115c --- /dev/null +++ b/tests/languages/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +packageName = { path = "../" } +reqwest = "0.11.5" +serde = "1.0.130" +serde_derive = "1.0.130" \ No newline at end of file diff --git a/tests/languages/rust/src/main.rs b/tests/languages/rust/src/main.rs new file mode 100644 index 000000000..e52a10c2f --- /dev/null +++ b/tests/languages/rust/src/main.rs @@ -0,0 +1,73 @@ +use packageName; +use std::path::PathBuf; +use reqwest::blocking::Response; +use serde_derive::Deserialize; + +#[derive(Deserialize)] +struct DefaultResponse { + result: Option, + message: Option +} + +fn parse_response(data: Response) -> String { + let response:DefaultResponse = match data.json() { + Ok(value) => value, + Err(err) => { + return err.to_string() + } + }; + + match response.result { + Some(data) => return data, + None => return match response.message { + Some(value) => value, + None => "Couldn't parse error.".to_string() + } + } + } + +fn main() { + let client = packageName::client::Client::new(); + + println!("Test Started"); + + let foo = packageName::services::Foo::new(&client); + let bar = packageName::services::Bar::new(&client); + let general = packageName::services::General::new(&client); + + // Foo + + println!("{}", parse_response(foo.get("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(foo.post("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(foo.put("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(foo.patch("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(foo.delete("string", 123, &["string in array"]).unwrap())); + + // Bar + + println!("{}", parse_response(bar.get("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(bar.post("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(bar.put("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(bar.patch("string", 123, &["string in array"]).unwrap())); + println!("{}", parse_response(bar.delete("string", 123, &["string in array"]).unwrap())); + + // General + + println!("{}", parse_response(general.redirect().unwrap())); + println!("{}", parse_response(general.upload("string", 123, &["string in array"], PathBuf::from("../../../resources/file.png")).unwrap())); + + match general.error400() { + Ok(data) => println!("{}", data.text().unwrap()), + Err(err) => println!("{}", err.message) + } + + match general.error500() { + Ok(data) => println!("{}", data.text().unwrap()), + Err(err) => println!("{}", err.message) + } + + match general.error502() { + Ok(data) => println!("{}", data.text().unwrap()), + Err(err) => println!("{}", err.message) + } +} \ No newline at end of file