-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #97 from DECENTfoundation/develop
Release 3.0.0
- Loading branch information
Showing
174 changed files
with
1,692 additions
and
1,363 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
plugins { | ||
kotlin("jvm") | ||
id(GradlePlugins.errorProne) | ||
application | ||
} | ||
|
||
application { | ||
mainClassName = "ch.decent.sdk.poet.MainKt" | ||
} | ||
|
||
dependencies { | ||
implementation(Libs.kotlin) | ||
implementation(Libs.kotlinReflect) | ||
implementation(Libs.kpoet) | ||
implementation("com.github.cretz.kastree:kastree-ast-psi:0.4.0") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.CodeBlock | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.TypeName | ||
|
||
data class ApiDescriptor( | ||
val packageSuffix: String, | ||
val createMethodName: String, | ||
val returnCodeBlock: String, | ||
val docBuilder: (String) -> String = { it }, | ||
val methodBuilder: FunSpec.Builder.(TypeName) -> Any = { returns(it) } | ||
) { | ||
val packageName = "$packageNameApi.$packageSuffix" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.AnnotationSpec | ||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.KModifier | ||
import com.squareup.kotlinpoet.PropertySpec | ||
import com.squareup.kotlinpoet.TypeSpec | ||
import kastree.ast.Node | ||
|
||
object Builders { | ||
|
||
private fun apiClassSpec(api: ApiDescriptor) = TypeSpec.classBuilder(apiRef.simpleName) | ||
.primaryConstructor(FunSpec.constructorBuilder() | ||
.addParameter(apiNameRef, apiRef) | ||
.build()) | ||
.superclass(ClassName(packageNameApi, "BaseCoreApi")) | ||
.addSuperclassConstructorParameter("$apiNameRef.core") | ||
.addProperties(apiClasses.map { ClassName(api.packageName, it.structured().name) }.map { | ||
val name = it.simpleName.decapitalize() | ||
PropertySpec.builder(name, it) | ||
.initializer("%T($apiNameRef.$name)", it) | ||
.build() | ||
}) | ||
.build() | ||
|
||
fun apiFile(api: ApiDescriptor) = apiClassSpec(api).let { FileSpec.builder(api.packageName, it.name!!).addType(it).build() } | ||
|
||
private fun apiServiceClasses(api: ApiDescriptor): List<TypeSpec> = apiClasses | ||
// .filter { it.structured().name == "AccountApi" } | ||
.map { | ||
val klass = ClassName(it.pkg!!.names.joinToString("."), it.structured().name) | ||
|
||
val apiProp = PropertySpec.builder(apiNameRef, klass, KModifier.PRIVATE) | ||
.initializer(apiNameRef) | ||
.build() | ||
|
||
val ctor = FunSpec.constructorBuilder() | ||
.addModifiers(KModifier.INTERNAL) | ||
.addParameter(apiProp.name, apiProp.type, KModifier.PRIVATE) | ||
.build() | ||
|
||
val apiDoc = DocReader.docs.getValue(klass.simpleName).toMutableList() | ||
val methods = it.structured().members | ||
.filterIsInstance<Node.Decl.Func>() | ||
.filterNot { it.hasKeyword(Node.Modifier.Keyword.PRIVATE) } | ||
.map { func -> | ||
val imports = it.imports.map { it.names.joinToString(".") } | ||
|
||
val builder = FunSpec.builder(func.name!!) | ||
.addCode("return $apiNameRef.%L(${func.paramNames()}).%L", func.name, api.returnCodeBlock) | ||
.addParameters(func.paramSpecs(imports)) | ||
|
||
if (func.typeParams.isNotEmpty()) builder.addTypeVariable(func.typeParams.single().typeName(imports)) | ||
val docs = DocReader.applyDocs(apiDoc, func, imports) | ||
docs?.let { builder.addKdoc(api.docBuilder(it)) } | ||
api.methodBuilder(builder, func.type!!.returnType(imports)) | ||
builder.addAnnotations(func.mods.filterIsInstance<Node.Modifier.AnnotationSet>().buildAnnotations(imports)) | ||
builder.build() | ||
} | ||
|
||
TypeSpec.classBuilder(klass) | ||
.addProperty(apiProp) | ||
.primaryConstructor(ctor) | ||
.addFunctions(methods) | ||
.build() | ||
} | ||
|
||
fun apiServiceFiles(api: ApiDescriptor) = apiServiceClasses(api).map { | ||
FileSpec.builder(api.packageName, it.name!!) | ||
.addAnnotation( | ||
AnnotationSpec.builder(Suppress::class) | ||
.addMember("%S", "TooManyFunctions") | ||
.addMember("%S", "LongParameterList") | ||
.build() | ||
) | ||
.addType(it) | ||
.build() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.CodeBlock | ||
import kastree.ast.Node | ||
import java.io.File | ||
|
||
object DocReader { | ||
private const val TOKEN_START = "/**" | ||
private const val TOKEN_END = "*/" | ||
private val REGEX_FUN = Regex("\\s*fun\\s(?:<.*?>\\s)?(\\w+)\\(.*") | ||
val REGEX_DOC_LINK = Regex("\\[\\w+]") | ||
|
||
data class FunDoc(val name: String, val docs: String) | ||
|
||
private fun File.docs(): List<FunDoc> { | ||
val funDocs = mutableListOf<FunDoc>() | ||
useLines { | ||
val iter = it.iterator() | ||
var hasDoc = false | ||
var acc = "" | ||
while (iter.hasNext()) { | ||
val line = iter.next().trim() | ||
if (line.startsWith(TOKEN_END)) hasDoc = false | ||
if (hasDoc) acc = acc + line.substringAfter("*").trim() + "\n" | ||
if (line.matches(REGEX_FUN)) { | ||
funDocs += FunDoc(REGEX_FUN.matchEntire(line)!!.groups[1]!!.value, acc) | ||
acc = "" | ||
} | ||
if (line.startsWith(TOKEN_START)) hasDoc = true | ||
} | ||
} | ||
return funDocs | ||
} | ||
|
||
val docs: Map<String, List<FunDoc>> | ||
get() = File(srcApi).listFiles()!! | ||
.associate { it.nameWithoutExtension to it.docs() } | ||
|
||
@Suppress("SpreadOperator") | ||
fun applyDocs(docs: MutableList<FunDoc>, f: Node.Decl.Func, i: List<String>) = | ||
docs.find { it.name == f.name }?.let { | ||
val args = mutableListOf<Any>() | ||
val withImports = it.docs.replace(REGEX_DOC_LINK) { | ||
it.value.drop(1).dropLast(1).let { | ||
if (it.first().isLowerCase()) { | ||
args.add(it.member(i)) | ||
"[%M]" | ||
} else { | ||
args.add(it.className(i)) | ||
"[%T]" | ||
} | ||
} | ||
} | ||
docs.remove(it) | ||
CodeBlock.of(withImports, *args.toTypedArray()).toString() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.ParameterSpec | ||
import com.squareup.kotlinpoet.TypeSpec | ||
import com.squareup.kotlinpoet.asClassName | ||
|
||
object Factory { | ||
|
||
private val docs = """ | ||
Create Api. At least one of HTTP or WebSocket URLs must be set. | ||
@param client OkHttp client handling the requests | ||
@param webSocketUrl URL for the webSocket requests, eg: wss://testnet-socket.dcore.io/ | ||
@param httpUrl URL for the HTTP requests, eg: https://testnet.dcore.io/ | ||
@param logger optional logger for requests | ||
@return DCore API for making requests | ||
""".trimIndent() | ||
|
||
private fun nullable(name: String, klass: ClassName) = | ||
ParameterSpec.builder(name, klass.copy(nullable = true)).defaultValue("null").build() | ||
|
||
val params = listOf( | ||
ParameterSpec.builder("client", ClassName.bestGuess("okhttp3.OkHttpClient")).build(), | ||
nullable("webSocketUrl", String::class.asClassName()), | ||
nullable("httpUrl", String::class.asClassName()), | ||
nullable("logger", ClassName.bestGuess("org.slf4j.Logger")) | ||
) | ||
|
||
fun builder(methodName: String, pckg: String) = FunSpec.builder(methodName) | ||
.addParameters(params) | ||
.addStatement("return %T(%T(%T(client, webSocketUrl, httpUrl, logger)))", ClassName(pckg, apiRef.simpleName), apiRef, clientRef) | ||
.addAnnotation(JvmStatic::class) | ||
.addKdoc(docs) | ||
.build() | ||
|
||
val rx = FunSpec.builder("createApiRx") | ||
.addParameters(params) | ||
.addStatement("return %T(%T(client, webSocketUrl, httpUrl, logger))", apiRef, clientRef) | ||
.addAnnotation(JvmStatic::class) | ||
.addKdoc(docs) | ||
.build() | ||
|
||
val file: FileSpec | ||
get() = FileSpec.builder("ch.decent.sdk", "DCoreSdk") | ||
.addType(TypeSpec.objectBuilder("DCoreSdk") | ||
.addFunction(rx) | ||
.apply { apis.forEach { addFunction(builder(it.createMethodName, it.packageName)) } } | ||
.build()) | ||
.build() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
@file:Suppress( | ||
"TooManyFunctions", | ||
"LongParameterList", | ||
"SpreadOperator", | ||
"ComplexMethod", | ||
"ThrowsCount" | ||
) | ||
|
||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.AnnotationSpec | ||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.CodeBlock | ||
import com.squareup.kotlinpoet.MemberName | ||
import com.squareup.kotlinpoet.ParameterSpec | ||
import com.squareup.kotlinpoet.ParameterizedTypeName | ||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
import com.squareup.kotlinpoet.TypeName | ||
import com.squareup.kotlinpoet.TypeVariableName | ||
import kastree.ast.Node | ||
|
||
fun Node.Expr.name() = (this as Node.Expr.Name).name | ||
fun Node.File.structured() = this.decls.single() as Node.Decl.Structured | ||
fun Node.WithModifiers.hasKeyword(keyword: Node.Modifier.Keyword) = this.mods.contains(Node.Modifier.Lit(keyword)) | ||
fun Node.Decl.Func.Param.isNullable() = this.type?.ref is Node.TypeRef.Nullable | ||
fun Node.Decl.Func.paramNames() = this.params.joinToString { it.name } | ||
|
||
fun Node.Decl.Func.paramSpecs(imports: List<String>) = this.params.map { p -> | ||
fun Node.Expr.defaultValue() = when (this) { | ||
is Node.Expr.Const -> CodeBlock.of(this.value) | ||
is Node.Expr.StringTmpl -> CodeBlock.of("%S", "") | ||
is Node.Expr.BinaryOp -> { | ||
when { | ||
this.lhs is Node.Expr.Name && this.lhs.name() == apiNameRef -> CodeBlock.of("$apiNameRef.${this.rhs.name()}") | ||
this.lhs is Node.Expr.Name -> CodeBlock.of("%T.${this.rhs.name()}", this.lhs.name().className(imports)) | ||
this.lhs is Node.Expr.BinaryOp -> (this.lhs as Node.Expr.BinaryOp) | ||
.let { CodeBlock.of("%T.${it.rhs.name()}.${this.rhs.name()}", it.lhs.name().className(imports)) } | ||
else -> throw IllegalStateException() | ||
} | ||
} | ||
is Node.Expr.Name -> CodeBlock.of("%M", this.name.member(imports)) | ||
is Node.Expr.Call -> { | ||
when { | ||
this.args.isEmpty() && this.expr.name().first().isLowerCase() -> CodeBlock.of("%M()", this.expr.name().member(imports)) | ||
this.args.isEmpty() -> CodeBlock.of("%T()", this.expr.name().className(imports)) | ||
else -> throw IllegalStateException(this.toString()) | ||
} | ||
} | ||
else -> throw IllegalStateException() | ||
} | ||
|
||
val builder = ParameterSpec.builder(p.name, p.type!!.typeName(imports).copy(nullable = p.isNullable())) | ||
p.default?.let { builder.defaultValue(it.defaultValue()) } | ||
return@map builder.build() | ||
} | ||
|
||
fun Node.TypeRef.Simple.fullName(imports: List<String>) = this.pieces.single().name.fullName(imports) | ||
fun Node.TypeRef.Simple.className(imports: List<String>) = ClassName.bestGuess(fullName(imports)) | ||
|
||
fun String.member(imports: List<String>) = MemberName(fullName(imports).substringBeforeLast("."), this) | ||
|
||
fun String.fullName(imports: List<String>) = when (this) { | ||
"Class" -> "java.lang.$this" | ||
"List", "Map", "emptyList" -> "kotlin.collections.$this" | ||
"Long", "Int", "Short", "Byte", "String", "Boolean", "Unit", "Any", "Deprecated" -> "kotlin.$this" | ||
"JvmOverloads", "JvmName" -> "kotlin.jvm.$this" | ||
else -> imports.find { it.substringAfterLast(".") == this }?.substringAfter("import ") | ||
} ?: this | ||
|
||
fun String.className(imports: List<String>) = ClassName.bestGuess(fullName(imports)) | ||
|
||
fun Node.Type.typeName(imports: List<String>): TypeName { | ||
fun recursive(r: Node.TypeRef?): TypeName { | ||
val type = when { | ||
r is Node.TypeRef.Simple && r.pieces.single().typeParams.isEmpty() -> r.className(imports) | ||
r is Node.TypeRef.Simple -> r.className(imports).parameterizedBy(*r.pieces.single().typeParams.map { recursive(it?.ref) }.toTypedArray()) | ||
r is Node.TypeRef.Nullable -> recursive(r.type) | ||
else -> throw IllegalStateException("unknown node: $r") | ||
} | ||
return if (type is ClassName && type.simpleName == "T") TypeVariableName("T") | ||
else type | ||
} | ||
|
||
return recursive(this.ref) | ||
} | ||
|
||
fun Node.Type.returnType(imports: List<String>): TypeName = | ||
(typeName(imports) as ParameterizedTypeName).typeArguments.single() | ||
|
||
fun Node.TypeParam.typeName(imports: List<String>): TypeVariableName = | ||
TypeVariableName(name, ClassName.bestGuess((type!! as Node.TypeRef.Simple).fullName(imports))) | ||
|
||
fun List<Node.Modifier.AnnotationSet>.buildAnnotations(imports: List<String>) = | ||
map { | ||
it.anns.single().let { | ||
val builder = AnnotationSpec.builder(it.names.single().className(imports)) | ||
it.args.singleOrNull()?.let { | ||
val str = ((it.expr as Node.Expr.StringTmpl).elems.single() as Node.Expr.StringTmpl.Elem.Regular).str | ||
builder.addMember("%S", str) | ||
} | ||
builder.build() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package ch.decent.sdk.poet | ||
|
||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.CodeBlock | ||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
import com.squareup.kotlinpoet.asClassName | ||
import kastree.ast.Node | ||
import kastree.ast.psi.Parser | ||
import java.io.File | ||
import java.util.concurrent.Future | ||
|
||
val out = File("../library/gen/main/java") | ||
val srcApi = "../library/src/main/java/ch/decent/sdk/api/rx" | ||
val packageName = "ch.decent.sdk" | ||
val packageNameApi = "$packageName.api" | ||
val apiNameRef = "api" | ||
val clientRef = ClassName.bestGuess("$packageName.DCoreClient") | ||
val apiRef = ClassName.bestGuess("$packageNameApi.rx.DCoreApi") | ||
|
||
val apiClasses = File(srcApi).listFiles()!!.map { Parser.parseFile(it.readText()) } | ||
.filterNot { it.structured().hasKeyword(Node.Modifier.Keyword.ABSTRACT) } | ||
.filterNot { it.structured().name == "CallbackApi" || it.structured().name == apiRef.simpleName } | ||
|
||
val apis = listOf( | ||
ApiDescriptor("blocking", "createApiBlocking", "blockingGet()"), | ||
ApiDescriptor("futures", "createApiFutures", "toFuture()") | ||
{ returns(Future::class.asClassName().parameterizedBy(it)) }, | ||
ApiDescriptor("callback", "createApi", "subscribeWith(callback)", | ||
{ it.substringBefore("@return").trimEnd() + "\n@param callback a callback object that asynchronously receives the result value or error " }, | ||
{ | ||
returns(ClassName.bestGuess("$packageNameApi.Cancelable"), CodeBlock.of("a request handler object which can be used to cancel the request")) | ||
addParameter("callback", ClassName.bestGuess("$packageNameApi.Callback").parameterizedBy(it)) | ||
}) | ||
) | ||
|
||
fun main() { | ||
// val path = System.out | ||
val path = out | ||
apis.forEach { | ||
Builders.apiFile(it).writeTo(path) | ||
Builders.apiServiceFiles(it).forEach { it.writeTo(path) } | ||
} | ||
Factory.file.writeTo(path) | ||
} | ||
|
||
fun Any?.print() = println(this.toString()) |
Oops, something went wrong.