Skip to content

Commit

Permalink
Merge pull request #97 from DECENTfoundation/develop
Browse files Browse the repository at this point in the history
Release 3.0.0
  • Loading branch information
petervanderkadecent authored Aug 19, 2019
2 parents 2b2fce2 + 66427b4 commit f4dd6ad
Show file tree
Hide file tree
Showing 174 changed files with 1,692 additions and 1,363 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ services: docker
env:
- DOCKER_COMPOSE_VERSION=1.23.2

before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/

cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/

before_install:
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
Expand Down
16 changes: 16 additions & 0 deletions apiGen/build.gradle.kts
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")
}
15 changes: 15 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/ApiDescriptor.kt
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"
}
81 changes: 81 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/Builders.kt
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()
}
}
57 changes: 57 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/DocReader.kt
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()
}
}
54 changes: 54 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/Factory.kt
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()
}
103 changes: 103 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/KastreeExt.kt
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()
}
}
46 changes: 46 additions & 0 deletions apiGen/src/main/java/ch/decent/sdk/poet/Main.kt
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())
Loading

0 comments on commit f4dd6ad

Please sign in to comment.