Skip to content

Commit 2b48bf6

Browse files
Merge pull request #559 from appwrite/feat-kotlin-generics
Feat kotlin generics
2 parents 86afa25 + 702c7ee commit 2b48bf6

File tree

16 files changed

+470
-285
lines changed

16 files changed

+470
-285
lines changed

src/SDK/Language/Android.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public function getFiles(): array
135135
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/extensions/JsonExtensions.kt',
136136
'template' => '/android/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt.twig',
137137
],
138+
[
139+
'scope' => 'default',
140+
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/extensions/TypeExtensions.kt',
141+
'template' => '/android/library/src/main/java/io/appwrite/extensions/TypeExtensions.kt.twig',
142+
],
138143
[
139144
'scope' => 'default',
140145
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/extensions/CollectionExtensions.kt',

src/SDK/Language/Kotlin.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Appwrite\SDK\Language;
44

55
use Appwrite\SDK\Language;
6+
use Twig\TwigFilter;
67

78
class Kotlin extends Language
89
{
@@ -381,6 +382,12 @@ public function getFiles(): array
381382
'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/JsonExtensions.kt',
382383
'template' => '/kotlin/src/main/kotlin/io/appwrite/extensions/JsonExtensions.kt.twig',
383384
],
385+
[
386+
'scope' => 'default',
387+
'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/extensions/TypeExtensions.kt',
388+
'template' => '/kotlin/src/main/kotlin/io/appwrite/extensions/TypeExtensions.kt.twig',
389+
'minify' => false,
390+
],
384391
[
385392
'scope' => 'default',
386393
'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/json/PreciseNumberAdapter.kt',
@@ -413,4 +420,102 @@ public function getFiles(): array
413420
],
414421
];
415422
}
423+
424+
public function getFilters(): array
425+
{
426+
return [
427+
new TwigFilter('returnType', function (array $method, array $spec, string $namespace, string $generic = 'T') {
428+
return $this->getReturnType($method, $spec, $namespace, $generic);
429+
}),
430+
new TwigFilter('modelType', function (array $property, array $spec, string $generic = 'T') {
431+
return $this->getModelType($property, $spec, $generic);
432+
}),
433+
new TwigFilter('propertyType', function (array $property, array $spec, string $generic = 'T') {
434+
return $this->getPropertyType($property, $spec, $generic);
435+
}),
436+
new TwigFilter('hasGenericType', function (string $model, array $spec) {
437+
return $this->hasGenericType($model, $spec);
438+
}),
439+
];
440+
}
441+
442+
protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T'): string
443+
{
444+
if ($method['type'] === 'webAuth') {
445+
return 'Bool';
446+
}
447+
if ($method['type'] === 'location') {
448+
return 'ByteArray';
449+
}
450+
451+
if (
452+
!\array_key_exists('responseModel', $method)
453+
|| empty($method['responseModel'])
454+
|| $method['responseModel'] === 'any'
455+
) {
456+
return 'Any';
457+
}
458+
459+
$ret = $this->toUpperCaseWords($method['responseModel']);
460+
461+
if ($this->hasGenericType($method['responseModel'], $spec)) {
462+
$ret .= '<' . $generic . '>';
463+
}
464+
465+
return $namespace . '.models.' . $ret;
466+
}
467+
468+
protected function getModelType(array $definition, array $spec, string $generic = 'T'): string
469+
{
470+
if ($this->hasGenericType($definition['name'], $spec)) {
471+
return $this->toUpperCaseWords($definition['name']) . '<' . $generic . '>';
472+
}
473+
return $this->toUpperCaseWords($definition['name']);
474+
}
475+
476+
protected function getPropertyType(array $property, array $spec, string $generic = 'T'): string
477+
{
478+
if (\array_key_exists('sub_schema', $property)) {
479+
$type = $this->toUpperCaseWords($property['sub_schema']);
480+
481+
if ($this->hasGenericType($property['sub_schema'], $spec)) {
482+
$type .= '<' . $generic . '>';
483+
}
484+
485+
if ($property['type'] === 'array') {
486+
$type = 'List<' . $type . '>';
487+
}
488+
} else {
489+
$type = $this->getTypeName($property);
490+
}
491+
492+
if (!$property['required']) {
493+
$type .= '?';
494+
}
495+
496+
return $type;
497+
}
498+
499+
protected function hasGenericType(?string $model, array $spec): string
500+
{
501+
if (empty($model) || $model === 'any') {
502+
return false;
503+
}
504+
505+
$model = $spec['definitions'][$model];
506+
507+
if ($model['additionalProperties']) {
508+
return true;
509+
}
510+
511+
foreach ($model['properties'] as $property) {
512+
if (!\array_key_exists('sub_schema', $property) || !$property['sub_schema']) {
513+
continue;
514+
}
515+
516+
return $this->hasGenericType($property['sub_schema'], $spec);
517+
}
518+
519+
return false;
520+
}
416521
}

templates/android/build.gradle.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ apply plugin: 'io.github.gradle-nexus.publish-plugin'
22

