Skip to content

Commit

Permalink
Duration support for gson
Browse files Browse the repository at this point in the history
  • Loading branch information
oldergod committed Jul 5, 2020
1 parent 781c90b commit ad619ae
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 2 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -6,10 +6,10 @@ buildscript {
'compiler': "com.squareup.wire:wire-compiler",
'gradlePlugin': "com.squareup.wire:wire-gradle-plugin",
'grpcClient': "com.squareup.wire:wire-grpc-client",
'gsonSupport': "com.squareup.wire:wire-gson-support",
'javaGenerator': "com.squareup.wire:wire-java-generator",
'kotlinGenerator': "com.squareup.wire:wire-kotlin-generator",
'moshiAdapter': "com.squareup.wire:wire-moshi-adapter",
'gsonSupport': "com.squareup.wire:wire-gson-support",
'runtime': "com.squareup.wire:wire-runtime",
'schema': "com.squareup.wire:wire-schema",
'testUtils': "com.squareup.wire:wire-test-utils",
Expand Down
@@ -0,0 +1,94 @@
/*
* Copyright 2020 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.wire

import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import java.io.IOException

/**
* Encode a duration as a JSON string like "1.200s". From the spec:
*
* > Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required
* > precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long
* > as they fit into nano-seconds precision and the suffix "s" is required.
*
* Note that [Duration] always returns a positive nanosPart, so "-1.200s" is represented as -2
* seconds and 800_000_000 nanoseconds.
*/
internal object DurationTypeAdapter : TypeAdapter<Duration>() {
override fun write(out: JsonWriter, duration: Duration?) {
out.value(durationToString(duration!!))
}

@Throws(IOException::class)
override fun read(input: JsonReader): Duration {
val string = input.nextString()
try {
return stringToDuration(string)
} catch (_: NumberFormatException) {
throw IOException("not a duration: $string at path ${input.path}")
}
}

// TODO(benoit) Shared this with moshi-adapter via `wire-runtime`.
internal fun durationToString(duration: Duration): String {
var seconds = duration.seconds
var nanos = duration.nano
var prefix = ""
if (seconds < 0L) {
if (seconds == Long.MIN_VALUE) {
prefix = "-922337203685477580" // Avoid overflow inverting MIN_VALUE.
seconds = 8
} else {
prefix = "-"
seconds = -seconds
}
if (nanos != 0) {
seconds -= 1L
nanos = 1_000_000_000 - nanos
}
}
return when {
nanos == 0 -> "%s%ds".format(prefix, seconds)
nanos % 1_000_000 == 0 -> "%s%d.%03ds".format(prefix, seconds, nanos / 1_000_000L)
nanos % 1_000 == 0 -> "%s%d.%06ds".format(prefix, seconds, nanos / 1_000L)
else -> "%s%d.%09ds".format(prefix, seconds, nanos / 1L)
}
}

// TODO(benoit) Shared this with moshi-adapter via `wire-runtime`.
/** Throws a NumberFormatException if the string isn't a number like "1s" or "1.23456789s". */
internal fun stringToDuration(string: String): Duration {
val sIndex = string.indexOf('s')
if (sIndex != string.length - 1) throw NumberFormatException()

val dotIndex = string.indexOf('.')
if (dotIndex == -1) {
val seconds = string.substring(0, sIndex).toLong()
return Duration.ofSeconds(seconds)
}

val seconds = string.substring(0, dotIndex).toLong()
var nanos = string.substring(dotIndex + 1, sIndex).toLong()
if (string.startsWith("-")) nanos = -nanos
val nanosDigits = sIndex - (dotIndex + 1)
for (i in nanosDigits until 9) nanos *= 10
for (i in 9 until nanosDigits) nanos /= 10
return Duration.ofSeconds(seconds, nanos)
}
}
Expand Up @@ -46,6 +46,7 @@ class WireTypeAdapterFactory : TypeAdapterFactory {
type.rawType == ByteString::class.java -> ByteStringTypeAdapter() as TypeAdapter<T>
Message::class.java.isAssignableFrom(type.rawType) ->
MessageTypeAdapter<Nothing, Nothing>(gson, type as TypeToken<Nothing>) as TypeAdapter<T>
type.rawType == Duration::class.java -> DurationTypeAdapter as TypeAdapter<T>
else -> null
}
}
Expand Down
@@ -0,0 +1,63 @@
/*
* Copyright 2020 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.wire

import com.squareup.wire.DurationTypeAdapter.durationToString
import com.squareup.wire.DurationTypeAdapter.stringToDuration
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class DurationTypeAdapterTest {
@Test
fun `string to duration`() {
assertThat(stringToDuration("1s")).isEqualTo(durationOfSeconds(1L, 0L))
assertThat(stringToDuration("1.00000000200s")).isEqualTo(durationOfSeconds(1L, 2L))
assertThat(stringToDuration("1.0000000020s")).isEqualTo(durationOfSeconds(1L, 2L))
assertThat(stringToDuration("1.000000002s")).isEqualTo(durationOfSeconds(1L, 2L))
assertThat(stringToDuration("1.000002s")).isEqualTo(durationOfSeconds(1L, 2000L))
assertThat(stringToDuration("1.002s")).isEqualTo(durationOfSeconds(1L, 2000000L))
assertThat(stringToDuration("1.2s")).isEqualTo(durationOfSeconds(1L, 200000000L))
assertThat(stringToDuration("-1.2s")).isEqualTo(durationOfSeconds(-1L, -200000000L))
assertThat(stringToDuration("-0.2s")).isEqualTo(durationOfSeconds(0L, -200000000L))
assertThat(stringToDuration("0.2s")).isEqualTo(durationOfSeconds(0L, 200000000L))
assertThat(stringToDuration("-9223372036854775808s")).isEqualTo(
durationOfSeconds(Long.MIN_VALUE, 0L))
assertThat(stringToDuration("9223372036854775807.999999999s"))
.isEqualTo(durationOfSeconds(Long.MAX_VALUE, 999_999_999L))
assertThat(stringToDuration("-9223372036854775807.999999999s"))
.isEqualTo(durationOfSeconds(-9223372036854775807L, -999_999_999L))
}

@Test
fun `duration to string`() {
assertThat(durationToString(durationOfSeconds( 0L, 0L))).isEqualTo("0s")
assertThat(durationToString(durationOfSeconds( 1L, 0L))).isEqualTo("1s")
assertThat(durationToString(durationOfSeconds( 1L, 2L))).isEqualTo( "1.000000002s")
assertThat(durationToString(durationOfSeconds( 1L, 20L))).isEqualTo( "1.000000020s")
assertThat(durationToString(durationOfSeconds( 1L, 2_000L))).isEqualTo( "1.000002s")
assertThat(durationToString(durationOfSeconds( 1L, 2_000_000L))).isEqualTo( "1.002s")
assertThat(durationToString(durationOfSeconds( 1L, 200_000_000L))).isEqualTo( "1.200s")
assertThat(durationToString(durationOfSeconds(-1L, -200_000_000L))).isEqualTo("-1.200s")
assertThat(durationToString(durationOfSeconds( 0L, -200_000_000L))).isEqualTo("-0.200s")
assertThat(durationToString(durationOfSeconds( 0L, -999_999_999L))).isEqualTo("-0.999999999s")
assertThat(durationToString(durationOfSeconds( 0L, -999_999_000L))).isEqualTo("-0.999999s")
assertThat(durationToString(durationOfSeconds( 0L, -999_900_000L))).isEqualTo("-0.999900s")
assertThat(durationToString(durationOfSeconds( 0L, -999_000_000L))).isEqualTo("-0.999s")
assertThat(durationToString(durationOfSeconds(Long.MIN_VALUE, 0L))).isEqualTo("-9223372036854775808s")
assertThat(durationToString(durationOfSeconds(Long.MIN_VALUE, 1L)))
.isEqualTo("-9223372036854775807.999999999s")
}
}
2 changes: 1 addition & 1 deletion wire-protoc-compatibility-tests/build.gradle
Expand Up @@ -45,8 +45,8 @@ dependencies {
testImplementation deps.junit
testImplementation deps.moshi
testImplementation deps.protobuf.javaUtil
testImplementation deps.wire.moshiAdapter
testImplementation deps.wire.gsonSupport
testImplementation deps.wire.moshiAdapter
testImplementation deps.wire.testUtils
}

Expand Down
Expand Up @@ -158,6 +158,36 @@ class Proto3WireProtocCompatibilityTests {
assertThat(jsonAdapter.fromJson(json)).isEqualTo(pizzaDelivery)
}

@Test fun gsonJson() {
val pizzaDelivery = PizzaDelivery(
address = "507 Cross Street",
pizzas = listOf(Pizza(toppings = listOf("pineapple", "onion"))),
delivered_within_or_free = durationOfSeconds(1_799L, 500_000_000L)
)
val json = """
|{
| "address": "507 Cross Street",
| "deliveredWithinOrFree": "1799.500s",
| "phoneNumber": "",
| "pizzas": [
| {
| "toppings": [
| "pineapple",
| "onion"
| ]
| }
| ]
|}
""".trimMargin()

val gson = GsonBuilder().registerTypeAdapterFactory(WireTypeAdapterFactory())
.disableHtmlEscaping()
.create()

assertJsonEquals(gson.toJson(pizzaDelivery), json)
assertThat(gson.fromJson(json, PizzaDelivery::class.java)).isEqualTo(pizzaDelivery)
}

@Test fun wireProtocJsonRoundTrip() {
val protocMessage = PizzaOuterClass.PizzaDelivery.newBuilder()
.setAddress("507 Cross Street")
Expand Down

0 comments on commit ad619ae

Please sign in to comment.