diff --git a/example.php b/example.php index eb1ebd40b..c4af8aceb 100644 --- a/example.php +++ b/example.php @@ -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'); } @@ -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') @@ -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') @@ -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') @@ -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') @@ -168,7 +165,7 @@ function getSSLPage($url) { \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ |_| |_| "); - $sdk = new SDK($language, new Swagger2($spec)); + $sdk = new SDK($language, new Swagger2($consoleSpec)); $sdk ->setName('NAME') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') @@ -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') diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index ca9356906..ef49b75bd 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -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; + } } diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 764949528..2d0acc63e 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -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; diff --git a/templates/dart/base/requests/api.twig b/templates/dart/base/requests/api.twig index 43683fd6c..fd9ca7ed2 100644 --- a/templates/dart/base/requests/api.twig +++ b/templates/dart/base/requests/api.twig @@ -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 %} \ No newline at end of file diff --git a/templates/dart/lib/services/service.dart.twig b/templates/dart/lib/services/service.dart.twig index b7ea9048f..1274a496d 100644 --- a/templates/dart/lib/services/service.dart.twig +++ b/templates/dart/lib/services/service.dart.twig @@ -1,11 +1,12 @@ 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)? 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>{% else %}Future{% endif %}{% else %}Future{% endif %}{% endmacro %} {%if service.description %} {{- service.description|dartComment | split(' ///') | join('///')}} @@ -13,11 +14,12 @@ part of '../{{ language.params.packageName }}.dart'; 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{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel | overrideIdentifier }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async { + {% if method.type == 'location' %}Future{% elseif isGenericMethod %}{{ _self.generic_return_type(method) }}{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel | overrideIdentifier }}{% if isGenericMethod %}{% 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 %} diff --git a/templates/dart/lib/src/models/model.dart.twig b/templates/dart/lib/src/models/model.dart.twig index f27126ff3..3f4f8c78f 100644 --- a/templates/dart/lib/src/models/model.dart.twig +++ b/templates/dart/lib/src/models/model.dart.twig @@ -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{% 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) %}{% endif %}>{% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier }}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map{% 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) %}{% 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 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 map) { +{% endif %} +{% endfor %}{% endif %}; + + factory {{ definition.name | caseUcfirst | overrideIdentifier}}.fromMap(Map map{% if definition.name | hasGenericType(spec) %}, [T Function(Map)? 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) %}{% 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 -%} @@ -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 %} ); } @@ -64,7 +77,7 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model } {% if definition.additionalProperties %} - T convertTo(T Function(Map) fromJson) => fromJson(data); + T convertTo(T Function(Map) fromJson) => fromJson(data as Map); {% endif %} {% for property in definition.properties %} {% if property.sub_schema %} @@ -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 %} } diff --git a/templates/dart/test/src/models/model_test.dart.twig b/templates/dart/test/src/models/model_test.dart.twig index ef1dad283..b6ed1b12e 100644 --- a/templates/dart/test/src/models/model_test.dart.twig +++ b/templates/dart/test/src/models/model_test.dart.twig @@ -12,7 +12,11 @@ void main() { test('model', () { final model = {{ definition.name | caseUcfirst | overrideIdentifier }}( {% for property in definition.properties | filter(p => p.required) %} +{% if property.name starts with '_' %} + {{ property.name | slice(1) | escapeKeyword }}: {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}Preferences(data: {}){% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, +{% else %} {{ property.name | escapeKeyword }}: {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}Preferences(data: {}){% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, +{% endif %} {% endfor %} {% if definition.additionalProperties %} data: {}, @@ -23,7 +27,11 @@ void main() { final result = {{ definition.name | caseUcfirst | overrideIdentifier }}.fromMap(map); {% for property in definition.properties | filter(p => p.required) %} +{% if property.name starts with '_' %} + expect(result.{{ property.name | slice(1) | escapeKeyword }}{% if property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}.data{% endif %}, {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}{"data": {}}{% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}); +{% else %} expect(result.{{ property.name | escapeKeyword }}{% if property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}.data{% endif %}, {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}{"data": {}}{% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}); +{% endif %} {% endfor %} }); });