33
// Top-level build file where you can add configuration options common to all sub-projects/modules.
44
buildscript {
5-
ext.kotlin_version = "1.5.31"
5+
ext.kotlin_version = "1.7.10"
66
version System.getenv("SDK_VERSION")
77
repositories {
88
maven { url "https://plugins.gradle.org/m2/" }
99
google()
1010
mavenCentral()
1111
}
1212
dependencies {
13-
classpath "com.android.tools.build:gradle:4.2.0"
13+
classpath "com.android.tools.build:gradle:4.2.2"
1414
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1515
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
1616

templates/android/docs/java/example.md.twig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
{% import 'kotlin/base/utils.twig' as utils %}
21
import {{ sdk.namespace | caseDot }}.Client;
32
import {{ sdk.namespace | caseDot }}.coroutines.CoroutineCallback;
43
{% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 %}

templates/android/library/build.gradle.twig

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ ext {
2222
version PUBLISH_VERSION
2323

2424
android {
25-
compileSdkVersion(31)
25+
compileSdkVersion(33)
2626

2727
defaultConfig {
2828
minSdkVersion(21)
29-
targetSdkVersion(31)
29+
targetSdkVersion(33)
3030
versionCode = 1
3131
versionName = "1.0"
3232
buildConfigField "String", "SDK_VERSION", "\"${PUBLISH_VERSION}\""
@@ -54,27 +54,27 @@ android {
5454

5555
dependencies {
5656
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION}")
57-
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
58-
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
57+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
58+
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1")
5959

60-
api(platform("com.squareup.okhttp3:okhttp-bom:4.9.0"))
60+
api(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
6161
api("com.squareup.okhttp3:okhttp")
6262
implementation("com.squareup.okhttp3:okhttp-urlconnection")
6363
implementation("com.squareup.okhttp3:logging-interceptor")
64-
implementation("com.google.code.gson:gson:2.8.7")
64+
implementation("com.google.code.gson:gson:2.9.0")
6565

66-
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
67-
implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1")
68-
implementation("androidx.appcompat:appcompat:1.3.1")
69-
implementation("androidx.fragment:fragment-ktx:1.3.6")
70-
implementation("androidx.activity:activity-ktx:1.3.1")
71-
implementation("androidx.browser:browser:1.3.0")
66+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
67+
implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
68+
implementation("androidx.appcompat:appcompat:1.5.1")
69+
implementation("androidx.fragment:fragment-ktx:1.5.3")
70+
implementation("androidx.activity:activity-ktx:1.6.0")
71+
implementation("androidx.browser:browser:1.4.0")
7272

7373
testImplementation 'junit:junit:4.+'
7474
testImplementation "androidx.test.ext:junit-ktx:1.1.3"
7575
testImplementation "androidx.test:core-ktx:1.4.0"
7676
testImplementation "org.robolectric:robolectric:4.5.1"
77-
testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1")
77+
testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1")
7878
}
7979

8080
apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package {{ sdk.namespace | caseDot }}.extensions
2+
3+
import kotlin.reflect.KClass
4+
import kotlin.reflect.typeOf
5+
6+
inline fun <reified T : Any> classOf(): Class<T> {
7+
return (typeOf<T>().classifier!! as KClass<T>).java
8+
}
Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,71 @@
1-
{% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<{{property.sub_schema | caseUcfirst}}>{% else %}{{property.sub_schema | caseUcfirst}}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map<String, Any>{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %}
21
package {{ sdk.namespace | caseDot }}.models
32

43
import com.google.gson.annotations.SerializedName
4+
import io.appwrite.extensions.jsonCast
55

66
/**
77
* {{ definition.description }}
88
*/
9-
data class {{ definition.name | caseUcfirst }}(
10-
{% for property in definition.properties %}
9+
{% if definition.properties | length != 0 or definition.additionalProperties %}data {% endif %}class {{ definition | modelType(spec) | raw }}(
10+
{%~ for property in definition.properties %}
1111
/**
1212
* {{ property.description }}
13-
*
1413
*/
1514
@SerializedName("{{ property.name | escapeKeyword | escapeDollarSign}}")
16-
{% if property.required %}val{% else%}var{% endif %} {{ property.name | escapeKeyword | removeDollarSign }}: {{_self.sub_schema(property)}}{% if not property.required %}?{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{{ "\n" }}{% endif %}
15+
{% if property.required -%} val
16+
{%- else -%} var
17+
{%- endif %} {{ property.name | escapeKeyword | removeDollarSign }}: {{ property | propertyType(spec) | raw }},
1718

18-
{% endfor %}
19-
{% if definition.additionalProperties %}
20-
val data: Map<String, Any>
21-
{% endif %}
19+
{%~ endfor %}
20+
{%~ if definition.additionalProperties %}
21+
/**
22+
* Additional properties
23+
*/
24+
@SerializedName("data")
25+
val data: T
26+
{%~ endif %}
2227
) {
23-
companion object {
24-
@Suppress("UNCHECKED_CAST")
25-
fun from(map: Map<String, Any>) = {{ definition.name | caseUcfirst }}(
26-
{% for property in definition.properties %}
27-
{{ property.name | escapeKeyword | removeDollarSign }} = {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{ property.name | escapeDollarSign }}"] as List<Map<String, Any>>).map { {{property.sub_schema | caseUcfirst}}.from(map = it) }{% else %}{{property.sub_schema | caseUcfirst}}.from(map = map["{{property.name | escapeDollarSign }}"] as Map<String, Any>){% endif %}{% else %}{% if property.type == "integer" or property.type == "number" %}({% endif %}map["{{ property.name | escapeDollarSign }}"]{% if property.type == "integer" or property.type == "number" %} as{% if not property.required %}?{% endif %} Number){% endif %}{% if property.type == "integer" %}{% if not property.required %}?{% endif %}.toLong(){% elseif property.type == "number" %}{% if not property.required %}?{% endif %}.toDouble(){% else %} as{% if not property.required %}?{% endif %} {{ _self.sub_schema(property) }}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %}
28-
29-
{% endfor %}
30-
{% if definition.additionalProperties %}
31-
data = map
32-
{% endif %}
33-
)
34-
}
35-
3628
fun toMap(): Map<String, Any> = mapOf(
37-
{% for property in definition.properties %}
38-
"{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %}
39-
40-
{% endfor %}
41-
{% if definition.additionalProperties %}
42-
"data" to data
43-
{% endif %}
29+
{%~ for property in definition.properties %}
30+
"{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any,
31+
{%~ endfor %}
32+
{%~ if definition.additionalProperties %}
33+
"data" to data!!.jsonCast(to = Map::class.java)
34+
{%~ endif %}
4435
)
45-
{% if definition.additionalProperties %}
4636

47-
fun <T> convertTo(fromJson: (Map<String, Any>) -> T): T {
48-
return fromJson(data)
49-
}
50-
{% endif %}
51-
{% for property in definition.properties %}
52-
{% if property.sub_schema %}
53-
{% for def in spec.definitions %}
54-
{% if def.name == property.sub_schema and def.additionalProperties and property.type == 'array' %}
37+
companion object {
38+
{%~ if definition.name | hasGenericType(spec) %}
39+
operator fun invoke(
40+
{%~ for property in definition.properties %}
41+
{{ property.name | escapeKeyword | removeDollarSign }}: {{ property | propertyType(spec, 'Map<String, Any>') | raw }},
42+
{%~ endfor %}
43+
{%~ if definition.additionalProperties %}
44+
data: Map<String, Any>
45+
{%~ endif %}
46+
) = {{ definition | modelType(spec, 'Map<String, Any>') | raw }}(
47+
{%~ for property in definition.properties %}
48+
{{ property.name | escapeKeyword | removeDollarSign }},
49+
{%~ endfor %}
50+
{%~ if definition.additionalProperties %}
51+
data
52+
{%~ endif %}
53+
)
54+
{%~ endif %}
5555

56-
fun <T> convertTo(fromJson: (Map<String, Any>) -> T) =
57-
{{property.name | removeDollarSign}}.map { it.convertTo(fromJson = fromJson) }
58-
{% endif %}
59-
{% endfor %}
60-
{% endif %}
61-
{% endfor %}
56+
@Suppress("UNCHECKED_CAST")
57+
fun {% if definition.name | hasGenericType(spec) %}<T> {% endif %}from(
58+
map: Map<String, Any>,
59+
{%~ if definition.name | hasGenericType(spec) %}
60+
nestedType: Class<T>
61+
{%~ endif %}
62+
) = {{ definition | modelType(spec) | raw }}(
63+
{%~ for property in definition.properties %}
64+
{{ property.name | escapeKeyword | removeDollarSign }} = {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{ property.name | escapeDollarSign }}"] as List<Map<String, Any>>).map { {{ property.sub_schema | caseUcfirst }}.from(map = it{% if definition.name | hasGenericType(spec) %}, nestedType{% endif %}) }{% else %}{{ property.sub_schema | caseUcfirst }}.from(map = map["{{property.name | escapeDollarSign }}"] as Map<String, Any>{% if definition.name | hasGenericType(spec) %}, nestedType{% endif %}){% endif %}{% else %}{% if property.type == "integer" or property.type == "number" %}({% endif %}map["{{ property.name | escapeDollarSign }}"]{% if property.type == "integer" or property.type == "number" %} as{% if not property.required %}?{% endif %} Number){% endif %}{% if property.type == "integer" %}{% if not property.required %}?{% endif %}.toLong(){% elseif property.type == "number" %}{% if not property.required %}?{% endif %}.toDouble(){% else %} as{% if not property.required %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% endif %},
65+
{%~ endfor %}
66+
{%~ if definition.additionalProperties %}
67+
data = map.jsonCast(to = nestedType)
68+
{%~ endif %}
69+
)
70+
}
6271
}

0 commit comments

Comments
 (0)