aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2018-04-19 15:46:48 -0700
committerJakob Odersky <jakob@odersky.com>2018-04-19 16:36:31 -0700
commit3c00a1ad8013c375a598dd88139dce174e9bf401 (patch)
tree0d8f77cf46ddb6c1100ec0ff76ff6dc0b26fedd6
parent83942244b48d8f7a68ecdaade90b92a79378d43c (diff)
downloadspray-json-derivation-3c00a1ad8013c375a598dd88139dce174e9bf401.tar.gz
spray-json-derivation-3c00a1ad8013c375a598dd88139dce174e9bf401.tar.bz2
spray-json-derivation-3c00a1ad8013c375a598dd88139dce174e9bf401.zip
Expose functionality to override parameter to field name mapping
-rw-r--r--build.sbt4
-rw-r--r--shared/src/main/scala/DerivedFormats.scala12
-rw-r--r--shared/src/test/scala/FieldNameTests.scala72
3 files changed, 85 insertions, 3 deletions
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}}"""
+ )
+ }
+
+}