From 3c00a1ad8013c375a598dd88139dce174e9bf401 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 19 Apr 2018 15:46:48 -0700 Subject: Expose functionality to override parameter to field name mapping --- build.sbt | 4 ++ shared/src/main/scala/DerivedFormats.scala | 12 +++-- shared/src/test/scala/FieldNameTests.scala | 72 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 shared/src/test/scala/FieldNameTests.scala diff --git a/build.sbt b/build.sbt index 0954c0a..c57e9c5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,6 @@ // shadow sbt-scalajs' crossProject and CrossType until Scala.js 1.0.0 is released import sbtcrossproject.{crossProject, CrossType} +import com.typesafe.tools.mima.core._ lazy val sprayJsonDerivation = crossProject(JVMPlatform, JSPlatform, NativePlatform) @@ -29,6 +30,9 @@ lazy val sprayJsonDerivation = .jvmSettings( mimaPreviousArtifacts := Set( "xyz.driver" %% "spray-json-derivation" % "0.3.1"), + mimaBinaryIssueFilters += + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "spray.json.DerivedFormats.extractFieldName"), crossScalaVersions := "2.12.4" :: "2.11.12" :: Nil ) .jsSettings( diff --git a/shared/src/main/scala/DerivedFormats.scala b/shared/src/main/scala/DerivedFormats.scala index eabfa82..4a0b8b4 100644 --- a/shared/src/main/scala/DerivedFormats.scala +++ b/shared/src/main/scala/DerivedFormats.scala @@ -9,11 +9,16 @@ import scala.language.experimental.macros trait DerivedFormats { self: BasicFormats => type Typeclass[T] = JsonFormat[T] + /** Convert the name of a parameter to that of a field in a JSON object. This + * method can be overriden to use alternative naming conventions. */ + def extractFieldName(paramName: String): String = paramName + def combine[T](ctx: CaseClass[JsonFormat, T]): JsonFormat[T] = new JsonFormat[T] { override def write(value: T): JsValue = { val fields: Seq[(String, JsValue)] = ctx.parameters.map { param => - param.label -> param.typeclass.write(param.dereference(value)) + extractFieldName(param.label) -> param.typeclass.write( + param.dereference(value)) } JsObject(fields: _*) } @@ -24,10 +29,11 @@ trait DerivedFormats { self: BasicFormats => ctx.rawConstruct(Seq.empty) } else { ctx.construct { param => + val fieldName = extractFieldName(param.label) val fieldValue = if (param.option) { - obj.fields.getOrElse(param.label, JsNull) + obj.fields.getOrElse(fieldName, JsNull) } else { - obj.fields(param.label) + obj.fields(fieldName) } param.typeclass.read(fieldValue) } diff --git a/shared/src/test/scala/FieldNameTests.scala b/shared/src/test/scala/FieldNameTests.scala new file mode 100644 index 0000000..f1b76a9 --- /dev/null +++ b/shared/src/test/scala/FieldNameTests.scala @@ -0,0 +1,72 @@ +package spray.json + +import org.scalatest._ + +trait SnakeCaseFormats { self: DerivedFormats => + override def extractFieldName(paramName: String) = + FieldNaming.substituteCamel(paramName, '_') +} +trait KebabCaseFormats { self: DerivedFormats => + override def extractFieldName(paramName: String) = + FieldNaming.substituteCamel(paramName, '-') +} + +object FieldNaming { + + @inline final private def isLower(ch: Char): Boolean = + ((ch & 0x20) != 0) || (ch == '_') + + @inline def substituteCamel(paramName: String, substitute: Char) = { + val length = paramName.length + val builder = new StringBuilder(length) + var i = 0 + while (i < length) { + val cur = paramName(i) + if (isLower(cur) && i + 1 < length) { + builder.append(cur) + val next = paramName(i + 1) + if (!isLower(next)) { + builder.append(substitute) + builder.append((next ^ 0x20).toChar) + } else { + builder.append(next) + } + i += 1 + } else { + builder.append((cur ^ 0x20).toChar) + } + i += 1 + } + builder.result() + } + +} + +class FieldNameTests extends FlatSpec with FormatTests { + + case class A(camelCASE: String, `__a_aB__`: Int, `a-a_B`: Int) + case class B(camelCaseA: A) + + trait All extends DefaultJsonProtocol with DerivedFormats { + implicit val bFormat = jsonFormat[B] + } + + { + object Protocol extends All with SnakeCaseFormats + import Protocol._ + "snake_case" should behave like checkRoundtrip( + B(A("helloWorld", 0, 0)), + """{"camel_case_a":{"camel_case":"helloWorld","__a_a_b__":0,"a-a_b":0}}""" + ) + } + + { + object Protocol extends All with KebabCaseFormats + import Protocol._ + "kebab-case" should behave like checkRoundtrip( + B(A("helloWorld", 0, 0)), + """{"camel-case-a":{"camel-case":"helloWorld","__a_a-b__":0,"a-a_b":0}}""" + ) + } + +} -- cgit v1.2.3