Skip to content

Commit a88f37d

Browse files
committed
docs for type spec generator
1 parent c77b0d7 commit a88f37d

File tree

10 files changed

+168
-132
lines changed

10 files changed

+168
-132
lines changed

core/src/main/kotlin/com/fractalwrench/json2kotlin/ClassTypeHolder.kt

Lines changed: 0 additions & 97 deletions
This file was deleted.

core/src/main/kotlin/com/fractalwrench/json2kotlin/GsonBuildDelegate.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@ import com.google.gson.annotations.SerializedName
44
import com.squareup.kotlinpoet.AnnotationSpec
55
import com.squareup.kotlinpoet.PropertySpec
66
import com.squareup.kotlinpoet.TypeSpec
7-
// TODO docs
8-
class GsonBuildDelegate: SourceBuildDelegate {
97

10-
private val regex = "%".toRegex()
8+
/**
9+
* A build delegate which alters the generated source code to include GSON annotations
10+
*/
11+
class GsonBuildDelegate : SourceBuildDelegate {
1112

12-
// TODO should pass TypedJsonElement (and as much info as possible,
13-
// maybe in a wrapper class e.g. PropBuildParams/ClassBuildParams)
13+
private val regex = "%".toRegex()
1414

15-
override fun prepareClassProperty(propertyBuilder: PropertySpec.Builder,
16-
kotlinIdentifier: String,
17-
jsonKey: String?) {
18-
if (kotlinIdentifier != jsonKey && jsonKey != null) {
15+
override fun prepareProperty(propertyBuilder: PropertySpec.Builder,
16+
kotlinIdentifier: String,
17+
jsonKey: String,
18+
commonElements: List<TypedJsonElement>) {
19+
if (kotlinIdentifier != jsonKey) {
1920
val serializedNameBuilder = AnnotationSpec.builder(SerializedName::class)
2021
serializedNameBuilder.addMember("value=\"${jsonKey.replace(regex, "%%")}\"", "")
2122
propertyBuilder.addAnnotation(serializedNameBuilder.build())
2223
}
2324
}
2425

2526
override fun prepareClass(classBuilder: TypeSpec.Builder,
26-
kotlinIdentifier: String,
2727
jsonElement: TypedJsonElement) {
2828
}
2929

core/src/main/kotlin/com/fractalwrench/json2kotlin/JsonFieldGrouper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class JsonFieldGrouper(private val groupingStrategy: GroupingStrategy =
1212
* Recursively groups a List of JSONElementstogether by any commonality (i.e. whether they should be
1313
* represented by the same type)
1414
*/
15-
fun groupJsonObjects(jsonElements: MutableList<TypedJsonElement>): List<List<TypedJsonElement>> {
15+
fun groupCommonJsonObjects(jsonElements: MutableList<TypedJsonElement>): List<List<TypedJsonElement>> {
1616
val allTypes: MutableList<MutableList<TypedJsonElement>> = mutableListOf()
1717

1818
while (jsonElements.isNotEmpty()) {

core/src/main/kotlin/com/fractalwrench/json2kotlin/JsonNames.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.fractalwrench.json2kotlin
22

3-
// TODO docs
43
internal fun nameForArrayField(index: Int, identifier: String): String =
54
if (index == 0) identifier else "$identifier${index + 1}"
65

7-
// FIXME pattern compilation
8-
96
fun String.standardiseNewline(): String {
107
return this.replace("\r\n", "\n")
118
}

core/src/main/kotlin/com/fractalwrench/json2kotlin/JsonProcessor.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.fractalwrench.json2kotlin
22

33
import com.google.gson.JsonElement
4-
import com.squareup.kotlinpoet.*
5-
import java.util.HashMap
4+
import com.squareup.kotlinpoet.ParameterizedTypeName
5+
import com.squareup.kotlinpoet.TypeName
6+
import com.squareup.kotlinpoet.TypeSpec
7+
import com.squareup.kotlinpoet.asTypeName
8+
import java.util.*
69

710
// TODO docs
811
internal class JsonProcessor(private val typeDetector: JsonTypeDetector) { // TODO crappy name

core/src/main/kotlin/com/fractalwrench/json2kotlin/JsonTypeDetector.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.google.gson.JsonElement
55
import com.google.gson.JsonObject
66
import com.google.gson.JsonPrimitive
77
import com.squareup.kotlinpoet.*
8-
import java.util.HashSet
8+
import java.util.*
99

1010
/**
1111
* Deduces the TypeName for a JSON field value. For a primitive such as a String, this is a simple operation.
@@ -46,16 +46,16 @@ internal class JsonTypeDetector {
4646
}
4747

4848
private fun typeForJsonObject(jsonObject: JsonObject,
49-
key: String,
50-
jsonElementMap: Map<JsonElement, TypeSpec>): TypeName {
49+
key: String,
50+
jsonElementMap: Map<JsonElement, TypeSpec>): TypeName {
5151
val existingTypeName = jsonElementMap[jsonObject]
5252
val identifier = existingTypeName?.name ?: key.toKotlinIdentifier().capitalize()
5353
return ClassName.bestGuess(identifier)
5454
}
5555

5656
private fun typeForJsonArray(jsonArray: JsonArray,
57-
key: String,
58-
jsonElementMap: Map<JsonElement, TypeSpec>): TypeName {
57+
key: String,
58+
jsonElementMap: Map<JsonElement, TypeSpec>): TypeName {
5959
val pair = findAllArrayTypes(jsonArray, key, jsonElementMap)
6060
val arrayTypes = pair.first
6161
val nullable = pair.second

core/src/main/kotlin/com/fractalwrench/json2kotlin/Kotlin2JsonConverter.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fractalwrench.json2kotlin
22

3-
import com.google.gson.*
3+
import com.google.gson.JsonParser
4+
import com.google.gson.JsonSyntaxException
45
import java.io.InputStream
56
import java.io.OutputStream
67

@@ -11,19 +12,18 @@ class Kotlin2JsonConverter(private val buildDelegate: SourceBuildDelegate = Gson
1112

1213
private val jsonReader = JsonReader(JsonParser())
1314
private val sourceFileWriter = SourceFileWriter()
14-
private val traverser = ReverseJsonTreeTraverser()
15+
private val treeTraverser = ReverseJsonTreeTraverser()
1516

1617
/**
1718
* Converts an InputStream of JSON to Kotlin source code, writing the result to the OutputStream.
1819
*/
1920
fun convert(input: InputStream, output: OutputStream, args: ConversionArgs) {
2021
try {
2122
val jsonRoot = jsonReader.readJsonTree(input, args)
22-
val stack = traverser.traverse(jsonRoot, args.rootClassName)
23-
val typeHolder = ClassTypeHolder(buildDelegate, ::defaultGroupingStrategy)
24-
typeHolder.processQueue(stack)
25-
26-
sourceFileWriter.writeSourceFile(typeHolder.stack, args, output)
23+
val jsonStack = treeTraverser.traverse(jsonRoot, args.rootClassName)
24+
val generator = TypeSpecGenerator(buildDelegate, ::defaultGroupingStrategy)
25+
val typeSpecs = generator.generateTypeSpecs(jsonStack)
26+
sourceFileWriter.writeSourceFile(typeSpecs, args, output)
2727
} catch (e: JsonSyntaxException) {
2828
throw IllegalArgumentException("Invalid JSON supplied", e)
2929
}

core/src/main/kotlin/com/fractalwrench/json2kotlin/SourceBuildDelegate.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@ package com.fractalwrench.json2kotlin
33
import com.squareup.kotlinpoet.PropertySpec
44
import com.squareup.kotlinpoet.TypeSpec
55

6-
// TODO docs
7-
interface SourceBuildDelegate { // FIXME ensure names are correct, as this will be public API!
8-
fun prepareClassProperty(propertyBuilder: PropertySpec.Builder,
9-
kotlinIdentifier: String,
10-
jsonKey: String?)
6+
/**
7+
* Implementations of this interface will receive callbacks whenever a class or property is
8+
* about to be constructed, allowing modification of the generated source code.
9+
*/
10+
interface SourceBuildDelegate {
1111

12+
/**
13+
* Provides access to a PropertyBuilder and common TypedJsonElements at the point of construction,
14+
* allowing for modifications before it is turned into a TypeSpec.
15+
*/
16+
fun prepareProperty(propertyBuilder: PropertySpec.Builder,
17+
kotlinIdentifier: String,
18+
jsonKey: String,
19+
commonElements: List<TypedJsonElement>)
20+
21+
/**
22+
* Provides access to a ClassBuilder and TypedJsonElement at the point of construction,
23+
* allowing for modifications before it is turned into a TypeSpec.
24+
*/
1225
fun prepareClass(classBuilder: TypeSpec.Builder,
13-
kotlinIdentifier: String,
1426
jsonElement: TypedJsonElement)
27+
1528
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.fractalwrench.json2kotlin
2+
3+
import com.squareup.kotlinpoet.*
4+
import java.util.*
5+
6+
/**
7+
* Generates TypeSpec objects from a TypedJsonElement stack, which is processed in BFS reverse level order.
8+
*/
9+
internal class TypeSpecGenerator(private val delegate: SourceBuildDelegate,
10+
groupingStrategy: GroupingStrategy) {
11+
12+
private val jsonProcessor = JsonProcessor(JsonTypeDetector())
13+
private val jsonFieldGrouper = JsonFieldGrouper(groupingStrategy)
14+
15+
/**
16+
* Processes JSON nodes in a reverse level order traversal,
17+
* by building class types for each level of the tree.
18+
*
19+
* The class types are returned as a Stack, sorted by level then alphabetically.
20+
*/
21+
fun generateTypeSpecs(bfsStack: Stack<TypedJsonElement>): Stack<TypeSpec> {
22+
val typeSpecs = Stack<TypeSpec>()
23+
var level = -1
24+
val levelQueue = LinkedList<TypedJsonElement>()
25+
26+
while (bfsStack.isNotEmpty()) {
27+
val pop = bfsStack.pop()
28+
29+
if (level != -1 && pop.level != level) {
30+
processTreeLevel(levelQueue, typeSpecs)
31+
}
32+
levelQueue.add(pop)
33+
level = pop.level
34+
}
35+
processTreeLevel(levelQueue, typeSpecs)
36+
return typeSpecs
37+
}
38+
39+
/**
40+
* Processes all the elements which belong to a single level in the tree. Each element is converted to a type,
41+
* sorted, then pushed to the stack.
42+
*/
43+
private fun processTreeLevel(levelQueue: LinkedList<TypedJsonElement>, stack: Stack<TypeSpec>) {
44+
val fieldValues = levelQueue.filter { it.isJsonObject }.toMutableList()
45+
46+
jsonFieldGrouper.groupCommonJsonObjects(fieldValues)
47+
.flatMap { convertJsonObjectsToTypes(it) }
48+
.sortedByDescending { it.name }
49+
.forEach { stack += it }
50+
levelQueue.clear()
51+
}
52+
53+
/**
54+
* Converts a List of JSON elements which share common fields into a Kotlin type. Properties
55+
* are sorted alphabetically.
56+
*/
57+
private fun convertJsonObjectsToTypes(commonElements: List<TypedJsonElement>): List<TypeSpec> {
58+
val fields = HashSet<String>()
59+
60+
commonElements.forEach {
61+
fields.addAll(it.asJsonObject.keySet())
62+
}
63+
64+
commonElements.sortedBy {
65+
it.kotlinIdentifier.toLowerCase()
66+
}
67+
68+
val classType = buildClass(commonElements, fields.sortedBy {
69+
it.toKotlinIdentifier().toLowerCase()
70+
}).build()
71+
72+
// FIXME encapsulation broken from this point
73+
return commonElements.filterNot {
74+
// reuse any types which already exist in the map
75+
val containsValue = jsonProcessor.jsonElementMap.containsValue(classType)
76+
jsonProcessor.jsonElementMap.put(it.jsonElement, classType)
77+
containsValue
78+
}.map { classType }
79+
}
80+
81+
/**
82+
* Builds a Kotlin class from a JSON element.
83+
*/
84+
private fun buildClass(commonElements: List<TypedJsonElement>, fields: Collection<String>): TypeSpec.Builder {
85+
val identifier = commonElements.last().kotlinIdentifier
86+
val classBuilder = TypeSpec.classBuilder(identifier.capitalize())
87+
val constructor = FunSpec.constructorBuilder()
88+
89+
if (fields.isNotEmpty()) {
90+
val fieldTypeMap = jsonProcessor.findDistinctTypesForFields(fields, commonElements)
91+
fields.forEach {
92+
buildProperty(it, fieldTypeMap, commonElements, classBuilder, constructor)
93+
}
94+
classBuilder.addModifiers(KModifier.DATA) // non-empty classes allow data modifier
95+
classBuilder.primaryConstructor(constructor.build())
96+
}
97+
98+
delegate.prepareClass(classBuilder, commonElements.last())
99+
return classBuilder
100+
}
101+
102+
/**
103+
* Builds a Kotlin property from a JSON element.
104+
*/
105+
private fun buildProperty(fieldKey: String,
106+
fieldTypeMap: Map<String, TypeName>,
107+
commonElements: List<TypedJsonElement>,
108+
classBuilder: TypeSpec.Builder,
109+
constructor: FunSpec.Builder) {
110+
111+
val kotlinIdentifier = fieldKey.toKotlinIdentifier()
112+
val typeName = fieldTypeMap[fieldKey]
113+
val initializer =
114+
PropertySpec.builder(kotlinIdentifier, typeName!!).initializer(kotlinIdentifier)
115+
delegate.prepareProperty(initializer, kotlinIdentifier, fieldKey, commonElements)
116+
classBuilder.addProperty(initializer.build())
117+
constructor.addParameter(kotlinIdentifier, typeName)
118+
}
119+
120+
}

spring/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ dependencies {
3333
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
3434
testImplementation('org.springframework.boot:spring-boot-starter-test')
3535
implementation 'com.google.code.gson:gson:2.8.2' // FIXME
36-
implementation 'com.squareup:kotlinpoet:0.6.0'
36+
implementation 'com.squareup:kotlinpoet:0.7.0'
3737
}

0 commit comments

Comments
 (0)