diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6218ee96..6d493e09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -230,11 +230,12 @@ jobs: gpg-passphrase: GPG_PASSPHRASE - name: Run run: | - ./gradlew :src:compiler:core:publish - ./gradlew :src:compiler:lib:publish - ./gradlew :src:converter:openapi:publish - ./gradlew :src:plugin:gradle:publish - ./gradlew :src:plugin:maven:publish + ./gradlew :src:compiler:core:publish \ + :src:compiler:lib:publish \ + :src:converter:openapi:publish \ + :src:plugin:gradle:publish \ + :src:plugin:maven:publish \ + :src:integration:jackson:publish release-lib-npm: diff --git a/README.md b/README.md index c58e0731..6eacf936 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,16 @@ wirespec compile ./todo.ws -o ./tmp -l Kotlin * Maven * Gradle +## Extentions * IntelliJ IDEA * Visual Studio Code +## Integration +Wirespec offers integration libraries with differ libraries. + +* [Jackson](src/integration/jackson) + # CLI ## Install diff --git a/settings.gradle.kts b/settings.gradle.kts index aa49b914..674d5c9c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,4 +38,6 @@ include( "src:plugin:maven", "src:plugin:gradle", "src:converter:openapi", + "src:integration:jackson", + "src:integration:wirespec", ) diff --git a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt index b1952ae8..b485896b 100644 --- a/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt +++ b/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt @@ -26,7 +26,7 @@ class JavaEmitter( | |public interface Wirespec { |${SPACER}interface Enum {}; - |${SPACER}interface Refined { String value(); }; + |${SPACER}interface Refined { String getValue(); }; |${SPACER}interface Endpoint {}; |${SPACER}enum Method { GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH, TRACE }; |${SPACER}record Content (String type, T body) {}; @@ -159,6 +159,8 @@ class JavaEmitter( |${SPACER}static boolean validate($name record) { |${SPACER}${validator.emit()} |${SPACER}} + |${SPACER}@Override + |${SPACER}public String getValue() { return value; } |} |""".trimMargin() } @@ -316,7 +318,7 @@ class JavaEmitter( fun String.sanitizeSymbol() = replace(".", "").replace(" ", "_") companion object { - private val reservedKeywords = listOf( + val reservedKeywords = listOf( "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", diff --git a/src/compiler/core/src/commonTest/kotlin/community/flock/wirespec/compiler/core/CompileRefinedTest.kt b/src/compiler/core/src/commonTest/kotlin/community/flock/wirespec/compiler/core/CompileRefinedTest.kt index bdc3f9c7..2b6b6b13 100644 --- a/src/compiler/core/src/commonTest/kotlin/community/flock/wirespec/compiler/core/CompileRefinedTest.kt +++ b/src/compiler/core/src/commonTest/kotlin/community/flock/wirespec/compiler/core/CompileRefinedTest.kt @@ -46,6 +46,8 @@ class CompileRefinedTest { static boolean validate(TodoId record) { return java.util.regex.Pattern.compile("^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}${'$'}").matcher(record.value).find(); } + @Override + public String getValue() { return value; } } """.trimIndent() diff --git a/src/integration/jackson/README.md b/src/integration/jackson/README.md new file mode 100644 index 00000000..e7cf7131 --- /dev/null +++ b/src/integration/jackson/README.md @@ -0,0 +1,96 @@ +# Jackson integration lib + +This library exposes a jackson module which adds specific serializers and deserializers to handel Wirespec refined and enum types. For more details about Jackson see: https://github.com/FasterXML/jackson + +## Usage +```xml + + community.flock.wirespec.integration + jackson + {VERSION} + +``` + +Register the Wirespec module + +```java +ObjectMapper objectMapper = new ObjectMapper() + .registerModules(new WirespecModule()); +``` + +## Docs + +### Refined +The wirespec Java and Kotlin emitter add an extra wrapper class for refined types. When objects are serialized wrapper class becomes visible in json representation. + +```java + +record TodoId(Sring value){} +record Todo(TodoId id, String name, boolean done){} +``` +When serialized to json with the default object mapper this wil result in the following output + +```json +{ + "id": { + "value": "123" + }, + "name": "My todo", + "done": true +} +``` +The Jackson module corrects this and flattens the output of the refined types + +```json +{ + "id": "123", + "name": "My todo", + "done": true +} +``` + +### Enum + +For Java and Kotlin some values are sanitized because the compiler does not except certain keywords. Wirespec emits an extra label with the original value for every enum. The toString method is overwritten and returns the orignal value. This module uses the toString method to serialize and deserialize enum values + +```wirespec +enum MyEnum { + true, false +} +``` + +The java emitter will generate the following enum class. The value true and false will be escaped because these are reserved keywords. + +```java +public enum MyEnum implements Wirespec.Enum { + _true("true"), + _false("false"); + + public final String label; + + MyEnum(String label) { + this.label = label; + } + + @Override + public String toString() { + return this.label; + } +} +``` + +### Reserved keywords +In java reserved keywords cannot be used as field name. The Wirespec [JavaEmitter](https://github.com/flock-community/wirespec/blob/master/src/compiler/core/src/commonMain/kotlin/community/flock/wirespec/compiler/core/emit/JavaEmitter.kt#L314) prefixes the fields with a `_`. The Jackson Module corrects this with a NamingStrategy that removes the `_` only for java record types + +```wirespec +type MyType { + final: Boolean +} +``` + +```java +public record MyType ( String _final){} +``` + +## Generate test classes +To test this module test classes are generated from a Wirespec specification. To regenerate the test classes run the following test [GenerateTestClasses.kt](src%2FjvmTest%2Fkotlin%2Fcommunity%2Fflock%2Fwirespec%2Fintegration%2Fjackson%2Fkotlin%2FGenerateTestClasses.kt) diff --git a/src/integration/jackson/build.gradle.kts b/src/integration/jackson/build.gradle.kts new file mode 100644 index 00000000..ed7c3b4a --- /dev/null +++ b/src/integration/jackson/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + kotlin("multiplatform") + kotlin("jvm") apply false + id("com.github.johnrengelman.shadow") apply false + id("com.goncalossilva.resources") version "0.4.0" +} + +group = "${Settings.GROUP_ID}.integration" +version = Settings.version + +repositories { + mavenCentral() + maven(uri("https://s01.oss.sonatype.org/service/local/repo_groups/public/content")) +} + +kotlin { + jvm { + withJava() + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + } + sourceSets { + commonMain { + dependencies { + compileOnly(project(":src:integration:wirespec")) + } + } + commonTest { + dependencies { + implementation(project(":src:integration:wirespec")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(kotlin("test-junit")) + } + } + val jvmMain by getting { + dependencies { + implementation(project(":src:compiler:core")) + compileOnly("com.fasterxml.jackson.core:jackson-databind:2.16.1") + } + } + val jvmTest by getting { + dependencies { + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2") + } + } + } +} diff --git a/src/integration/jackson/src/commonTest/resources/wirespec/todos.ws b/src/integration/jackson/src/commonTest/resources/wirespec/todos.ws new file mode 100644 index 00000000..e3d0dcae --- /dev/null +++ b/src/integration/jackson/src/commonTest/resources/wirespec/todos.ws @@ -0,0 +1,23 @@ +refined TodoId /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/g + +type Todo { + id: TodoId, + name: String, + final: Boolean, + category: TodoCategory +} + +type TodoInput { + name: String, + done: Boolean +} + +type Error { + code: String, + description: String +} + +enum TodoCategory { + WORK, + LIFE +} diff --git a/src/integration/jackson/src/jvmMain/kotlin/community/flock/wirespec/integration/jackson/WirespecModule.kt b/src/integration/jackson/src/jvmMain/kotlin/community/flock/wirespec/integration/jackson/WirespecModule.kt new file mode 100644 index 00000000..0446897c --- /dev/null +++ b/src/integration/jackson/src/jvmMain/kotlin/community/flock/wirespec/integration/jackson/WirespecModule.kt @@ -0,0 +1,186 @@ +package community.flock.wirespec.integration.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.BeanDescription +import com.fasterxml.jackson.databind.DeserializationConfig +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.cfg.MapperConfig +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod +import com.fasterxml.jackson.databind.introspect.AnnotatedParameter +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import community.flock.wirespec.Wirespec +import community.flock.wirespec.compiler.core.emit.JavaEmitter +import java.io.IOException + + +/** + * A Jackson module that handles deserialization of all Wirespec.Refined, to ensure + * collapse / expanse of the wrapper class around the string value. + * + * Example + * ```kt + * data class Id(value: String): Wirespec.Refined + * data class Task(id: Id, title: String) + * ``` + * + * Having an object such as + * ``` + * Task{id: Id("123"), title: "improve API contracts"} + * ``` + * will serialise to: + * ```json + * {id:"123", title: "improve API contracts"} + * ``` + * flattening the Wirespec.Refined as a String. Conversely, such JSON will deserialize back + * into the original `Task`, expanding the `id` field into a type safe Id data class. + * + * @see Wirespec.Refined + */ +class WirespecModule : SimpleModule() { + + override fun getModuleName(): String = "Wirespec Jackson Module" + + init { + addSerializer(Wirespec.Refined::class.java, RefinedSerializer()) + addSerializer(Wirespec.Enum::class.java, EnumSerializer()) + setDeserializerModifier(WirespecDeserializerModifier()) + setNamingStrategy(JavaReservedKeywordNamingStrategy()) + } +} + +/** + * Serializer that flattens any Wirespec.Refined wrapped String value during serialization. + * + * @see Wirespec.Refined + * @see WirespecModule + */ +private class RefinedSerializer(x: Class? = null) : StdSerializer(x) { + + override fun serialize(value: Wirespec.Refined, gen: JsonGenerator, provider: SerializerProvider) { + return gen.writeString(value.value) + } +} + +/** + * Serializer Wirespec.Enum classes. + * + * @see Wirespec.Enum + * @see WirespecModule + */ +private class EnumSerializer(x: Class? = null) : StdSerializer(x) { + + override fun serialize(value: Wirespec.Enum, gen: JsonGenerator, provider: SerializerProvider) { + return gen.writeString(value.toString()) + } +} + +/** + * Deserializer Wirespec.Refined classes. + * + * @see Wirespec.Refined + * @see WirespecModule + */ +class RefinedDeserializer(val vc: Class<*>) : StdDeserializer(vc) { + @Throws(IOException::class, JsonProcessingException::class) + override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Wirespec.Refined { + val node = jp.codec.readTree(jp) + return vc.declaredConstructors.first().newInstance(node.asText()) as Wirespec.Refined + } +} + +/** + * Deserializer Wirespec.Enum classes. + * + * @see Wirespec.Enum + * @see WirespecModule + */ +class EnumDeserializer(val vc: Class<*>) : StdDeserializer>(vc) { + @Throws(IOException::class, JsonProcessingException::class) + override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Enum<*> { + val node = jp.codec.readTree(jp) + val enum = vc.enumConstants.find { + val toString = it.javaClass.getDeclaredMethod("toString") + toString.invoke(it) == node.asText() + } + return enum as Enum<*> + } +} + +/** + * Jackson modifier intercept the deserialization of Wirespec.Enum and Wirespec.Refined and modifies the deserializer + * + * @see Wirespec.Enum + * @see WirespecModule + */ +private class WirespecDeserializerModifier : BeanDeserializerModifier() { + override fun modifyEnumDeserializer( + config: DeserializationConfig, + type: JavaType, + beanDesc: BeanDescription, + deserializer: JsonDeserializer<*> + ): JsonDeserializer<*> { + if (Wirespec.Enum::class.java.isAssignableFrom(beanDesc.beanClass)) { + return super.modifyDeserializer(config, beanDesc, EnumDeserializer(beanDesc.beanClass)) + } + return super.modifyEnumDeserializer(config, type, beanDesc, deserializer) + } + + override fun modifyDeserializer( + config: DeserializationConfig, + beanDesc: BeanDescription, + deserializer: JsonDeserializer<*> + ): JsonDeserializer<*> { + if (Wirespec.Refined::class.java.isAssignableFrom(beanDesc.beanClass)) { + return super.modifyDeserializer(config, beanDesc, RefinedDeserializer(beanDesc.beanClass)) + } + return super.modifyDeserializer(config, beanDesc, deserializer) + } +} + +internal class JavaReservedKeywordNamingStrategy : PropertyNamingStrategy() { + + override fun nameForGetterMethod(config: MapperConfig<*>, method: AnnotatedMethod, defaultName: String): String { + if (Record::class.java.isAssignableFrom(method.declaringClass)) { + return translate(defaultName) + } + return defaultName + } + + override fun nameForSetterMethod(config: MapperConfig<*>, method: AnnotatedMethod, defaultName: String): String { + if (Record::class.java.isAssignableFrom(method.declaringClass)) { + return translate(defaultName) + } + return defaultName + } + + override fun nameForConstructorParameter( + config: MapperConfig<*>, + ctorParam: AnnotatedParameter, + defaultName: String + ): String { + if (Record::class.java.isAssignableFrom(ctorParam.owner.rawType)) { + return translate(defaultName) + } + return defaultName + } + + private fun translate(property: String): String { + val keywords = JavaEmitter.reservedKeywords.map { "_$it" } + if (keywords.contains(property)) { + return property.drop(1) + } else { + return property + } + } +} + diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/WirespecModuleJavaTest.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/WirespecModuleJavaTest.java new file mode 100644 index 00000000..96e8f577 --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/WirespecModuleJavaTest.java @@ -0,0 +1,38 @@ +package community.flock.wirespec.integration.jackson.java; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import community.flock.wirespec.integration.jackson.WirespecModule; +import community.flock.wirespec.integration.jackson.java.generated.Todo; +import community.flock.wirespec.integration.jackson.java.generated.TodoCategory; +import community.flock.wirespec.integration.jackson.java.generated.TodoId; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class WirespecModuleJavaTest { + + Todo todo = new Todo( + new TodoId("123"), + "Do It now", + false, + TodoCategory.WORK + ); + + String json = "{\"id\":\"123\",\"name\":\"Do It now\",\"final\":false,\"category\":\"WORK\"}"; + + ObjectMapper objectMapper = new ObjectMapper() + .registerModules(new WirespecModule()); + + @Test + public void serializeJavaRefined() throws JsonProcessingException { + var res = objectMapper.writeValueAsString(todo); + assertEquals(json, res); + } + + @Test + public void deserializeJavaRefined() throws JsonProcessingException { + var res = objectMapper.readValue(json, Todo.class); + assertEquals(todo, res); + } +} \ No newline at end of file diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Error.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Error.java new file mode 100644 index 00000000..d867061e --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Error.java @@ -0,0 +1,8 @@ +package community.flock.wirespec.integration.jackson.java.generated; + +import community.flock.wirespec.Wirespec; + +public record Error( + String code, + String description +) {}; diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Todo.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Todo.java new file mode 100644 index 00000000..bca9182a --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/Todo.java @@ -0,0 +1,10 @@ +package community.flock.wirespec.integration.jackson.java.generated; + +import community.flock.wirespec.Wirespec; + +public record Todo( + TodoId id, + String name, + Boolean _final, + TodoCategory category +) {}; diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoCategory.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoCategory.java new file mode 100644 index 00000000..003ae6a8 --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoCategory.java @@ -0,0 +1,16 @@ +package community.flock.wirespec.integration.jackson.java.generated; + +import community.flock.wirespec.Wirespec; + +public enum TodoCategory implements Wirespec.Enum { + WORK("WORK"), + LIFE("LIFE"); + public final String label; + TodoCategory(String label) { + this.label = label; + } + @Override + public String toString() { + return label; + } +} diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoId.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoId.java new file mode 100644 index 00000000..9e7fb35d --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoId.java @@ -0,0 +1,11 @@ +package community.flock.wirespec.integration.jackson.java.generated; + +import community.flock.wirespec.Wirespec; + +public record TodoId (String value) implements Wirespec.Refined { + static boolean validate(TodoId record) { + return java.util.regex.Pattern.compile("^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$").matcher(record.value).find(); + } + @Override + public String getValue() { return value; } +} diff --git a/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoInput.java b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoInput.java new file mode 100644 index 00000000..f92657eb --- /dev/null +++ b/src/integration/jackson/src/jvmTest/java/community/flock/wirespec/integration/jackson/java/generated/TodoInput.java @@ -0,0 +1,8 @@ +package community.flock.wirespec.integration.jackson.java.generated; + +import community.flock.wirespec.Wirespec; + +public record TodoInput( + String name, + Boolean done +) {}; diff --git a/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/GenerateTestClasses.kt b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/GenerateTestClasses.kt new file mode 100644 index 00000000..afd7412e --- /dev/null +++ b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/GenerateTestClasses.kt @@ -0,0 +1,45 @@ +package community.flock.wirespec.integration.jackson.kotlin + +import arrow.core.getOrElse +import community.flock.wirespec.compiler.core.Wirespec +import community.flock.wirespec.compiler.core.emit.JavaEmitter +import community.flock.wirespec.compiler.core.emit.KotlinEmitter +import community.flock.wirespec.compiler.core.parse +import community.flock.wirespec.compiler.utils.noLogger +import java.io.File +import kotlin.test.Test + +class GenerateTestClasses { + + val basePkg = "community.flock.wirespec.integration.jackson" + val javaPkg = "${basePkg}.java.generated" + val kotlinPkg = "${basePkg}.kotlin.generated" + + val javaEmitter = JavaEmitter(javaPkg) + val kotlinEmitter = KotlinEmitter(kotlinPkg) + + fun pkgToPath(pkg: String) = pkg.split(".").joinToString("/") + + val baseDir = File("src/jvmTest") + val javaDir = baseDir.resolve("java").resolve(pkgToPath(javaPkg)) + val kotlinDir = baseDir.resolve("kotlin").resolve(pkgToPath(kotlinPkg)) + + @Test + fun generate(){ + val todoFile = File("src/commonTest/resources/wirespec/todos.ws").readText() + val ast = Wirespec.parse(todoFile)(noLogger).fold ({e -> error("Cannot parse wirespec: ${e.first().message}") }, { it }) + val emittedJava = javaEmitter.emit(ast) + val emittedKotlin = kotlinEmitter.emit(ast) + + javaDir.mkdirs() + emittedJava.forEach { + javaDir.resolve("${it.typeName}.java").writeText(it.result) + } + + kotlinDir.mkdirs() + emittedKotlin.forEach { + kotlinDir.resolve("Todo.kt").writeText(it.result) + } + } +} + diff --git a/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/WirespecModuleKotlinTest.kt b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/WirespecModuleKotlinTest.kt new file mode 100644 index 00000000..72ab38d6 --- /dev/null +++ b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/WirespecModuleKotlinTest.kt @@ -0,0 +1,39 @@ +package community.flock.wirespec.integration.jackson.kotlin + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import community.flock.wirespec.integration.jackson.WirespecModule +import community.flock.wirespec.integration.jackson.kotlin.generated.Todo +import community.flock.wirespec.integration.jackson.kotlin.generated.TodoCategory +import community.flock.wirespec.integration.jackson.kotlin.generated.TodoId +import kotlin.test.Test +import kotlin.test.assertEquals + +class WirespecModuleKotlinTest { + + val todo = Todo( + id = TodoId("123"), + name = "Do It now", + final = false, + category = TodoCategory.LIFE + ) + + val json = "{\"id\":\"123\",\"name\":\"Do It now\",\"final\":false,\"category\":\"LIFE\"}" + + val objectMapper = ObjectMapper() + .registerKotlinModule() + .registerModules(WirespecModule()) + + @Test + fun serializeRefined(){ + val res = objectMapper.writeValueAsString(todo) + assertEquals(json, res) + } + + @Test + fun deserializeRefined(){ + val res = objectMapper.readValue(json) + assertEquals(todo, res) + } +} \ No newline at end of file diff --git a/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/generated/Todo.kt b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/generated/Todo.kt new file mode 100644 index 00000000..d0861b77 --- /dev/null +++ b/src/integration/jackson/src/jvmTest/kotlin/community/flock/wirespec/integration/jackson/kotlin/generated/Todo.kt @@ -0,0 +1,32 @@ +package community.flock.wirespec.integration.jackson.kotlin.generated + +import community.flock.wirespec.Wirespec + +data class TodoId(override val value: String): Wirespec.Refined +fun TodoId.validate() = Regex("^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$").matches(value) + +data class Todo( + val id: TodoId, + val name: String, + val final: Boolean, + val category: TodoCategory +) + +data class TodoInput( + val name: String, + val done: Boolean +) + +data class Error( + val code: String, + val description: String +) + +enum class TodoCategory (val label: String): Wirespec.Enum { + WORK("WORK"), + LIFE("LIFE"); + + override fun toString(): String { + return label + } +} diff --git a/src/integration/wirespec/build.gradle.kts b/src/integration/wirespec/build.gradle.kts new file mode 100644 index 00000000..cf27963b --- /dev/null +++ b/src/integration/wirespec/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + kotlin("multiplatform") + kotlin("jvm") apply false + id("com.github.johnrengelman.shadow") apply false +} + +group = "${Settings.GROUP_ID}.integration" +version = Settings.version + +repositories { + mavenCentral() + maven(uri("https://s01.oss.sonatype.org/service/local/repo_groups/public/content")) +} + +kotlin { + jvm { + withJava() + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + } + sourceSets { + commonMain { + dependencies { + + } + } + commonTest { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(kotlin("test-junit")) + } + } + val jvmMain by getting { + dependencies { + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.8") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2") + } + } + } +} diff --git a/src/integration/wirespec/src/jvmMain/kotlin/flock/community/wirespec/Wirespec.kt b/src/integration/wirespec/src/jvmMain/kotlin/flock/community/wirespec/Wirespec.kt new file mode 100644 index 00000000..62e53888 --- /dev/null +++ b/src/integration/wirespec/src/jvmMain/kotlin/flock/community/wirespec/Wirespec.kt @@ -0,0 +1,26 @@ +package community.flock.wirespec + +import java.lang.reflect.Type +import java.lang.reflect.ParameterizedType + +object Wirespec { + interface Enum + interface Endpoint + interface Refined { val value: String } + enum class Method { GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH, TRACE } + @JvmRecord data class Content (val type:String, val body:T ) + interface Request { val path:String; val method: Method; val query: Map>; val headers: Map>; val content:Content? } + interface Response { val status:Int; val headers: Map>; val content:Content? } + interface ContentMapper { fun read(content: Content, valueType: Type): Content fun write(content: Content): Content } + @JvmStatic fun getType(type: Class<*>, isIterable: Boolean): Type { + return if (isIterable) { + object : ParameterizedType { + override fun getRawType() = MutableList::class.java + override fun getActualTypeArguments() = arrayOf(type) + override fun getOwnerType() = null + } + } else { + type + } + } +}