Skip to content

Commit

Permalink
Supports snakecase.
Browse files Browse the repository at this point in the history
  • Loading branch information
jkugiya committed Jul 22, 2018
1 parent 326a001 commit 8c601fc
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/main/scala/spray/json/SnakeCaseJsonSupport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package spray.json

import scala.reflect.ClassTag
import scala.collection.mutable

/**
* Provides snake cased JsonFormats
*/
trait SnakeCaseJsonSupport extends DefaultJsonProtocol {

override protected def extractFieldNames(classTag: ClassTag[_]): Array[String] = {
def snakify(name: String) = {
val chars = name.toCharArray
val len = name.length
val sb = new mutable.StringBuilder(len + len / 5)

def isAlphabetic(char: Char): Boolean = (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')

def go(i: Int, rest: Int, processedUpper: Boolean, processedAlphaNumeric: Boolean): String =
if (rest == 0) {
sb.toString()
} else if (rest > 1 && chars(i).isUpper && chars(i + 1).isLower) {
if (processedAlphaNumeric) {
sb.append('_')
}
sb.append(chars(i).toLower).append(chars(i + 1))
go(i + 2, rest - 2, false, true)
} else if (chars(i).isUpper) {
if (!processedUpper && processedAlphaNumeric) {
sb.append('_')
}
sb.append(chars(i).toLower)
go(i + 1, rest - 1, true, chars(i).isDigit || isAlphabetic(chars(i)))
} else if (!isAlphabetic(chars(i))) {
sb.append(chars(i))
go(i + 1, rest - 1, false, chars(i).isDigit)
} else {
sb.append(chars(i).toLower)
go(i + 1, rest - 1, chars(i).isUpper, chars(i).isDigit || isAlphabetic(chars(i)))
}

go(0, len, true, false)
}

super.extractFieldNames(classTag).map {
snakify(_)
}
}

}

object SnakeCaseJsonSupport extends SnakeCaseJsonSupport
94 changes: 94 additions & 0 deletions src/test/scala/spray/json/SnakeCaseJsonSupportSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package spray.json

import java.util.Locale

import org.specs2.mutable._

class SnakeCaseJsonSupportSpec extends Specification with SnakeCaseJsonSupport {

private val PASS1 = """([A-Z]+)([A-Z][a-z])""".r
private val PASS2 = """([a-z\d])([A-Z])""".r
private val REPLACEMENT = "$1_$2"

def snakify(name: String) =
PASS2.replaceAllIn(PASS1.replaceAllIn(name, REPLACEMENT), REPLACEMENT).toLowerCase(Locale.US)

"SnakeCaseJsonSUpport" should {
"convert field names to snake cased fields names." in {
// Given
case class SampleFields(
HelloWorld: Int = 0,
A1AAbBBBBBbCCcc: Int = 0,
AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA: Int = 0,
x: Int = 0,
Y: Int = 0,
`^There-arE_Non_(Alphabetic))_character!S`: Int = 0,
`tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s`: Int = 0,
` There are space character `: Int = 0,
` これは マルチバイトAbcのaBc確認です。`: Int = 0,
aa: Int = 0,
AA: Int = 0,
Aa: Int = 0,
aA: Int = 0,
AAA: Int = 0,
AAa: Int = 0,
AaA: Int = 0,
aAA: Int = 0,
aaA: Int = 0,
aAa: Int = 0,
Aaa: Int = 0,
aaa: Int = 0
)
implicit val jsonFormat: RootJsonFormat[SampleFields] =
jsonFormat21(SampleFields.apply)
// When
val sampleFields = SampleFields().toJson.asJsObject
// Then
val fields = sampleFields.fields
fields.contains(snakify("HelloWorld")) mustEqual true
fields.contains(snakify("A1AAbBBBBBbCCcc")) mustEqual true
fields.contains(snakify("AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA")) mustEqual true
fields.contains(snakify("x")) mustEqual true
fields.contains(snakify("Y")) mustEqual true
fields.contains(snakify("^There-arE_Non_(Alphabetic))_character!S")) mustEqual true
fields.contains(snakify("tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s")) mustEqual true
fields.contains(snakify(" There are space character ")) mustEqual true
fields.contains(snakify(" これは マルチバイトAbcのaBc確認です。")) mustEqual true
fields.contains(snakify("aa")) mustEqual true
fields.contains(snakify("aA")) mustEqual true
fields.contains(snakify("Aa")) mustEqual true
fields.contains(snakify("AA")) mustEqual true
fields.contains(snakify("AAA")) mustEqual true
fields.contains(snakify("aaA")) mustEqual true
fields.contains(snakify("aAa")) mustEqual true
fields.contains(snakify("Aaa")) mustEqual true
fields.contains(snakify("AAa")) mustEqual true
fields.contains(snakify("AaA")) mustEqual true
fields.contains(snakify("aAA")) mustEqual true
fields.contains(snakify("aaa")) mustEqual true
}
"deserialize snake cased json" in {
// Given
case class SampleFields(
a: Int,
aA: Int,
helloWorld: Int,
HelloTheWorld: Int
)
implicit val jsonFormat: RootJsonFormat[SampleFields] =
jsonFormat4(SampleFields.apply)
val json = JsObject(
"hello_the_world" -> 4.toJson,
"a_a" -> 2.toJson,
"a" -> 1.toJson,
"hello_world" -> 3.toJson
)
// When
val result = jsonFormat.read(json)
// Then
result mustEqual SampleFields(
a = 1, aA = 2, helloWorld = 3, HelloTheWorld = 4
)
}
}
}

0 comments on commit 8c601fc

Please sign in to comment.