Skip to content

feat: generics support for dart #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions example.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ function getSSLPage($url) {
return $result;
}

// Leave the platform you want uncommented
// $platform = 'client';
$platform = 'console';
// $platform = 'server';
$consoleSpec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.7.x/app/config/specs/swagger2-latest-console.json");
$clientSpec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.7.x/app/config/specs/swagger2-latest-client.json");
$serverSpec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.7.x/app/config/specs/swagger2-latest-server.json");

$spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.7.x/app/config/specs/swagger2-latest-{$platform}.json");

if(empty($spec)) {
if (empty($consoleSpec) || empty($clientSpec) || empty($serverSpec)) {
throw new Exception('Failed to fetch spec from Appwrite server');
}

Expand All @@ -53,7 +50,7 @@ function getSSLPage($url) {
$php
->setComposerVendor('appwrite')
->setComposerPackage('appwrite');
$sdk = new SDK($php, new Swagger2($spec));
$sdk = new SDK($php, new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -76,7 +73,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/php');

// // Web
$sdk = new SDK(new Web(), new Swagger2($spec));
$sdk = new SDK(new Web(), new Swagger2($clientSpec));

$sdk
->setName('NAME')
Expand All @@ -101,7 +98,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/web');

// Deno
$sdk = new SDK(new Deno(), new Swagger2($spec));
$sdk = new SDK(new Deno(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -125,7 +122,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/deno');

// Node
$sdk = new SDK(new Node(), new Swagger2($spec));
$sdk = new SDK(new Node(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand Down Expand Up @@ -168,7 +165,7 @@ function getSSLPage($url) {
\_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/
|_| |_| ");

$sdk = new SDK($language, new Swagger2($spec));
$sdk = new SDK($language, new Swagger2($consoleSpec));

$sdk
->setName('NAME')
Expand Down Expand Up @@ -198,7 +195,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/cli');

// Ruby
$sdk = new SDK(new Ruby(), new Swagger2($spec));
$sdk = new SDK(new Ruby(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -221,7 +218,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/ruby');

// Python
$sdk = new SDK(new Python(), new Swagger2($spec));
$sdk = new SDK(new Python(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -248,7 +245,7 @@ function getSSLPage($url) {
$dart = new Dart();
$dart->setPackageName('dart_appwrite');

$sdk = new SDK($dart, new Swagger2($spec));
$sdk = new SDK($dart, new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -275,7 +272,7 @@ function getSSLPage($url) {
// Flutter
$flutter = new Flutter();
$flutter->setPackageName('appwrite');
$sdk = new SDK($flutter, new Swagger2($spec));
$sdk = new SDK($flutter, new Swagger2($clientSpec));

$sdk
->setName('NAME')
Expand All @@ -302,7 +299,7 @@ function getSSLPage($url) {
// React Native
$reactNative = new ReactNative();
$reactNative->setNPMPackage('react-native-appwrite');
$sdk = new SDK($reactNative, new Swagger2($spec));
$sdk = new SDK($reactNative, new Swagger2($clientSpec));

$sdk
->setName('NAME')
Expand All @@ -328,7 +325,7 @@ function getSSLPage($url) {

// GO

$sdk = new SDK(new Go(), new Swagger2($spec));
$sdk = new SDK(new Go(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -353,7 +350,7 @@ function getSSLPage($url) {


// Swift (Server)
$sdk = new SDK(new Swift(), new Swagger2($spec));
$sdk = new SDK(new Swift(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -377,7 +374,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/swift');

// Swift (Client)
$sdk = new SDK(new Apple(), new Swagger2($spec));
$sdk = new SDK(new Apple(), new Swagger2($clientSpec));

$sdk
->setName('NAME')
Expand All @@ -401,7 +398,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/apple');

// DotNet
$sdk = new SDK(new DotNet(), new Swagger2($spec));
$sdk = new SDK(new DotNet(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -425,7 +422,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/dotnet');

// REST
$sdk = new SDK(new REST(), new Swagger2($spec));
$sdk = new SDK(new REST(), new Swagger2($serverSpec));

$sdk
->setName('NAME')
Expand All @@ -447,7 +444,7 @@ function getSSLPage($url) {

// Android

$sdk = new SDK(new Android(), new Swagger2($spec));
$sdk = new SDK(new Android(), new Swagger2($clientSpec));

$sdk
->setName('Android')
Expand All @@ -471,7 +468,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/android');

// Kotlin
$sdk = new SDK(new Kotlin(), new Swagger2($spec));
$sdk = new SDK(new Kotlin(), new Swagger2($serverSpec));

$sdk
->setName('Kotlin')
Expand All @@ -495,7 +492,7 @@ function getSSLPage($url) {
$sdk->generate(__DIR__ . '/examples/kotlin');

// GraphQL
$sdk = new SDK(new GraphQL(), new Swagger2($spec));
$sdk = new SDK(new GraphQL(), new Swagger2($serverSpec));

$sdk
->setName('GraphQL')
Expand Down
53 changes: 53 additions & 0 deletions src/SDK/Language/Dart.php
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,59 @@ public function getFilters(): array
new TwigFilter('caseEnumKey', function (string $value) {
return $this->toCamelCase($value);
}),

new TwigFilter('hasGenericType', function (?string $model, array $spec) {
return $this->hasGenericType($model, $spec);
}),
new TwigFilter('propertyType', function (array $property, array $spec, string $generic = 'T') {
return $this->getPropertyType($property, $spec, $generic);
}),
];
}

protected function hasGenericType(?string $model, array $spec): bool
{
if (empty($model) || $model === 'any') {
return false;
}

$model = $spec['definitions'][$model];

if ($model['additionalProperties']) {
return true;
}

foreach ($model['properties'] as $property) {
if (!\array_key_exists('sub_schema', $property) || !$property['sub_schema']) {
continue;
}

return $this->hasGenericType($property['sub_schema'], $spec);
}

return false;
}

protected function getPropertyType(array $property, array $spec, string $generic = 'T'): string
{
if (\array_key_exists('sub_schema', $property)) {
$type = $this->toPascalCase($property['sub_schema']);

if ($this->hasGenericType($property['sub_schema'], $spec)) {
$type .= '<' . $generic . '>';
}

if ($property['type'] === 'array') {
$type = 'List<' . $type . '>';
}
} else {
$type = $this->getTypeName($property);
}

if (!$property['required']) {
$type .= '?';
}

return $type;
}
}
2 changes: 1 addition & 1 deletion src/SDK/Language/Kotlin.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ protected function getPropertyType(array $property, array $spec, string $generic
return $type;
}

protected function hasGenericType(?string $model, array $spec): string
protected function hasGenericType(?string $model, array $spec): bool
{
if (empty($model) || $model === 'any') {
return false;
Expand Down
13 changes: 12 additions & 1 deletion templates/dart/base/requests/api.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,15 @@

final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: apiParams, headers: apiHeaders);

return {% if method.responseModel and method.responseModel != 'any' %}models.{{method.responseModel | caseUcfirst | overrideIdentifier}}.fromMap(res.data){% else %} res.data{% endif %};
{% if method.responseModel and method.responseModel != 'any' %}
{% set modelName = method.responseModel | caseUcfirst | overrideIdentifier %}
{% if modelName == 'Document' %}
return models.{{modelName}}.fromMap(res.data, fromJson);
{% elseif modelName ends with 'List' %}
return models.{{modelName}}.fromMap(res.data, fromJson);
{% else %}
return models.{{modelName}}.fromMap(res.data);
{% endif %}
{% else %}
return res.data;
{% endif %}
12 changes: 7 additions & 5 deletions templates/dart/lib/services/service.dart.twig
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
part of '../{{ language.params.packageName }}.dart';
{% macro parameter(parameter) %}{% if parameter.required %}required {% endif %}{{ parameter | typeName }}{% if not parameter.required or parameter.nullable %}?{% endif %} {{ parameter.name | caseCamel | overrideIdentifier }}{% endmacro %}
{% macro method_parameters(parameters, consumes) %}
{% if parameters|length > 0 %}{{ '{' }}{% for parameter in parameters %}{{ _self.parameter(parameter) }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in consumes %}, Function(UploadProgress)? onProgress{% endif %}{{ '}' }}{% endif %}
{% macro method_parameters(parameters, consumes, hasGenerics) %}
{% if parameters|length > 0 or hasGenerics %}{{ '{' }}{% for parameter in parameters %}{{ _self.parameter(parameter) }}{% if not loop.last %}, {% endif %}{% endfor %}{% if hasGenerics %}{% if parameters|length > 0 %}, {% endif %}T Function(Map<String, dynamic>)? fromJson{% endif %}{% if 'multipart/form-data' in consumes %}{% if parameters|length > 0 or hasGenerics %}, {% endif %}Function(UploadProgress)? onProgress{% endif %}{{ '}' }}{% endif %}
{% endmacro %}
{% macro service_params(parameters) %}
{% if parameters|length > 0 %}{{ ', {' }}{% for parameter in parameters %}{% if parameter.required %}required {% endif %}this.{{ parameter.name | caseCamel | overrideIdentifier }}{% if not loop.last %}, {% endif %}{% endfor %}{{ '}' }}{% endif %}
{% macro service_params(parameters, hasGenerics) %}
{% if parameters|length > 0 or hasGenerics %}{{ ', {' }}{% for parameter in parameters %}{% if parameter.required %}required {% endif %}this.{{ parameter.name | caseCamel | overrideIdentifier }}{% if not loop.last %}, {% endif %}{% endfor %}{% if hasGenerics %}{% if parameters|length > 0 %}, {% endif %}this.fromJson{% endif %}{{ '}' }}{% endif %}
{% endmacro %}
{% macro generic_return_type(method) %}{% if method.responseModel and method.responseModel != 'any' %}{% set modelName = method.responseModel | caseUcfirst | overrideIdentifier %}{% if modelName == 'Document' or modelName ends with 'List' %}Future<models.{{modelName}}<T>>{% else %}Future<models.{{modelName}}>{% endif %}{% else %}Future{% endif %}{% endmacro %}

{%if service.description %}
{{- service.description|dartComment | split(' ///') | join('///')}}
{% endif %}
class {{ service.name | caseUcfirst }} extends Service {
{{ service.name | caseUcfirst }}(super.client);
{% for method in service.methods %}
{% set isGenericMethod = method.responseModel and (method.responseModel | caseUcfirst == 'Document' or method.responseModel | caseUcfirst ends with 'List') %}

{%~ if method.description %}
{{ method.description | dartComment }}
{% endif %}
{% if method.type == 'location' %}Future<Uint8List>{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future<models.{{method.responseModel | caseUcfirst | overrideIdentifier}}>{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel | overrideIdentifier }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async {
{% if method.type == 'location' %}Future<Uint8List>{% elseif isGenericMethod %}{{ _self.generic_return_type(method) }}{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future<models.{{method.responseModel | caseUcfirst | overrideIdentifier}}>{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel | overrideIdentifier }}{% if isGenericMethod %}<T>{% endif %}({{ _self.method_parameters(method.parameters.all, method.consumes, isGenericMethod) }}) async {
final String apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}{% if parameter.enumValues | length > 0 %}.value{% endif %}){% endfor %};

{% if 'multipart/form-data' in method.consumes %}
Expand Down
37 changes: 28 additions & 9 deletions templates/dart/lib/src/models/model.dart.twig
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
{% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}>{% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier }}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map<String, dynamic>{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %}
{% macro sub_schema(property, spec) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}{% if definition.name | hasGenericType(spec) %}<T>{% endif %}>{% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier }}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map<String, dynamic>{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %}
part of '../../models.dart';

/// {{ definition.description }}
class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model {
class {{ definition.name | caseUcfirst | overrideIdentifier }}{% if definition.name | hasGenericType(spec) %}<T>{% endif %} implements Model {
{% for property in definition.properties %}
/// {{ property.description }}
final {% if not property.required %}{{_self.sub_schema(property)}}? {{ property.name | escapeKeyword }}{% else %}{{_self.sub_schema(property)}} {{ property.name | escapeKeyword }}{% endif %};
final {% if not property.required %}{{_self.sub_schema(property, spec)}}? {{ property.name | escapeKeyword }}{% else %}{{_self.sub_schema(property, spec)}} {{ property.name | escapeKeyword }}{% endif %};

{% endfor %}
{%~ if definition.additionalProperties %}
final Map<String, dynamic> data;
final T data;

{% endif %}
{{ definition.name | caseUcfirst | overrideIdentifier}}({% if definition.properties | length or definition.additionalProperties %}{{ '{' }}{% endif %}

{% for property in definition.properties %}
{% if property.name starts with '_' %}
{% if property.required %}required {% endif %}{{_self.sub_schema(property, spec)}}{% if not property.required %}?{% endif %} {{ property.name | slice(1) | escapeKeyword }},
{% else %}
{% if property.required %}required {% endif %}this.{{ property.name | escapeKeyword }},
{% endif %}
{% endfor %}
{% if definition.additionalProperties %}
required this.data,
{% endif %}
{% if definition.properties | length or definition.additionalProperties %}{{ '}' }}{% endif %});
{% if definition.properties | length or definition.additionalProperties %}{{ '}' }}{% endif %}){% set hasUnderscoreProps = false %}{% for property in definition.properties %}{% if property.name starts with '_' %}{% set hasUnderscoreProps = true %}{% endif %}{% endfor %}{% if hasUnderscoreProps %} :
{% for property in definition.properties %}
{% if property.name starts with '_' %}
{{ property.name | escapeKeyword }} = {{ property.name | slice(1) | escapeKeyword }}{% if not loop.last %},{% endif %}

factory {{ definition.name | caseUcfirst | overrideIdentifier}}.fromMap(Map<String, dynamic> map) {
{% endif %}
{% endfor %}{% endif %};

factory {{ definition.name | caseUcfirst | overrideIdentifier}}.fromMap(Map<String, dynamic> map{% if definition.name | hasGenericType(spec) %}, [T Function(Map<String, dynamic>)? fromJson]{% endif %}) {
return {{ definition.name | caseUcfirst | overrideIdentifier }}(
{% for property in definition.properties %}
{% if property.name starts with '_' %}
{{ property.name | slice(1) | escapeKeyword }}:{{' '}}{% else %}
{{ property.name | escapeKeyword }}:{{' '}}
{% endif %}
{%- if property.sub_schema -%}
{%- if property.type == 'array' -%}
List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}>.from(map['{{property.name | escapeDollarSign }}'].map((p) => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.fromMap(p)))
List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}{% if property.sub_schema | hasGenericType(spec) %}<T>{% endif %}>.from(map['{{property.name | escapeDollarSign }}'].map((p) => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.fromMap(p{% if property.sub_schema | hasGenericType(spec) %}, fromJson{% endif %})))
{%- else -%}
{{property.sub_schema | caseUcfirst | overrideIdentifier}}.fromMap(map['{{property.name | escapeDollarSign }}'])
{%- endif -%}
Expand All @@ -47,7 +60,7 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model
{%- endif -%},
{% endfor %}
{% if definition.additionalProperties %}
data: map,
data: fromJson != null ? fromJson(map) : map as T,
{% endif %}
);
}
Expand All @@ -64,7 +77,7 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model
}
{% if definition.additionalProperties %}

T convertTo<T>(T Function(Map<String, dynamic>) fromJson) => fromJson(data);
T convertTo<T>(T Function(Map<String, dynamic>) fromJson) => fromJson(data as Map<String, dynamic>);
{% endif %}
{% for property in definition.properties %}
{% if property.sub_schema %}
Expand All @@ -77,4 +90,10 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model
{% endfor %}
{% endif %}
{% endfor %}

{% for property in definition.properties %}
{% if property.name starts with '_' %}
{{_self.sub_schema(property, spec)}}{% if not property.required %}?{% endif %} get {{ property.name | slice(1) | escapeKeyword }} => {{ property.name | escapeKeyword }};
{% endif %}
{% endfor %}
}
Loading
Loading