diff --git a/.travis.yml b/.travis.yml index 60f3afc03..aa442208d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ env: - SDK=DartBeta - SDK=DartStable - SDK=Deno1171 + - SDK=Elixir114 - SDK=FlutterStable - SDK=Go112 - SDK=Go118 diff --git a/example.php b/example.php index 4b3fbc909..b15a8133a 100644 --- a/example.php +++ b/example.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Language\CLI; use Appwrite\SDK\Language\PHP; use Appwrite\SDK\Language\Python; +use Appwrite\SDK\Language\Elixir; use Appwrite\SDK\Language\Ruby; use Appwrite\SDK\Language\Dart; use Appwrite\SDK\Language\Go; @@ -235,6 +236,29 @@ function getSSLPage($url) { $sdk->generate(__DIR__ . '/examples/python'); + // Elixir + $sdk = new SDK(new Elixir(), 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**') + ->setGitUserName('repoowner') + ->setGitRepoName('reponame') + ->setTwitter('appwrite_io') + ->setDiscord('564160730845151244', 'https://appwrite.io/discord') + ->setDefaultHeaders([ + 'X-Appwrite-Response-Format' => '0.7.0', + ]) + ; + + $sdk->generate(__DIR__ . '/examples/elixir'); + // Dart $dart = new Dart(); $dart->setPackageName('dart_appwrite'); diff --git a/src/SDK/Language/Elixir.php b/src/SDK/Language/Elixir.php new file mode 100644 index 000000000..441bf576b --- /dev/null +++ b/src/SDK/Language/Elixir.php @@ -0,0 +1,314 @@ + 'packageName', + ]; + + /** + * @param string $name + * @return $this + */ + public function setPipPackage($name): self + { + $this->setParam('pipPackage', $name); + + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return 'Elixir'; + } + + /** + * Get Language Keywords List + * + * @return array + */ + public function getKeywords(): array + { + return [ + 'false', + 'is', + 'nil', + 'for', + 'try', + 'true', + 'def', + 'alias', + 'and', + 'del', + 'not', + 'with', + 'as', + 'case', + 'if', + 'or', + 'assert', + 'else', + 'import', + 'when', + 'in', + 'throw', + ]; + } + + /** + * @return array + */ + public function getIdentifierOverrides(): array + { + return []; + } + + /** + * @return array + */ + public function getFiles(): array + { + return [ + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'elixir/README.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'elixir/CHANGELOG.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'elixir/LICENSE.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'mix.exs', + 'template' => 'elixir/mix.exs.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '.formatter.exs', + 'template' => 'elixir/.formatter.exs.twig', + 'minify' => false, + ], + /*[ + 'scope' => 'service', + 'destination' => 'docs/{{service.name | caseSnake}}.md', + 'template' => 'elixir/docs/service.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'service', + 'destination' => 'docs/{{service.name | caseSnake}}.md', + 'template' => 'elixir/docs/service.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', + 'template' => 'elixir/docs/example.md.twig', + 'minify' => false, + ],*/ + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/client.ex', + 'template' => 'elixir/lib/appwrite/client.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/permission.ex', + 'template' => 'elixir/lib/appwrite/permission.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/role.ex', + 'template' => 'elixir/lib/appwrite/role.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/id.ex', + 'template' => 'elixir/lib/appwrite/id.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/query.ex', + 'template' => 'elixir/lib/appwrite/query.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/exception.ex', + 'template' => 'elixir/lib/appwrite/exception.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseSnake}}/input_file.ex', + 'template' => 'elixir/lib/appwrite/input_file.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'service', + 'destination' => 'lib/{{ spec.title | caseSnake}}/services/{{service.name | caseSnake}}.ex', + 'template' => 'elixir/lib/appwrite/services/service.ex.twig', + 'minify' => false, + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => 'elixir/docs/example.md.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '.travis.yml', + 'template' => 'elixir/.travis.yml.twig', + 'minify' => false, + ], + ]; + } + + /** + * @param array $parameter + * @return string + * @throws Exception + */ + public function getTypeName(array $parameter): string + { + throw new Exception('Method not supported for Elixir SDKs'); + } + + /** + * @param array $param + * @return string + */ + public function getParamDefault(array $param): string + { + $type = $param['type'] ?? ''; + $default = $param['default'] ?? ''; + $required = $param['required'] ?? ''; + + if($required) { + return ''; + } + + $output = '='; + + if(empty($default) && $default !== 0 && $default !== false) { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_BOOLEAN: + $output .= 'None'; + break; + case self::TYPE_STRING: + $output .= "''"; + break; + case self::TYPE_ARRAY: + $output .= '[]'; + break; + case self::TYPE_OBJECT: + case self::TYPE_FILE: + $output .= '{}'; + break; + } + } + else { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= $default; + break; + case self::TYPE_FILE: + #$output .= json_encode($default); + $output .= '{}'; + break; + case self::TYPE_BOOLEAN: + $output .= ($default) ? 'True' : 'False'; + break; + case self::TYPE_STRING: + $output .= "'$default'"; + break; + } + } + + return $output; + } + + /** + * @param array $param + * @return string + */ + public function getParamExample(array $param): string + { + $type = $param['type'] ?? ''; + $example = $param['example'] ?? ''; + + $output = ''; + + if(empty($example) && $example !== 0 && $example !== false) { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_BOOLEAN: + $output .= 'None'; + break; + case self::TYPE_STRING: + $output .= "''"; + break; + case self::TYPE_ARRAY: + $output .= '[]'; + break; + case self::TYPE_OBJECT: + $output .= '{}'; + break; + case self::TYPE_FILE: + $output .= "InputFile.from_path('file.png')"; + break; + } + } + else { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= $example; + break; + case self::TYPE_BOOLEAN: + $output .= ($example) ? 'True' : 'False'; + break; + case self::TYPE_STRING: + $output .= "'{$example}'"; + break; + case self::TYPE_FILE: + $output .= "InputFile.from_path('file.png')"; + break; + } + } + + return $output; + } +} diff --git a/templates/elixir/.formatter.exs.twig b/templates/elixir/.formatter.exs.twig new file mode 100644 index 000000000..d2cda26ed --- /dev/null +++ b/templates/elixir/.formatter.exs.twig @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/templates/elixir/.travis.yml.twig b/templates/elixir/.travis.yml.twig new file mode 100644 index 000000000..712f791bb --- /dev/null +++ b/templates/elixir/.travis.yml.twig @@ -0,0 +1,16 @@ +language: python + +python: + - "3.8" + +jobs: + include: + - stage: pypi release + python: "3.8" + script: echo "Deploying to pypi ..." + deploy: + provider: pypi + username: "__token__" + password: $PYPI_TOKEN + on: + tags: true \ No newline at end of file diff --git a/templates/elixir/CHANGELOG.md.twig b/templates/elixir/CHANGELOG.md.twig new file mode 100644 index 000000000..a544d26c9 --- /dev/null +++ b/templates/elixir/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{sdk.changelog}} \ No newline at end of file diff --git a/templates/elixir/LICENSE.twig b/templates/elixir/LICENSE.twig new file mode 100644 index 000000000..854eb1949 --- /dev/null +++ b/templates/elixir/LICENSE.twig @@ -0,0 +1 @@ +{{sdk.licenseContent | raw}} \ No newline at end of file diff --git a/templates/elixir/README.md.twig b/templates/elixir/README.md.twig new file mode 100644 index 000000000..43a44a194 --- /dev/null +++ b/templates/elixir/README.md.twig @@ -0,0 +1,47 @@ +# {{ spec.title }} {{sdk.name}} SDK + +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) +{% if sdk.twitterHandle %} +[![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) +{% endif %} +{% if sdk.discordChannel %} +[![Discord](https://img.shields.io/discord/{{ sdk.discordChannel }}?label=discord&style=flat-square)]({{ sdk.discordUrl }}) +{% endif %} +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} + +{{ sdk.description }} + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `appwrite` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:{{ language.params.pipPackage }}, "~> {{ sdk.version }}"} + ] +end +``` + +{% 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. diff --git a/templates/elixir/base/params.twig b/templates/elixir/base/params.twig new file mode 100644 index 000000000..3473163d2 --- /dev/null +++ b/templates/elixir/base/params.twig @@ -0,0 +1,40 @@ +{% if method.parameters.all | length %} +{% for parameter in method.parameters.all %} +{% if parameter.required %} + if is_nil({{ parameter.name | escapeKeyword | caseSnake }})do + throw(Error.exception("Missing required parameter: \"{{ parameter.name | escapeKeyword | caseSnake }}\"")) + end + +{% else %} + {{ parameter.name | escapeKeyword | caseSnake }} = Keyword.get(opts, :{{ parameter.name | escapeKeyword | caseSnake }}) +{% endif %} +{% endfor %} +{% if 'multipart/form-data' in method.consumes %} + on_progress = Keyword.get(opts, :on_progress) +{% endif %} +{% for parameter in method.parameters.path %} + path = String.replace(path, "{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}", {{ parameter.name | escapeKeyword | caseSnake }}) +{% endfor %} + + params = %{ +{% for parameter in method.parameters.query %} + "{{ parameter.name }}" => {{ parameter.name | escapeKeyword | caseSnake }}, +{% endfor %} +{% for parameter in method.parameters.body %} +{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} + "{{ parameter.name }}" => if {{ parameter.name | escapeKeyword | caseSnake }} in [true, false] do "#{{"{"}}{{ parameter.name | escapeKeyword | caseSnake }}{{"}"}}" else {{ parameter.name | escapeKeyword | caseSnake }} end, +{% else %} + "{{ parameter.name }}" => {{ parameter.name | escapeKeyword | caseSnake }}, +{% endif %} +{% endfor %} +{% for parameter in method.parameters.formData %} +{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} + "{{ parameter.name }}" => if {{ parameter.name | escapeKeyword | caseSnake }} in [true, false] do "#{{"{"}}{{ parameter.name | escapeKeyword | caseSnake }}{{"}"}}" else {{ parameter.name | escapeKeyword | caseSnake }} end, +{% else %} + "{{ parameter.name }}" => {{ parameter.name | escapeKeyword | caseSnake }}, +{% endif %} +{% endfor %} + } +{% else %} + params = %{} +{% endif %} \ No newline at end of file diff --git a/templates/elixir/base/requests/api.twig b/templates/elixir/base/requests/api.twig new file mode 100644 index 000000000..6afcc63df --- /dev/null +++ b/templates/elixir/base/requests/api.twig @@ -0,0 +1,14 @@ + Client.call( + client, + :{{ method.method | caseLower }}, + path, + %{ +{% for parameter in method.parameters.header %} + "{{ parameter.name }}" => {{ parameter.name | escapeKeyword | caseSnake }}, +{% endfor %} +{% for key, header in method.headers %} + "{{ key }}" => "{{ header }}", +{% endfor %} + }, + params + ) \ No newline at end of file diff --git a/templates/elixir/base/requests/file.twig b/templates/elixir/base/requests/file.twig new file mode 100644 index 000000000..3d4703ead --- /dev/null +++ b/templates/elixir/base/requests/file.twig @@ -0,0 +1,21 @@ +{% for parameter in method.parameters.all %} +{% if parameter.type == 'file' %} + param_name = "{{ parameter.name }}" + +{% endif %} +{% endfor %} + upload_id = "" +{% for parameter in method.parameters.all %} +{% if parameter.isUploadID %} + upload_id = {{ parameter.name | escapeKeyword | caseSnake }} +{% endif %} +{% endfor %} + + Client.chunked_upload(client, path, %{ +{% for parameter in method.parameters.header %} + "{{ parameter.name }}" => {{ parameter.name | escapeKeyword | caseSnake }}, +{% endfor %} +{% for key, header in method.headers %} + "{{ key }}" => "{{ header }}", +{% endfor %} + }, params, param_name, on_progress, upload_id) \ No newline at end of file diff --git a/templates/elixir/docs/example.md.twig b/templates/elixir/docs/example.md.twig new file mode 100644 index 000000000..f7a57f592 --- /dev/null +++ b/templates/elixir/docs/example.md.twig @@ -0,0 +1,15 @@ +alias {{ spec.title | caseUcfirst }}.Client +{% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 %} +alias {{ spec.title | caseUcfirst }}.InputFile +{% endif %} +alias {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }} + +client = Client.get_client('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint + +{% for node in method.security %} +{% for key,header in node|keys %} + Client.set_{{header | caseSnake}}(clinet, "{{node[header]['x-appwrite']['demo']}}") # {{node[header].description}} +{% endfor %} +{% endfor %}) + +result = {{ service.name | caseUcfirst }}.{{ method.name | caseSnake }}(client{% for parameter in method.parameters.all %}, {% if parameter.required %}{{ parameter | paramExample }}{% endif %}{% endfor %}) diff --git a/templates/elixir/lib/appwrite/client.ex.twig b/templates/elixir/lib/appwrite/client.ex.twig new file mode 100644 index 000000000..bf36c837c --- /dev/null +++ b/templates/elixir/lib/appwrite/client.ex.twig @@ -0,0 +1,322 @@ +defmodule Appwrite.Client do + @moduledoc false + + alias {{spec.title | caseUcfirst}}.Error + alias {{spec.title | caseUcfirst}}.InputFile + alias Tesla.Multipart + + defstruct endpoint: "{{spec.endpoint}}", + chunk_size: 5 * 1024 * 1024, + http_client: nil, + global_headers: %{ + "x-sdk-name" => "{{sdk.name}}", + "x-sdk-platform" => "{{sdk.platform}}", + "x-sdk-language" => "{{language.name | caseLower}}", + "x-sdk-version" => "{{sdk.version}}", +{% for key,header in spec.global.defaultHeaders %} + "{{key}}" => "{{header}}", +{% endfor %} + }, + self_signed?: false + + def get_client(endpoint \\ "{{spec.endpoint}}", opts \\ []) do + self_signed? = + if is_nil(opts[:self_signed?]) do + false + else + opts[:self_signed?] + end + + %__MODULE__{endpoint: endpoint, self_signed?: self_signed?} + |> setup_http_client + end + + def update_params(%__MODULE__{} = client, opts) do + global_headers = Map.merge(client.global_headers, opts[:global_headers] or %{}) + + self_signed? = + if is_nil(opts[:self_signed?]) do + client.self_signed? + else + opts[:self_signed?] + end + + %__MODULE__{ + chunk_size: opts[:chunk_size] or client.chunk_size, + endpoint: opts[:endpoint] or client.endpoint, + global_headers: global_headers, + self_signed?: self_signed? + } + end + + def add_headers(%__MODULE__{global_headers: headers} = client, %{} = new_headers) do + new_headers = Map.merge(headers, new_headers) + %{client | global_headers: new_headers} + end + + def add_header(%__MODULE__{} = client, header, value) do + add_headers(client, %{header => value}) + end + + def remove_header(%__MODULE__{global_headers: headers} = client, header_name) do + {_, new_headers} = Map.pop(headers, header_name) + %{client | global_headers: new_headers} + end + + def set_self_signed(client, status \\ true) do + %{client | self_signed?: status} + end + + def set_endpoint(client, endpoint) do + %{client | endpoint: endpoint} + |> setup_http_client + end +{% for header in spec.global.headers %} + +{% if header.description %} + @doc "{{header.description}}" +{% endif %} + def set_{{header.key | caseSnake}}(client, value) do + %{client | "{{header.name|lower}}": value} + end +{% endfor %} + + defp setup_http_client(%__MODULE__{} = client) do + middleware = [ + {Tesla.Middleware.BaseUrl, client.endpoint}, + Tesla.Middleware.JSON, + Tesla.Middleware.FollowRedirects + ] + + r = Tesla.client(middleware) + + %{client | http_client: r} + end + + defp handle_response(response) do + if response.status >= 400 do + case response do + %{body: %{"message" => message}} -> + {:error, {:{{spec.title | caseSnake}}_error, message, response.status, response}} + %{body: body} -> + {:error, {:{{spec.title | caseSnake}}_error, body, response.status, response}} + end + else + {:ok, response} + end + end + + def call(client, method, path, request_headers, params \\ %{}) do + {params, body} = + if method != :get do + {% verbatim %}{%{}, params}{% endverbatim %} + else + {params, nil} + end + + headers = + Map.merge(client.global_headers, request_headers) + |> Map.to_list() + + case Tesla.request(client.http_client, + method: method, + url: path, + headers: headers, + body: body, + query: params + ) do + {:ok, response} -> handle_response(response) + {:error, error} -> {:error, error} + end + end + + defp chunks(binary, n) do + do_chunks(binary, n, []) + end + + defp do_chunks(binary, n, acc) when byte_size(binary) <= n do + Enum.reverse([binary | acc]) + end + + defp do_chunks(binary, n, acc) do + <> = binary + do_chunks(rest, n, [<> | acc]) + end + + def chunked_upload(client, path, _headers, params, param_name, on_progress, upload_id) do + {input_file, params} = Map.pop!(params, param_name) + + {size, stream, file_name} = + case input_file do + %InputFile{file_content: content, file_name: file_name, file_path: nil} -> + size = byte_size(content) + stream = chunks(content, client.chunk_size) + {size, stream, file_name} + + %InputFile{file_content: nil, file_name: file_name, file_path: file_path} -> + file_name = + if is_nil(file_name) do + Path.basename(file_path) + else + file_name + end + + size = File.stat!(file_path).size + stream = File.stream!(file_path, [], client.chunk_size) + {size, stream, file_name} + + _ -> + throw( + Error.exception( + "Please provide an %Appwrite.InputFile{} struct with either file_content or file_path defined.\n" <> + "example: %InputFile{file_content: \"content\", file_path: nil}" + ) + ) + end + + offset = get_offset(client, path, upload_id) + + result = + stream + |> Stream.drop(offset) + |> Enum.reduce_while( + %{ + client: client, + path: path, + params: params, + on_progress: on_progress, + upload_id: upload_id, + size: size, + file_name: file_name, + offset: offset, + headers: %{}, + response: nil + }, + fn x, acc -> upload_part(x, acc) end + ) + + case result do + %{response: response} -> + {:ok, response} + {:error, error} -> + {:error, error} + end + end + + defp get_offset(_client, _path, "unique()"), do: 0 + + defp get_offset(client, path, upload_id) do + case call(client, :get, "#{path}/#{upload_id}", %{"content-type" => "text/json"}) do + {:ok, %{body: response}} -> + case response["chunksUploaded"] do + nil -> 0 + offset -> offset + end + {:error, _} -> 0 + end + end + + defp upload_part(chunk, params) do + client = params.client + + mp = + Multipart.new() + |> Multipart.add_content_type_param("charset=utf-8") + |> Multipart.add_file_content(chunk, params.file_name) + + mp = + params.params + |> flatten + |> Enum.reduce(mp, fn {key, value}, acc -> Multipart.add_field(acc, key, "#{value}") end) + + request_headers = + client.global_headers + |> Map.merge(params.headers) + |> add_chunk_header(params.offset, client.chunk_size, params.size) + |> Map.to_list() + + case Tesla.request(client.http_client, + method: :post, + url: params.path, + headers: request_headers, + body: mp + ) do + {:ok, response} -> + case handle_response(response) do + {:ok, response} -> + notify_progress(response, params) + headers = set_appwrite_id_header(params.headers, response.body["$id"]) + + {:cont, %{params | offset: params.offset + 1, headers: headers, response: response}} + {:error, error} -> + {:halt, {:error, error}} + end + {:error, error} -> + {:halt, {:error, error}} + end + end + + defp add_chunk_header(headers, _offset, chunk_size, file_size) when chunk_size > file_size, + do: headers + + defp add_chunk_header(headers, offset, chunk_size, file_size) do + sent_size = get_size(offset, chunk_size, file_size) + end_range = Enum.min([sent_size + chunk_size - 1, file_size]) + + Map.put(headers, "content-range", "bytes #{sent_size}-#{end_range}/#{file_size}") + end + + defp notify_progress(_response, %{on_progress: nil}), do: :ok + + defp notify_progress(response, params) do + chunk_size = params.client.chunk_size + new_offset = params.offset + 1 + size_uploaded = Enum.min([new_offset * chunk_size + chunk_size, params.size]) + + params.on_progress(%{ + id: response["$id"], + progress: get_size(new_offset, chunk_size, params.size), + size_uploaded: size_uploaded, + chunks_total: response["chunksTotal"], + chunks_uploaded: response["chunksUploaded"] + }) + end + + defp set_appwrite_id_header(headers, nil), do: headers + defp set_appwrite_id_header(headers, id), do: Map.put(headers, "x-appwrite-id", id) + + defp get_size(offset, chunk_size, size) do + Enum.min([ + offset * chunk_size, + size + ]) + end + + defp flatten(data, options \\ []) + + defp flatten(data, options) when is_map(data) do + data + |> Enum.flat_map(fn x -> flatten(x, options) end) + end + + defp flatten(data, options) when is_list(data) do + data + |> Enum.with_index + |> Enum.flat_map(fn {x, i} -> flatten({i, x}, options) end) + end + + defp flatten({key, value}, options) when is_list(value) or is_map(value) do + flatten(value, Keyword.get_and_update(options, :prefix, fn + nil -> {nil, "#{key}"} + prefix -> {prefix, "#{prefix}[#{key}]"} + end) |> elem(1)) + end + + defp flatten({key, value}, options) do + if prefix = Keyword.get(options, :prefix) do + [{"#{prefix}[#{key}]", value}] + else + [{"#{key}", value}] + end + end +end diff --git a/templates/elixir/lib/appwrite/exception.ex.twig b/templates/elixir/lib/appwrite/exception.ex.twig new file mode 100644 index 000000000..5cca7b53a --- /dev/null +++ b/templates/elixir/lib/appwrite/exception.ex.twig @@ -0,0 +1,18 @@ +defmodule {{spec.title | caseUcfirst}}.Error do + @moduledoc """ + An Appwrite error. + """ + + @type t() :: %__MODULE__{message: binary, code: integer, response: any} + defexception [:message, :code, :response] + + @impl true + def exception(message, opts \\ []) do + %__MODULE__{message: message, code: opts[:code], response: opts[:response]} + end + + @impl true + def message(%__MODULE__{message: message, code: code, response: response}) do + "#{message}, code: #{code}, response: #{response}" + end +end diff --git a/templates/elixir/lib/appwrite/id.ex.twig b/templates/elixir/lib/appwrite/id.ex.twig new file mode 100644 index 000000000..025f66695 --- /dev/null +++ b/templates/elixir/lib/appwrite/id.ex.twig @@ -0,0 +1,6 @@ +defmodule Appwrite.Id do + @moduledoc false + + def custom(id), do: id + def unique(), do: "unique()" +end diff --git a/templates/elixir/lib/appwrite/input_file.ex.twig b/templates/elixir/lib/appwrite/input_file.ex.twig new file mode 100644 index 000000000..8b6c46146 --- /dev/null +++ b/templates/elixir/lib/appwrite/input_file.ex.twig @@ -0,0 +1,13 @@ +defmodule Appwrite.InputFile do + @moduledoc false + + defstruct [:file_path, :file_content, :file_name] + + def from_binary(file_content, file_name) do + %__MODULE__{file_content: file_content, file_name: file_name} + end + + def from_path(file_path, file_name \\ nil) do + %__MODULE__{file_path: file_path, file_name: file_name} + end +end diff --git a/templates/elixir/lib/appwrite/permission.ex.twig b/templates/elixir/lib/appwrite/permission.ex.twig new file mode 100644 index 000000000..6368ba9b9 --- /dev/null +++ b/templates/elixir/lib/appwrite/permission.ex.twig @@ -0,0 +1,9 @@ +defmodule Appwrite.Permission do + @moduledoc false + + def read(role), do: "read(\"#{role}\")" + def write(role), do: "write(\"#{role}\")" + def create(role), do: "create(\"#{role}\")" + def update(role), do: "update(\"#{role}\")" + def delete(role), do: "delete(\"#{role}\")" +end diff --git a/templates/elixir/lib/appwrite/query.ex.twig b/templates/elixir/lib/appwrite/query.ex.twig new file mode 100644 index 000000000..8e4342e12 --- /dev/null +++ b/templates/elixir/lib/appwrite/query.ex.twig @@ -0,0 +1,34 @@ +defmodule Appwrite.Query do + @moduledoc false + + def equal(attribute, value), do: add_query(attribute, "equal", value) + def not_equal(attribute, value), do: add_query(attribute, "notEqual", value) + def less_than(attribute, value), do: add_query(attribute, "lessThan", value) + def less_than_equal(attribute, value), do: add_query(attribute, "lessThanEqual", value) + def greater_than(attribute, value), do: add_query(attribute, "greaterThan", value) + def greater_than_equal(attribute, value), do: add_query(attribute, "greaterThanEqual", value) + def search(attribute, value), do: add_query(attribute, "search", value) + + def order_asc(attribute), do: "orderAsc(\"#{attribute}\")" + def order_desc(attribute), do: "orderDesc(\"#{attribute}\")" + def cursor_before(id), do: "cursorBefore(\"#{id}\")" + def cursor_after(id), do: "cursorAfter(\"#{id}\")" + def limit(limit), do: "limit(#{limit})" + def offset(offset), do: "offset(#{offset})" + + def add_query(attribute, method, values) when is_list(values) do + params = + values + |> Enum.map(&parse_value(&1)) + |> Enum.join(",") + + "#{method}(\"#{attribute}\", [#{params}])" + end + + def add_query(attribute, method, value) do + "#{method}(\"#{attribute}\", [#{parse_value(value)}])" + end + + def parse_value(value) when is_binary(value), do: "\"#{value}\"" + def parse_value(value), do: "#{value}" +end diff --git a/templates/elixir/lib/appwrite/role.ex.twig b/templates/elixir/lib/appwrite/role.ex.twig new file mode 100644 index 000000000..3f4f477aa --- /dev/null +++ b/templates/elixir/lib/appwrite/role.ex.twig @@ -0,0 +1,33 @@ +defmodule Appwrite.Role do + @moduledoc false + + def any(), do: "any" + + def user(id, status \\ nil) do + if status do + "user:#{id}/#{status}" + else + "user:#{id}" + end + end + + def users(status \\ nil) do + if status do + "users/#{status}" + else + "users" + end + end + + def guests(), do: "guests" + + def team(id, role \\ nil) do + if role do + "team:#{id}/#{role}" + else + "team:#{id}" + end + end + + def member(id), do: "member:#{id}" +end diff --git a/templates/elixir/lib/appwrite/services/service.ex.twig b/templates/elixir/lib/appwrite/services/service.ex.twig new file mode 100644 index 000000000..18954594d --- /dev/null +++ b/templates/elixir/lib/appwrite/services/service.ex.twig @@ -0,0 +1,21 @@ +defmodule {{spec.title | caseUcfirst}}.Services.{{ service.name | caseUcfirst }} do + @moduledoc false + + alias {{spec.title | caseUcfirst}}.Error + alias {{spec.title | caseUcfirst}}.Client +{% for method in service.methods %} + +{% if method.title %} + @doc "{{ method.title }}"{% endif %} + def {{ method.name | caseSnake }}(client{% if method.parameters.all|filter(p => p.required) |length > 0 %}, {% endif %}{% for parameter in method.parameters.all|filter(p => p.required) %}{{ parameter.name | escapeKeyword | caseSnake }}{% if not loop.last %}, {% endif %}{% endfor %}{% if (method.parameters.all|filter(p => not p.required) |length > 0) or 'multipart/form-data' in method.consumes %}, opts \\ []{% endif %}) do + path = "{{ method.path }}" + +{{ include('elixir/base/params.twig') }} +{% if 'multipart/form-data' in method.consumes %} +{{ include('elixir/base/requests/file.twig') }} +{% else %} +{{ include('elixir/base/requests/api.twig') }} +{% endif %} + end +{% endfor %} +end \ No newline at end of file diff --git a/templates/elixir/mix.exs.twig b/templates/elixir/mix.exs.twig new file mode 100644 index 000000000..1e8b29fe0 --- /dev/null +++ b/templates/elixir/mix.exs.twig @@ -0,0 +1,29 @@ +defmodule Appwrite.MixProject do + use Mix.Project + + def project do + [ + app: :{{spec.title | caseSnake}}, + version: "{{ sdk.version }}", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:tesla, "~> 1.4.0"}, + {:finch, "~> 0.3"}, + {:jason, ">= 1.0.0"} + ] + end +end diff --git a/tests/Base.php b/tests/Base.php index c7eef9924..567a094a2 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -191,7 +191,7 @@ public function testHTTPSuccess(): void $this->assertGreaterThanOrEqual(count($this->expectedOutput), count($output)); foreach ($this->expectedOutput as $i => $row) { - $this->assertEquals($output[$i], $row); + $this->assertEquals($row, $output[$i]); } } @@ -204,7 +204,7 @@ private function rmdirRecursive($dir) if ('.' === $file || '..' === $file) { continue; } - if (\is_dir("$dir/$file")) { + if (\is_dir("$dir/$file") && !\is_link("$dir/$file")) { $this->rmdirRecursive("$dir/$file"); } else { \unlink("$dir/$file"); diff --git a/tests/Elixir114Test.php b/tests/Elixir114Test.php new file mode 100644 index 000000000..b84137094 --- /dev/null +++ b/tests/Elixir114Test.php @@ -0,0 +1,31 @@ + :ok + {:ok, %{body: %{"result" => result}}} -> IO.puts(result) + {:ok, %{body: body}} -> IO.puts(body) + {:error, {:appwrite_error, message, _, _}} -> IO.puts(message) +end + +alias Appwrite.Client +alias Appwrite.Services.Foo +alias Appwrite.Services.Bar +alias Appwrite.Services.General +alias Appwrite.InputFile +alias Appwrite.Query +alias Appwrite.Permission +alias Appwrite.Role +alias Appwrite.Id + +Logger.configure(level: :error) + +client = Client.get_client +|> Client.add_header("Origin", "http://localhost") +|> Client.set_self_signed(true) + +Foo.get(client, "string", 123, ["string in array"]) |> print_result.() +Foo.post(client, "string", 123, ["string in array"]) |> print_result.() +Foo.put(client, "string", 123, ["string in array"]) |> print_result.() +Foo.patch(client, "string", 123, ["string in array"]) |> print_result.() +Foo.delete(client, "string", 123, ["string in array"]) |> print_result.() + +Bar.get(client, "string", 123, ["string in array"]) |> print_result.() +Bar.post(client, "string", 123, ["string in array"]) |> print_result.() +Bar.put(client, "string", 123, ["string in array"]) |> print_result.() +Bar.patch(client, "string", 123, ["string in array"]) |> print_result.() +Bar.delete(client, "string", 123, ["string in array"]) |> print_result.() + +General.redirect(client) |> print_result.() + +General.upload(client, "string", 123, ["string in array"], InputFile.from_path("../../resources/file.png")) +|> print_result.() + +General.upload(client, "string", 123, ["string in array"], InputFile.from_path("../../resources/large_file.mp4")) +|> print_result.() + +General.upload(client, "string", 123, ["string in array"], File.read!("../../resources/file.png") |> InputFile.from_binary("file.png")) +|> print_result.() + +General.upload(client, "string", 123, ["string in array"], File.read!("../../resources/large_file.mp4") |> InputFile.from_binary("/large_file.mp4")) +|> print_result.() + +General.download(client) |> print_result.() +General.empty(client) |> print_result.() + +({:error, {:appwrite_error, _, _, _}} = General.error400(client)) |> print_result.() +({:error, {:appwrite_error, _, _, _}} = General.error500(client)) |> print_result.() +({:error, {:appwrite_error, _, _, _}} = General.error502(client)) |> print_result.() + +Query.equal("released", true) |> IO.puts +Query.equal("title", ["Spiderman", "Dr. Strange"]) |> IO.puts +Query.not_equal("title", "Spiderman") |> IO.puts +Query.less_than("releasedYear", 1990) |> IO.puts +Query.greater_than("releasedYear", 1990) |> IO.puts +Query.search("name", "john") |> IO.puts +Query.order_asc("title") |> IO.puts +Query.order_desc("title") |> IO.puts +Query.cursor_after("my_movie_id") |> IO.puts +Query.cursor_before("my_movie_id") |> IO.puts +Query.limit(50) |> IO.puts +Query.offset(20) |> IO.puts + +Role.any |> Permission.read |> IO.puts +Role.user(Id.custom("userid")) |> Permission.write |> IO.puts +Role.users |> Permission.create |> IO.puts +Role.guests |> Permission.update |> IO.puts +Role.team("teamId", "owner") |> Permission.delete |> IO.puts +Role.team("teamId") |> Permission.delete |> IO.puts +Role.member("memberId") |> Permission.create |> IO.puts +Role.users("verified") |> Permission.update |> IO.puts +Role.user(Id.custom("userid"), "unverified") |> Permission.update |> IO.puts + +Id.unique |> IO.puts +Id.custom("custom_id") |> IO.puts + +General.headers(client) |> print_result.()