-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
165 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
113 changes: 113 additions & 0 deletions
113
src/test/scala/spray/json/SnakeCaseJsonSupportSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
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 | ||
) | ||
case class SampleFields2( | ||
aaa123bbb: Int = 0, | ||
aaA123bbb: Int = 0, | ||
aaa123Bbb: Int = 0, | ||
aaA123Bbb: Int = 0, | ||
aaa123: Int = 0, | ||
aaA123: Int = 0, | ||
aAA123: Int = 0 | ||
) | ||
implicit val jsonFormat1: RootJsonFormat[SampleFields] = | ||
jsonFormat21(SampleFields.apply) | ||
implicit val jsonFormat2: RootJsonFormat[SampleFields2] = | ||
jsonFormat7(SampleFields2.apply) | ||
// When | ||
val sampleFields1 = SampleFields().toJson.asJsObject | ||
val sampleFields2 = SampleFields2().toJson.asJsObject | ||
// Then | ||
val fields1 = sampleFields1.fields | ||
fields1.contains(snakify("HelloWorld")) mustEqual true | ||
fields1.contains(snakify("A1AAbBBBBBbCCcc")) mustEqual true | ||
fields1.contains(snakify("AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA")) mustEqual true | ||
fields1.contains(snakify("x")) mustEqual true | ||
fields1.contains(snakify("Y")) mustEqual true | ||
fields1.contains(snakify("^There-arE_Non_(Alphabetic))_character!S")) mustEqual true | ||
fields1.contains(snakify("tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s")) mustEqual true | ||
fields1.contains(snakify(" There are space character ")) mustEqual true | ||
fields1.contains(snakify(" これは マルチバイトAbcのaBc確認です。")) mustEqual true | ||
fields1.contains(snakify("aa")) mustEqual true | ||
fields1.contains(snakify("aA")) mustEqual true | ||
fields1.contains(snakify("Aa")) mustEqual true | ||
fields1.contains(snakify("AA")) mustEqual true | ||
fields1.contains(snakify("AAA")) mustEqual true | ||
fields1.contains(snakify("aaA")) mustEqual true | ||
fields1.contains(snakify("aAa")) mustEqual true | ||
fields1.contains(snakify("Aaa")) mustEqual true | ||
fields1.contains(snakify("AAa")) mustEqual true | ||
fields1.contains(snakify("AaA")) mustEqual true | ||
fields1.contains(snakify("aAA")) mustEqual true | ||
fields1.contains(snakify("aaa")) mustEqual true | ||
val fields2 = sampleFields2.fields | ||
fields2.contains(snakify("aaa123bbb")) mustEqual true | ||
fields2.contains(snakify("aaa123Bbb")) mustEqual true | ||
fields2.contains(snakify("aaA123bbb")) mustEqual true | ||
fields2.contains(snakify("aaa123")) mustEqual true | ||
fields2.contains(snakify("aaA123")) mustEqual true | ||
fields2.contains(snakify("aAA123")) 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 | ||
) | ||
} | ||
} | ||
} |