From 6b24beeef3b2d582aa0e7df7e94fa6323d49cd25 Mon Sep 17 00:00:00 2001 From: Jon Peterson Date: Mon, 1 Aug 2016 00:22:29 -0400 Subject: [PATCH] Added README doc, updated build, and added licensing. --- README.md | 128 ++++++++++++++++- build.gradle | 130 +++++++++++++++++- .../module/versioning/JsonVersionedModel.java | 1 - .../versioning/VersionedModelConverter.java | 23 ++++ .../VersionedModelDeserializer.java | 23 ++++ .../versioning/VersionedModelSerializer.java | 23 ++++ .../module/versioning/VersioningModule.java | 23 ++++ .../versioning/VersioningModuleTest.groovy | 27 +++- 8 files changed, 372 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a635f6e..0f04108 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,126 @@ -# jackson-module-model-versioning -Jackson 2.x module to support versioning and transforming of modules. +# Jackson Model Versioning Module +Jackson 2.x module for handling versioning of models. + +## The Problem +Let's say we create an API that accepts the following car data JSON: +```json +{ + "model": "honda:civic", + "year": 2016, + "new": "true" +} +``` + +Later, we decide that `model` should be split into two fields (`make` and `model`). +```json +{ + "make": "honda" + "model": "civic", + "year": 2016, + "new": "true" +} +``` + +Then we decide that `new` should be actually be retyped and renamed (boolean `used`). +```json +{ + "make": "honda" + "model": "civic", + "year": 2016, + "used": false +} +``` + +By this point, we have three formats of data that clients might be sending to our API. Hopefully we had the foresight to implement versioning on the models or API call's parameters. + +There are many ways this could be achieved, such as creating multiple model objects (one per version) and assigning them to each API call's versioned endpoint: +``` +POST /api/car/v1/ <- CarV1 +GET /api/car/v1/ -> List +GET /api/car/v1/{id} -> CarV1 + +POST /api/car/v2/ <- CarV2 +GET /api/car/v1/ -> List +GET /api/car/v2/{id} -> CarV2 +... +``` + +Another, less boilerplate-reliant way to do this is to have a single version of the endpoints, but to version the models themselves instead. This is where this module comes in. + +## The Solution +By using this Jackson module, we can annotate a single POJO with version-relevant conversion logic to execute on the raw JSON data before deserializing it into the model. +```groovy +@JsonVersionedModel(currentVersion = '3', converterClass = CarVersionedModelConverter) +class Car { + String make + String model + int year + boolean used +} +``` +```groovy +class CarVersionedModelConverter implements VersionedModelConverter { + @Override + def void convert(String modelVersion, ObjectNode modelData) { + // model version is an int + def modelVersionNum = modelVersion as int + + // version 1 had a single 'model' field that combined 'make' and 'model' with a colon delimiter; split + if(modelVersionNum < 2) { + def makeAndModel = modelData.get('model').asText().split(':') + modelData.put('make', makeAndModel[0]) + modelData.put('model', makeAndModel[1]) + } + + // version 1-2 had a 'new' text field instead of a boolean 'used' field; convert and invert + if(modelVersionNum < 3) + modelData.put('used', !(modelData.remove('new').asText() as boolean)) + } +} +``` + +All that's left is to configure the Jackson ObjectMapper with the module and test it out. +```groovy +def mapper = new ObjectMapper().registerModule(new VersioningModule()) +def hondaCivic = mapper.readValue( + '{"model": "honda:civic", "year": 2016, "new": "true", "modelVersion": "1"}', + Car +) +println mapper.writeValueAsString(hondaCivic) +// prints '{"make": "honda", "model": "civic", "year": 2016, "used": false, "modelVersion": "3"}' + +def toyotaCamry = mapper.readValue( + '{"make": "toyota", "model": "camry", "year": 2012, "new": "false", "modelVersion": "2"}', + Car +) +println mapper.writeValueAsString(hondaCivic) +// prints '{"make": "toyota", "model": "camry", "year": 2012, "used": true, "modelVersion": "3"}' + +def mazda6 = mapper.readValue( + '{"make": "mazda", "model": "6", "year": 2017, "used": false, "modelVersion": "3"}', + Car +) +println mapper.writeValueAsString(mazda6) +// prints '{"make": "mazda", "model": "6", "year": 2017, "used": false, "modelVersion": "3"}' +``` + +## Compatibility +Compiled for Java 6 and tested with Jackson 2.2 - 2.8. + +## Getting Started with Gradle +```groovy +dependencies { + compile 'com.github.jonpeterson:jackson-module-model-versioning:1.0.0' +} +``` + +## Getting Started with Maven +```xml + + com.github.jonpeterson + jackson-module-model-versioning + 1.0.0 + +``` + +## [JavaDoc](https://jonpeterson.github.io/docs/jackson-module-model-versioning/1.0.0/index.html) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 582b4b2..7c9f5c2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,13 @@ buildscript { ext { + testJacksonVersions = ['2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0', '2.8.0'] + // external dependency versions groovyVersion = '2.4.5' gradleVersion = '2.14.1' - jacksonVersion = '[2.2,)' + jacksonVersion = "[${testJacksonVersions.first()},)" + licensePluginVersion = '0.13.1' + releasePluginVersion = '2.4.0' spockVersion = '1.0-groovy-2.4' // external dependencies @@ -14,6 +18,15 @@ buildscript { repositories { mavenCentral() + jcenter() + maven { + url 'https://plugins.gradle.org/m2/' + } + } + + dependencies { + classpath "gradle.plugin.nl.javadude.gradle.plugins:license-gradle-plugin:${licensePluginVersion}" + classpath "net.researchgate:gradle-release:${releasePluginVersion}" } } @@ -27,6 +40,10 @@ group = 'com.github.jonpeterson' apply plugin: 'java' apply plugin: 'groovy' +apply plugin: 'com.github.hierynomus.license' +apply plugin: 'net.researchgate.release' +apply plugin: 'maven' +apply plugin: 'signing' sourceCompatibility = 1.6 targetCompatibility = 1.6 @@ -40,4 +57,115 @@ dependencies { testCompile groovy testCompile spockCore +} + +license { + include '**/*.java' + include '**/*.groovy' + mapping { + java = 'SLASHSTAR_STYLE' + groovy = 'SLASHSTAR_STYLE' + } +} + +// don't run tests with default test task +test.excludes = ['**/*'] + +// dynamically set up test executions for each version of Jackson +testJacksonVersions.each { version -> + def safeVersion = version.replaceAll('\\.', '_') + + configurations { + it."testJackson${safeVersion}Compile".extendsFrom testCompile + it."testJackson${safeVersion}Runtime".extendsFrom testRuntime + } + + sourceSets{ + it."testJackson$safeVersion" { + compileClasspath += sourceSets.main.output + sourceSets.test.output + runtimeClasspath += sourceSets.main.output + sourceSets.test.output + } + } + + dependencies { + it."testJackson${safeVersion}Compile" jacksonDatabind.replace(jacksonVersion, version) + } + + task "testJackson$safeVersion"(type: Test) { + testClassesDir = sourceSets.test.output.classesDir + classpath = sourceSets."testJackson$safeVersion".runtimeClasspath + } + + test.dependsOn "testJackson$safeVersion" +} + + +task javadocJar(type: Jar) { + dependsOn javadoc + classifier = 'javadoc' + from "$buildDir/javadoc" +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +build { + dependsOn javadocJar + dependsOn sourcesJar +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +signing { + sign configurations.archives +} + +uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { deployment -> signing.signPom(deployment) } + + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + pom.project { + name 'Jackson Model Versioning Module' + packaging 'jar' + description 'Jackson 2.x module for handling versioning of models.' + url 'https://github.com/jonpeterson/jackson-module-module-versioning' + + scm { + connection 'scm:git:git://github.com/jonpeterson/jackson-module-module-versioning.git' + developerConnection 'scm:git:git@github.com:jonpeterson/jackson-module-module-versioning.git' + url 'https://github.com/jonpeterson/jackson-module-module-versioning' + } + + licenses { + license { + name 'MIT License' + url 'http://www.opensource.org/licenses/mit-license.php' + } + } + + developers { + developer { + id 'jonpeterson' + name 'Jonathan Peterson' + email 'jonathan.p.peterson@gmail.com' + url 'https://github.com/jonpeterson' + } + } + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java index 7a7bf0b..4f29375 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java @@ -21,7 +21,6 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.annotation.JacksonAnnotation; diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java index 2efaab2..2e0ab96 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java @@ -1,3 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java index 696f78f..da0bd93 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java @@ -1,3 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java index c971340..9e146bd 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java @@ -1,3 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.core.JsonFactory; diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java index 881252e..856afd9 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java @@ -1,3 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.databind.*; diff --git a/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy b/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy index 3c18d33..b59f055 100644 --- a/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy +++ b/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy @@ -1,3 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.github.jonpeterson.jackson.module.versioning import com.fasterxml.jackson.databind.MapperFeature @@ -59,7 +82,7 @@ class VersioningModuleTest extends Specification { .registerModule(new VersioningModule()) expect: - def serialized = mapper.readValue( + def deserialized = mapper.readValue( '''{ | "type": "sedan", | "cars": [ @@ -118,7 +141,7 @@ class VersioningModuleTest extends Specification { CarsByType ) - mapper.writeValueAsString(serialized).replaceAll('\r\n', '\n') == + mapper.writeValueAsString(deserialized).replaceAll('\r\n', '\n') == '''{ | "cars" : [ { | "make" : "honda",