diff --git a/.travis.yml b/.travis.yml index 99e74d250..0d6244f9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,9 @@ env: - SDK=SwiftServer55 - SDK=WebChromium - SDK=WebNode + - SDK=Rust + - SDK=RustBeta + - SDK=RustNightly notifications: email: diff --git a/example.php b/example.php index 4b3fbc909..a7a34b4b5 100644 --- a/example.php +++ b/example.php @@ -20,6 +20,7 @@ use Appwrite\SDK\Language\Flutter; use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Kotlin; +use Appwrite\SDK\Language\Rust; try { @@ -457,6 +458,31 @@ function getSSLPage($url) { ]) ; $sdk->generate(__DIR__ . '/examples/kotlin'); + + // Rust + $rust = new Rust(); + $rust->setPackageName('appwrite'); + + $sdk = new SDK($rust, new Swagger2($spec)); + + $sdk + ->setName('NAME') + ->setDescription('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 https://appwrite.io/docs') + ->setShortDescription('Appwrite Rust SDK') + ->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'); } catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php new file mode 100644 index 000000000..a28aa44be --- /dev/null +++ b/src/SDK/Language/Rust.php @@ -0,0 +1,299 @@ + '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" + ]; + } + + /** + * @return array + */ + public function getIdentifierOverrides() + { + return []; + } + + /** + * @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' => 'copy', + 'destination' => '.github/workflows/cargo-publish.yml', + 'template' => '/rust/.github/workflows/cargo-publish.yml', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => '/rust/CHANGELOG.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '.gitignore', + 'template' => '/rust/.gitignore.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => '/rust/LICENSE.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'src/models/attribute.rs', + 'template' => '/rust/src/models/attribute.rs.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' => 'default', + 'destination' => 'src/models/mod.rs', + 'template' => '/rust/src/models/mod.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'service', + 'destination' => 'src/services/{{service.name | caseDash}}.rs', + 'template' => '/rust/src/services/service.rs.twig', + 'minify' => false, + ], + [ + 'scope' => 'definition', + 'destination' => '/src/models/{{definition.name | caseSnake }}.rs', + 'template' => '/rust/src/models/model.rs.twig', + 'minify' => false, + ] + ]; + } +} diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index 436d16446..11b339bcc 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -141,6 +141,50 @@ public function __construct(Language $language, Spec $spec) } return implode("\n", $value); }, ['is_safe' => ['html']])); + + $this->twig->addFilter(new TwigFilter('comment2', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " * " . wordwrap($value[$key], 75, "\n * "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('comment3', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " * " . wordwrap($value[$key], 75, "\n * "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('dartComment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('dotnetComment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('swiftComment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('rubyComment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " # " . wordwrap($line, 75, "\n # "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('escapeDollarSign', function ($value) { return str_replace('$', '\$', $value); }, ['is_safe'=>['html']])); diff --git a/templates/rust/.github/workflows/cargo-publish.yml b/templates/rust/.github/workflows/cargo-publish.yml new file mode 100644 index 000000000..4d4969316 --- /dev/null +++ b/templates/rust/.github/workflows/cargo-publish.yml @@ -0,0 +1,22 @@ +name: Publish Package to crates.io +on: + release: + types: [published] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Dry Run Deployment + run: | + cargo publish --dry-run + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - name: Final Deployment + run: | + cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/templates/rust/.gitignore.twig b/templates/rust/.gitignore.twig new file mode 100644 index 000000000..5028f0063 --- /dev/null +++ b/templates/rust/.gitignore.twig @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# 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..5cb8bd406 --- /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 }}" +license = "{{ spec.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..ae5aea7cf --- /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 }} = "{{ sdk.version }}" +``` + +{% if sdk.gettingStarted %} + +{{ sdk.gettingStarted|raw }} +{% endif %} + +## Contribution + +This library is auto-generated by Appwrite's 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..8baa22d3f --- /dev/null +++ b/templates/rust/docs/example.md.twig @@ -0,0 +1,20 @@ +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 = match {{ 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 %}) { + Ok(response) => response, + Err(error) => { + println!("Error: {}", error); + return; + } +}; + +println!("{:?}", response); \ 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..ee66d208d --- /dev/null +++ b/templates/rust/src/client.rs.twig @@ -0,0 +1,324 @@ +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, +} + +pub static CHUNK_SIZE: u64 = 5*1024*1024; // 5MB + +#[derive(Clone, Deserialize, Serialize, Debug)] +#[serde(untagged)] +pub enum ParamType { + Bool(bool), + Number(i64), + String(String), + Array(Vec), + FilePath(PathBuf), + Object(HashMap), + Float(f64), + StreamData(Vec, String), + OptionalBool(Option), + OptionalNumber(Option), + OptionalArray(Option>), + OptionalFilePath(Option), + OptionalObject(Option>), + OptionalFloat(Option), + {% for definition in spec.definitions %} +{{definition.name | caseUcfirst}}(crate::models::{{definition.name|caseUcfirst}}), + {% endfor %} +} + +// 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()), + ParamType::StreamData(data, filename) => form = form.part(k, reqwest::blocking::multipart::Part::bytes(data).file_name(filename)), + // 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..967adc43b --- /dev/null +++ b/templates/rust/src/lib.rs.twig @@ -0,0 +1,27 @@ +/*! +This SDK is compatible with Appwrite server version 0.12.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/{{ language.params.packageName }}) and can be +used by adding `{{ language.params.packageName }}` to your dependencies in your project's `Cargo.toml`. + +```toml +[dependencies] +{{ language.params.packageName }} = "{{ sdk.version }}" +``` + +# 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(unused_imports)] +#![allow(non_snake_case)] +#[macro_use] +extern crate serde_derive; + +pub mod client; +pub mod services; +pub mod models; \ No newline at end of file diff --git a/templates/rust/src/models/attribute.rs.twig b/templates/rust/src/models/attribute.rs.twig new file mode 100644 index 000000000..f7050ee42 --- /dev/null +++ b/templates/rust/src/models/attribute.rs.twig @@ -0,0 +1,12 @@ +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(untagged)] +pub enum Attribute { + AttributeString(crate::models::AttributeString), + AttributeInteger(crate::models::AttributeInteger), + AttributeFloat(crate::models::AttributeFloat), + AttributeBoolean(crate::models::AttributeBoolean), + AttributeEmail(crate::models::AttributeEmail), + AttributeEnum(crate::models::AttributeEnum), + AttributeIp(crate::models::AttributeIp), + AttributeUrl(crate::models::AttributeUrl) +} \ No newline at end of file diff --git a/templates/rust/src/models/mod.rs.twig b/templates/rust/src/models/mod.rs.twig new file mode 100644 index 000000000..acf0e3623 --- /dev/null +++ b/templates/rust/src/models/mod.rs.twig @@ -0,0 +1,8 @@ +{% for definition in spec.definitions %} +mod {{definition.name | caseSnake}}; +pub use self::{{definition.name | caseSnake}}::{{definition.name|caseUcfirst}}; +{% if definition.name == "attributeList" %} +mod attribute; +pub use self::attribute::Attribute; +{% endif %} +{% endfor %} \ No newline at end of file diff --git a/templates/rust/src/models/model.rs.twig b/templates/rust/src/models/model.rs.twig new file mode 100644 index 000000000..3e22fc2b4 --- /dev/null +++ b/templates/rust/src/models/model.rs.twig @@ -0,0 +1,118 @@ +#![allow(unused)] +use serde::{Deserialize, Serialize, Deserializer}; +use std::collections::HashMap; +use serde_json::value::Value; +use std::fmt::Display; +use super::*; + +#[derive(Debug, Serialize, Clone)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum EmptyOption { + Some(T), + None {}, +} + +impl Display for EmptyOption +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + EmptyOption::Some(t) => write!(f, "{}", t), + EmptyOption::None {} => write!(f, ""), + } + } +} + +impl<'de, T> Deserialize<'de> for EmptyOption +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Option::deserialize(deserializer).map(Into::into) + } +} + +impl From> for Option { + fn from(empty_option: EmptyOption) -> Option { + match empty_option { + EmptyOption::Some(option) => Some(option), + EmptyOption::None {} => None, + } + } +} + +impl From> for EmptyOption { + fn from(option: Option) -> EmptyOption { + match option { + Some(option) => EmptyOption::Some(option), + None {} => EmptyOption::None {}, + } + } +} + +impl EmptyOption { + fn into_option(self) -> Option { + self.into() + } + fn as_option(&self) -> Option<&T> { + match self { + EmptyOption::Some(option) => Some(option), + EmptyOption::None {} => None, + } + } +} + +{% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}Vec<{{property.sub_schema | caseUcfirst}}>{% else %}{{property.sub_schema | caseUcfirst}}{% endif %}{% else %}{{property.type | typeName}}{% endif %}{% endmacro %} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct {{ definition.name | caseUcfirst }} { +{% for property in definition.properties %} + {% if property.name | escapeKeyword | removeDollarSign != property.name %} + #[serde(rename(serialize = "{{ property.name | escapeKeyword | removeDollarSign }}", deserialize = "{{ property.name }}"))] + {% endif %} + pub {{ property.name | escapeKeyword | removeDollarSign }}: {% if not property.required %}EmptyOption<{% endif %}{% if property.type == 'string' %}String{% elseif property.type == "array" and property.items['x-anyOf'] and 'attribute' in property.items['x-anyOf'][0]['$ref'] %}Vec{% elseif _self.sub_schema(property) == '&[&str]' %}Vec{% else %}{{_self.sub_schema(property)}}{% endif %}{% if not property.required %}>{% endif %}, +{% endfor %} +{% if definition.additionalProperties %} + #[serde(default)] + pub data: HashMap, +{% endif %} +} + +impl Display for {{ definition.name | caseUcfirst }} { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut formatBuffer = String::new(); +{% for property in definition.properties %} +{% if property.type != 'array' %} + formatBuffer.push_str(&format!("{:?}", self.{{ property.name | escapeKeyword | removeDollarSign }})); +{% else %} + for item in &self.{{ property.name | escapeKeyword | removeDollarSign }} { + formatBuffer.push_str(&format!("{:?}", item)); + } +{% endif %} +{% endfor %} +{% if definition.additionalProperties %} + for (key, value) in &self.data { + formatBuffer.push_str(&format!("{}", value)); + } +{% endif %} + + write!(f, "{}", formatBuffer) + } +} + +impl {{ definition.name | caseUcfirst }} { + pub fn new({% for property in definition.properties %}{{ property.name | escapeKeyword | removeDollarSign }}: {% if not property.required %}EmptyOption<{% endif %}{% if property.type == 'string' %}String{% elseif property.type == "array" and property.items['x-anyOf'] and 'attribute' in property.items['x-anyOf'][0]['$ref'] %}Vec{% elseif _self.sub_schema(property) == '&[&str]' %}Vec{% else %}{{_self.sub_schema(property)}}{% endif %}{% if not property.required %}>{% endif %}, {%endfor%}) -> Self { + Self { + {% for property in definition.properties %} +{{ property.name | escapeKeyword | removeDollarSign }}: {% if not property.required %}EmptyOption::from({{ property.name | escapeKeyword | removeDollarSign }}){% else %}{{ property.name | escapeKeyword | removeDollarSign }}{% endif %}, + {% endfor %} +{% if definition.additionalProperties %} +data: HashMap::new(), +{% endif %} +} + } +} \ No newline at end of file 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..cf2c7a5d3 --- /dev/null +++ b/templates/rust/src/services/service.rs.twig @@ -0,0 +1,220 @@ +use crate::client::{Client, ParamType}; +use std::collections::HashMap; +use crate::services::{{spec.title | caseUcfirst}}Exception; +use crate::models; +use serde_json::json; +use std::io::Read; + +#[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<{% if method.type == 'location' %}Vec{% elseif method.responseModel and method.responseModel != 'any' %}models::{{method.responseModel | caseUcfirst}}{% else %}serde_json::value::Value{% endif %}, {{spec.title | caseUcfirst}}Exception> { + let path = "{{ method.path|replace({'{': '', '}': ""}) }}"{% for parameter in method.parameters.path %}.replace("{{ parameter.name | caseCamel }}", &{{ parameter.name | caseSnake }}){% endfor %}; +{% if method.headers %} + let {% if 'multipart/form-data' in method.consumes %}mut{% endif %} 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 {% if 'multipart/form-data' in method.consumes %}mut{% endif %} params: HashMap = [ +{% for parameter in method.parameters.query %} +{% if parameter.type != 'file' %} + ("{{ 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({% 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 %})), +{% endif %} +{% endfor %} +{% for parameter in method.parameters.body %} +{% if parameter.type != 'file' %} + ("{{ 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({% endif %}{% if parameter.type == 'object' and parameter.required != 1 %}match {% 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' and parameter.required != 1 %} { + Some(data) => data, + None => Some(HashMap::new()) + }{% elseif parameter.type == 'object' and parameter.required == 1 %}.unwrap(){% endif %})), +{% endif %} +{% endfor %} + ].iter().cloned().collect(); + +{% if 'multipart/form-data' in method.consumes %} +{% for parameter in method.parameters.all %} +{% if parameter.type == 'file' %} + let mut fileBuf = std::fs::File::open({{ parameter.name | caseSnake | escapeKeyword }}.clone()).unwrap(); + + let size = fileBuf.metadata().unwrap().len(); + + match size { + size if size <= crate::client::CHUNK_SIZE => { + params.insert("{{ parameter.name }}".to_string(), ParamType::FilePath({{ parameter.name | caseSnake | escapeKeyword }})); + match self.client.clone().call("POST", &path, Some(headers), Some(params)) { + Ok(r) => { + Ok(r.json::().unwrap()) + } + Err(e) => { + Err(e) + } + } + } + _ => { + // Stream Data. + let mut id = "".to_string(); + + let mut resumeCounter: u64 = 0; + let totalCounters = (((size / crate::client::CHUNK_SIZE) as f64).ceil() as u64) + 1; + +{% for parameter in method.parameters.all %} +{% if parameter.isUploadID %} + if file_id != "unique()" { + let filePath = format!("{{ method.path|replace({'{': '', '}': ""}) }}{}", file_id); + match self.client.clone().call("GET", &filePath, Some(headers.clone()), None) { + Ok(r) => { + match r.json::() { + Ok(data) => { + resumeCounter = data["chunksUploaded"].as_u64().unwrap(); + }, + Err(_e) => () + }; + } + Err(_e) => () + }; + } +{% endif %} +{% endfor %} + + let response: reqwest::blocking::Response; + + for counter in resumeCounter..totalCounters { + let mut headers: HashMap = [ + ("content-type".to_string(), "multipart/form-data".to_string()), + ].iter().cloned().collect(); + + let mut params = params.clone(); + + headers.insert("content-range".to_string(), format!("bytes {}-{}/{}", (counter * crate::client::CHUNK_SIZE), + std::cmp::min((counter * crate::client::CHUNK_SIZE) + crate::client::CHUNK_SIZE - 1, size), size)); + + if id.len() != 0 { + headers.insert("x-appwrite-id".to_string(), id.to_string()); + } + + let mut chunk = Vec::with_capacity(crate::client::CHUNK_SIZE as usize); + + match fileBuf.by_ref().take(crate::client::CHUNK_SIZE).read_to_end(&mut chunk) { + Ok(_) => (), + Err(e) => { + return Err(AppwriteException::new(format!("A error occoured. ERR: {}, This could either be a connection error or an internal Appwrite error. Please check your Appwrite instance logs. ", e), 0, "".to_string())) + } + }; + + params.insert("file".to_string(), ParamType::StreamData(chunk, {{ parameter.name | caseSnake | escapeKeyword }}.file_name().unwrap().to_string_lossy().to_string())); + + let response = match self.client.clone().call("POST", &path, Some(headers), Some(params)) { + Ok(r) => r, + Err(e) => { + return Err(e); + } + }; + + // If last chunk, return the response. + if counter == totalCounters - 1 { + return Ok(response.json::().unwrap()); + } else { + if id.len() == 0 { + id = response.json::().unwrap()["$id"].as_str().unwrap().to_owned(); + } + } + }; + + return Err(AppwriteException::new("Error uploading chunk data.".to_string(), 500, "0".to_string())); + } + } +{% endif %} +{% endfor %} +{% else %} + let response = self.client.clone().call("{{ method.method | caseUpper }}", &path, {% if method.headers %}Some(headers){% else %}None{% endif %}, Some(params) ); + +{% if method.type == 'location' %} + let processedResponse:Vec = match response { + Ok(mut r) => { + let mut buf: Vec = vec![]; + match r.copy_to(&mut buf) { + Ok(_) => (), + Err(e) => { + return Err({{spec.title | caseUcfirst}}Exception::new(format!("Error copying response to buffer: {}", e), 0, "".to_string())); + } + }; + buf + } + Err(e) => { + return Err(e); + } + }; + + Ok(processedResponse) +{% elseif method.responseModel and method.responseModel != 'any' %} + let processedResponse:models::{{method.responseModel | caseUcfirst}} = match response { + Ok(r) => { + match r.json() { + Ok(json) => json, + Err(e) => { + return Err({{spec.title | caseUcfirst}}Exception::new(format!("Error parsing response json: {}", e), 0, "".to_string())); + } + } + } + Err(e) => { + return Err(e); + } + }; + + Ok(processedResponse) +{% else %} + match response { + Ok(r) => { + let status_code = r.status(); + if status_code == reqwest::StatusCode::NO_CONTENT { + Ok(json!(true)) + } else { + Ok(serde_json::from_str(&r.text().unwrap()).unwrap()) + } + } + Err(e) => { + Err(e) + } + } +{% endif %} +{% endif %} + } +{% endfor %} +} diff --git a/tests/RustBetaTest.php b/tests/RustBetaTest.php new file mode 100644 index 000000000..ad665bdc2 --- /dev/null +++ b/tests/RustBetaTest.php @@ -0,0 +1,21 @@ + println!("{}", data), + Err(err) => println!("{}", err.message) + } + + match general.error500() { + Ok(data) => println!("{}", data), + Err(err) => println!("{}", err.message) + } + + match general.error502() { + Ok(data) => println!("{}", data), + Err(err) => println!("{}", err.message) + } +} \ No newline at end of file