From af3fe02c35341f45f0aeefe291e97c0dc2e6ce10 Mon Sep 17 00:00:00 2001 From: Yair Levi Date: Mon, 8 Jan 2024 23:10:51 +0200 Subject: [PATCH 1/3] Add ignore and only specifications in @BindType --- .../org/levi/coffee/annotations/BindType.java | 15 ++++++- .../org/levi/coffee/internal/CodeGenerator.kt | 8 ++-- .../org/levi/coffee/internal/TypeConverter.kt | 43 ++++++++++++++++++- src/test/kotlin/Main.kt | 5 ++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindType.java b/src/main/kotlin/org/levi/coffee/annotations/BindType.java index dbccc6d..ebfb485 100644 --- a/src/main/kotlin/org/levi/coffee/annotations/BindType.java +++ b/src/main/kotlin/org/levi/coffee/annotations/BindType.java @@ -8,5 +8,18 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface BindType { - String[] exclude() default {}; // TODO: optionally, ignore some fields. + /** + * Specifies which fields to pick for the typescript type. + * If empty of not provided at all, it will be ignored. + * If provided, will override the exclude tag. + * @return the exclusively included fields' names. + */ + String[] only() default {}; + + /** + * Specifies which fields to ignore when building the typescript type. + * If empty or not provided, the type will include all fields. + * @return the ignored fields' names + */ + String[] ignore() default {}; // TODO: optionally, ignore some fields. } \ No newline at end of file diff --git a/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt b/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt index dcd02e6..26f2480 100644 --- a/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt +++ b/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt @@ -57,13 +57,15 @@ internal object CodeGenerator { continue } + val destructedClass = TypeConverter.getDestructedClass(c) + // Declare type and export - writer.println("export type ${c.simpleName} = {") + writer.println("export type ${destructedClass.name} = {") // Add fields and map the types from java to typescript - for (field in c.declaredFields) { + for (field in destructedClass.fields) { val name = field.name - val type = TypeConverter.convert(field.genericType, false) + val type = TypeConverter.convert(field.type, false) writer.println("\t$name: $type") } writer.println("}\n") diff --git a/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt b/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt index 764646b..2d0b829 100644 --- a/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt +++ b/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt @@ -1,9 +1,21 @@ package org.levi.coffee.internal +import org.levi.coffee.annotations.BindType import org.slf4j.LoggerFactory import java.lang.reflect.Type import java.util.* import java.util.regex.Pattern +import kotlin.collections.ArrayList + +internal class DestructedField( + val name: String, + val type: Type, +) + +internal class DestructedClass( + val name: String, + val fields: List +) internal object TypeConverter { val boundTypes: MutableSet = HashSet() @@ -61,8 +73,10 @@ internal object TypeConverter { while (matcher.find()) { val javaType = matcher.group() if (!jsTypes.containsKey(javaType) && !boundTypes.contains(javaType)) { - log.error("java type $javaType is not recognized. Did you forget to @BindType ?\n" + - "Used 'any' instead, just in case.") + log.error( + "java type $javaType is not recognized. Did you forget to @BindType ?\n" + + "Used 'any' instead, just in case." + ) type = type.replace(javaType, jsTypes.getOrDefault(javaType, "any")) } else if (!jsTypes.containsKey(javaType) && addTypePrefix) { type = type.replace(javaType, "t.$javaType") @@ -72,4 +86,29 @@ internal object TypeConverter { } return type } + + /** + * Assuming @BindType annotation is present. + */ + fun getDestructedClass(c: Class<*>): DestructedClass { + // check if "only" is present. if so, take only the fields specified. + // if not, check if "ignore" is present. take only the fields NOT specified. + val annotation = c.getAnnotation(BindType::class.java) + + val fields: List = + if (annotation.only.isNotEmpty()) { + c.declaredFields + .filter { annotation.only.contains(it.name) } + .map { DestructedField(it.name, it.genericType) } + } else { + c.declaredFields + .filter { !annotation.ignore.contains(it.name) } + .map { DestructedField(it.name, it.genericType) } + } + + return DestructedClass( + name = c.simpleName, + fields = fields + ) + } } \ No newline at end of file diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index e258efe..a6f855c 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -5,7 +5,10 @@ import org.levi.coffee.annotations.BindType import java.io.BufferedReader import java.io.File -@BindType +@BindType( + only = [], + ignore = ["age"] +) class Person( val name: String = "", var age: Int = 0, From 7ecfa04e3b2559e0c0f650da20ddc875e5a0297b Mon Sep 17 00:00:00 2001 From: Yair Levi Date: Tue, 9 Jan 2024 13:38:07 +0200 Subject: [PATCH 2/3] Convert to kotlin --- .../levi/coffee/annotations/BindMethod.java | 10 ----- .../org/levi/coffee/annotations/BindMethod.kt | 9 +++++ .../{BindType.java => BindType.kt} | 20 ++++------ src/test/kotlin/Main.kt | 38 +++++++++++-------- 4 files changed, 39 insertions(+), 38 deletions(-) delete mode 100644 src/main/kotlin/org/levi/coffee/annotations/BindMethod.java create mode 100644 src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt rename src/main/kotlin/org/levi/coffee/annotations/{BindType.java => BindType.kt} (51%) diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindMethod.java b/src/main/kotlin/org/levi/coffee/annotations/BindMethod.java deleted file mode 100644 index 68baa16..0000000 --- a/src/main/kotlin/org/levi/coffee/annotations/BindMethod.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.levi.coffee.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface BindMethod { } diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt b/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt new file mode 100644 index 0000000..7dd8043 --- /dev/null +++ b/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt @@ -0,0 +1,9 @@ +package org.levi.coffee.annotations + +@Retention(AnnotationRetention.RUNTIME) +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +annotation class BindMethod diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindType.java b/src/main/kotlin/org/levi/coffee/annotations/BindType.kt similarity index 51% rename from src/main/kotlin/org/levi/coffee/annotations/BindType.java rename to src/main/kotlin/org/levi/coffee/annotations/BindType.kt index ebfb485..848ee36 100644 --- a/src/main/kotlin/org/levi/coffee/annotations/BindType.java +++ b/src/main/kotlin/org/levi/coffee/annotations/BindType.kt @@ -1,25 +1,19 @@ -package org.levi.coffee.annotations; +package org.levi.coffee.annotations -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface BindType { +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class BindType( /** * Specifies which fields to pick for the typescript type. * If empty of not provided at all, it will be ignored. * If provided, will override the exclude tag. * @return the exclusively included fields' names. */ - String[] only() default {}; - + val only: Array = [], /** * Specifies which fields to ignore when building the typescript type. * If empty or not provided, the type will include all fields. * @return the ignored fields' names */ - String[] ignore() default {}; // TODO: optionally, ignore some fields. -} \ No newline at end of file + val ignore: Array = [], // TODO: optionally, ignore some fields. +) \ No newline at end of file diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index 68f4d01..cc1b17f 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -1,3 +1,4 @@ +import com.google.gson.Gson import org.levi.coffee.Ipc import org.levi.coffee.Window import org.levi.coffee.annotations.BindMethod @@ -5,9 +6,9 @@ import org.levi.coffee.annotations.BindType import java.io.BufferedReader import java.io.File - only = [], @BindType( - ignore = ["age"] + only = [], + ignore = ["age"], ) class Person( val name: String = "", @@ -29,17 +30,24 @@ class Person( } fun main() { - val win = Window() - win.setSize(700, 700) - win.setTitle("My first Javatron app!") - - win.setURL("http://localhost:5173") - win.bind( - Person(), - ) - - win.addBeforeStartCallback { println("Started app...") } - win.addOnCloseCallback { println("Closed the app!") } - - win.run() + val g = Gson() + val json = """{"name":"Yair"}""" + val p = g.fromJson(json, Person::class.java) + println(p.age) + println(p.name) + println(p.hobbies) + println(p.string) +// val win = Window() +// win.setSize(700, 700) +// win.setTitle("My first Javatron app!") +// +// win.setURL("http://localhost:5173") +// win.bind( +// Person(), +// ) +// +// win.addBeforeStartCallback { println("Started app...") } +// win.addOnCloseCallback { println("Closed the app!") } +// +// win.run() } From 374e9083496458b0d7ac604a6168dfe72be431de Mon Sep 17 00:00:00 2001 From: Yair Levi Date: Tue, 9 Jan 2024 14:34:46 +0200 Subject: [PATCH 3/3] feat: Binding annotations more robust. - Add @BindAllMethods and @IgnoreMethod - Add 'only' and 'ignore' to fields at @BindType - fix bug: when more than once a custom type is present, prefixes the type with multiple 't.'. e.g., t.t.t.person when there's 2 Person in the type - Map>. --- .../levi/coffee/annotations/BindAllMethods.kt | 8 ++++ .../org/levi/coffee/annotations/BindMethod.kt | 3 ++ .../org/levi/coffee/annotations/BindType.kt | 3 ++ .../levi/coffee/annotations/IgnoreMethod.kt | 13 ++++++ .../org/levi/coffee/internal/BindFilter.kt | 27 +++++++++++ .../org/levi/coffee/internal/CodeGenerator.kt | 24 +++++----- .../org/levi/coffee/internal/TypeConverter.kt | 46 +++---------------- src/test/kotlin/Main.kt | 42 +++++++---------- 8 files changed, 88 insertions(+), 78 deletions(-) create mode 100644 src/main/kotlin/org/levi/coffee/annotations/BindAllMethods.kt create mode 100644 src/main/kotlin/org/levi/coffee/annotations/IgnoreMethod.kt create mode 100644 src/main/kotlin/org/levi/coffee/internal/BindFilter.kt diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindAllMethods.kt b/src/main/kotlin/org/levi/coffee/annotations/BindAllMethods.kt new file mode 100644 index 0000000..34ac3e6 --- /dev/null +++ b/src/main/kotlin/org/levi/coffee/annotations/BindAllMethods.kt @@ -0,0 +1,8 @@ +package org.levi.coffee.annotations + +/** + * Use on a class instead of @BindMethod on all functions separately. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class BindAllMethods diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt b/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt index 7dd8043..3056d54 100644 --- a/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt +++ b/src/main/kotlin/org/levi/coffee/annotations/BindMethod.kt @@ -1,5 +1,8 @@ package org.levi.coffee.annotations +/** + * Specify to bind a certain function to the frontend. + */ @Retention(AnnotationRetention.RUNTIME) @Target( AnnotationTarget.FUNCTION, diff --git a/src/main/kotlin/org/levi/coffee/annotations/BindType.kt b/src/main/kotlin/org/levi/coffee/annotations/BindType.kt index 848ee36..25f00e0 100644 --- a/src/main/kotlin/org/levi/coffee/annotations/BindType.kt +++ b/src/main/kotlin/org/levi/coffee/annotations/BindType.kt @@ -1,5 +1,8 @@ package org.levi.coffee.annotations +/** + * Specify a class to convert to a typescript type on the frontend. + */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.CLASS) annotation class BindType( diff --git a/src/main/kotlin/org/levi/coffee/annotations/IgnoreMethod.kt b/src/main/kotlin/org/levi/coffee/annotations/IgnoreMethod.kt new file mode 100644 index 0000000..be07052 --- /dev/null +++ b/src/main/kotlin/org/levi/coffee/annotations/IgnoreMethod.kt @@ -0,0 +1,13 @@ +package org.levi.coffee.annotations + +/** + * When using @BindAllMethods on a class, use @IgnoreMethod to mark methods + * to be ignored and not bind to the frontend. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +annotation class IgnoreMethod diff --git a/src/main/kotlin/org/levi/coffee/internal/BindFilter.kt b/src/main/kotlin/org/levi/coffee/internal/BindFilter.kt new file mode 100644 index 0000000..5e4c48a --- /dev/null +++ b/src/main/kotlin/org/levi/coffee/internal/BindFilter.kt @@ -0,0 +1,27 @@ +package org.levi.coffee.internal + +import org.levi.coffee.annotations.BindAllMethods +import org.levi.coffee.annotations.BindMethod +import org.levi.coffee.annotations.BindType +import org.levi.coffee.annotations.IgnoreMethod +import java.lang.reflect.Field +import java.lang.reflect.Method + +object BindFilter { + fun methodsOf(c: Class<*>): List { + if (c.isAnnotationPresent(BindAllMethods::class.java)) { + return c.declaredMethods.filter { !it.isAnnotationPresent(IgnoreMethod::class.java) } + } + return c.declaredMethods.filter { it.isAnnotationPresent(BindMethod::class.java) } + } + + fun fieldsOf(c: Class<*>): List { + // Assuming "@BindType()" present on class "c" + + val annotation = c.getAnnotation(BindType::class.java) + if (annotation.only.isNotEmpty()) { + return c.declaredFields.filter { annotation.only.contains(it.name) } + } + return c.declaredFields.filter { !annotation.ignore.contains(it.name) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt b/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt index 9c8340b..2454b13 100644 --- a/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt +++ b/src/main/kotlin/org/levi/coffee/internal/CodeGenerator.kt @@ -37,22 +37,22 @@ internal object CodeGenerator { fun generateTypes(vararg objects: Any) { try { val writer = PrintWriter(TYPES_FILE_PATH) - val classes = objects.map { it.javaClass } + val classes = objects.map { it::class.java } TypeConverter.boundTypes.addAll(classes.map { it.simpleName }) for (c in classes) { if (!c.isAnnotationPresent(BindType::class.java)) { continue } - val destructedClass = TypeConverter.getDestructedClass(c) + val fieldsToBind = BindFilter.fieldsOf(c) // Declare type and export - writer.println("export type ${destructedClass.name} = {") + writer.println("export type ${c.simpleName} = {") // Add fields and map the types from java to typescript - for (field in destructedClass.fields) { + for (field in fieldsToBind) { val name = field.name - val type = TypeConverter.convert(field.type, false) + val type = TypeConverter.convert(field.genericType, false) writer.println("\t$name: $type") } writer.println("}\n") @@ -68,7 +68,7 @@ internal object CodeGenerator { fun generateFunctions(vararg objects: Any) { for (c in objects.map { it.javaClass }) { - val methodCount = c.declaredMethods.filter { it.isAnnotationPresent(BindMethod::class.java) }.size + val methodCount = BindFilter.methodsOf(c).size if (methodCount == 0) { continue } @@ -84,9 +84,9 @@ internal object CodeGenerator { FileUtil.createOrReplaceFile(path) val writer = PrintWriter(path) - for (method in c.declaredMethods) { - if (!method.isAnnotationPresent(BindMethod::class.java)) continue + val methodsToBind = BindFilter.methodsOf(c) + for (method in methodsToBind) { val methodName = method.name val argsString = method.parameters.joinToString(",") { it.name } writer.println("export function $methodName($argsString) {") @@ -107,13 +107,11 @@ internal object CodeGenerator { FileUtil.createOrReplaceFile(path) val writer = PrintWriter(path) - writer.println("import * as t from '../types';\n") + val methodsToBind = BindFilter.methodsOf(c) - for (method in c.declaredMethods.sortedBy { it.name }) { - if (!method.isAnnotationPresent(BindMethod::class.java)) { - continue - } + writer.println("import * as t from '../types';\n") + for (method in methodsToBind) { val argsString = method.parameters.joinToString(", ") { "${it.name}: ${TypeConverter.convert(it.parameterizedType, true)}" } diff --git a/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt b/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt index 2d0b829..a3760a2 100644 --- a/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt +++ b/src/main/kotlin/org/levi/coffee/internal/TypeConverter.kt @@ -1,21 +1,8 @@ package org.levi.coffee.internal -import org.levi.coffee.annotations.BindType import org.slf4j.LoggerFactory import java.lang.reflect.Type -import java.util.* import java.util.regex.Pattern -import kotlin.collections.ArrayList - -internal class DestructedField( - val name: String, - val type: Type, -) - -internal class DestructedClass( - val name: String, - val fields: List -) internal object TypeConverter { val boundTypes: MutableSet = HashSet() @@ -69,9 +56,13 @@ internal object TypeConverter { val pattern = Pattern.compile("[a-zA-Z0-9]+") val matcher = pattern.matcher(type) - // Each type found, swap for the corresponding type in Typescript + val types = HashSet() while (matcher.find()) { - val javaType = matcher.group() + types.add(matcher.group()) + } + + // Each type found, swap for the corresponding type in Typescript + for (javaType in types) { if (!jsTypes.containsKey(javaType) && !boundTypes.contains(javaType)) { log.error( "java type $javaType is not recognized. Did you forget to @BindType ?\n" + @@ -86,29 +77,4 @@ internal object TypeConverter { } return type } - - /** - * Assuming @BindType annotation is present. - */ - fun getDestructedClass(c: Class<*>): DestructedClass { - // check if "only" is present. if so, take only the fields specified. - // if not, check if "ignore" is present. take only the fields NOT specified. - val annotation = c.getAnnotation(BindType::class.java) - - val fields: List = - if (annotation.only.isNotEmpty()) { - c.declaredFields - .filter { annotation.only.contains(it.name) } - .map { DestructedField(it.name, it.genericType) } - } else { - c.declaredFields - .filter { !annotation.ignore.contains(it.name) } - .map { DestructedField(it.name, it.genericType) } - } - - return DestructedClass( - name = c.simpleName, - fields = fields - ) - } } \ No newline at end of file diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index cc1b17f..ec7b7fb 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -1,22 +1,21 @@ import com.google.gson.Gson import org.levi.coffee.Ipc import org.levi.coffee.Window +import org.levi.coffee.annotations.BindAllMethods import org.levi.coffee.annotations.BindMethod import org.levi.coffee.annotations.BindType +import org.levi.coffee.annotations.IgnoreMethod import java.io.BufferedReader import java.io.File -@BindType( - only = [], - ignore = ["age"], -) +@BindType(ignore = ["age"]) class Person( val name: String = "", var age: Int = 0, val hobbies: List = emptyList(), val string: Map> = emptyMap(), ) { - + @BindMethod fun addTwoNumbers(a: Int, b: Int): Int { return a + b; } @@ -30,24 +29,17 @@ class Person( } fun main() { - val g = Gson() - val json = """{"name":"Yair"}""" - val p = g.fromJson(json, Person::class.java) - println(p.age) - println(p.name) - println(p.hobbies) - println(p.string) -// val win = Window() -// win.setSize(700, 700) -// win.setTitle("My first Javatron app!") -// -// win.setURL("http://localhost:5173") -// win.bind( -// Person(), -// ) -// -// win.addBeforeStartCallback { println("Started app...") } -// win.addOnCloseCallback { println("Closed the app!") } -// -// win.run() + val win = Window() + win.setSize(700, 700) + win.setTitle("My first Javatron app!") + + win.setURL("http://localhost:5173") + win.bind( + Person(), + ) + + win.addBeforeStartCallback { println("Started app...") } + win.addOnCloseCallback { println("Closed the app!") } + + win.run() }