Skip to content

Commit 8c601fc

Browse files
committed
Supports snakecase.
1 parent 326a001 commit 8c601fc

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package spray.json
2+
3+
import scala.reflect.ClassTag
4+
import scala.collection.mutable
5+
6+
/**
7+
* Provides snake cased JsonFormats
8+
*/
9+
trait SnakeCaseJsonSupport extends DefaultJsonProtocol {
10+
11+
override protected def extractFieldNames(classTag: ClassTag[_]): Array[String] = {
12+
def snakify(name: String) = {
13+
val chars = name.toCharArray
14+
val len = name.length
15+
val sb = new mutable.StringBuilder(len + len / 5)
16+
17+
def isAlphabetic(char: Char): Boolean = (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
18+
19+
def go(i: Int, rest: Int, processedUpper: Boolean, processedAlphaNumeric: Boolean): String =
20+
if (rest == 0) {
21+
sb.toString()
22+
} else if (rest > 1 && chars(i).isUpper && chars(i + 1).isLower) {
23+
if (processedAlphaNumeric) {
24+
sb.append('_')
25+
}
26+
sb.append(chars(i).toLower).append(chars(i + 1))
27+
go(i + 2, rest - 2, false, true)
28+
} else if (chars(i).isUpper) {
29+
if (!processedUpper && processedAlphaNumeric) {
30+
sb.append('_')
31+
}
32+
sb.append(chars(i).toLower)
33+
go(i + 1, rest - 1, true, chars(i).isDigit || isAlphabetic(chars(i)))
34+
} else if (!isAlphabetic(chars(i))) {
35+
sb.append(chars(i))
36+
go(i + 1, rest - 1, false, chars(i).isDigit)
37+
} else {
38+
sb.append(chars(i).toLower)
39+
go(i + 1, rest - 1, chars(i).isUpper, chars(i).isDigit || isAlphabetic(chars(i)))
40+
}
41+
42+
go(0, len, true, false)
43+
}
44+
45+
super.extractFieldNames(classTag).map {
46+
snakify(_)
47+
}
48+
}
49+
50+
}
51+
52+
object SnakeCaseJsonSupport extends SnakeCaseJsonSupport
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package spray.json
2+
3+
import java.util.Locale
4+
5+
import org.specs2.mutable._
6+
7+
class SnakeCaseJsonSupportSpec extends Specification with SnakeCaseJsonSupport {
8+
9+
private val PASS1 = """([A-Z]+)([A-Z][a-z])""".r
10+
private val PASS2 = """([a-z\d])([A-Z])""".r
11+
private val REPLACEMENT = "$1_$2"
12+
13+
def snakify(name: String) =
14+
PASS2.replaceAllIn(PASS1.replaceAllIn(name, REPLACEMENT), REPLACEMENT).toLowerCase(Locale.US)
15+
16+
"SnakeCaseJsonSUpport" should {
17+
"convert field names to snake cased fields names." in {
18+
// Given
19+
case class SampleFields(
20+
HelloWorld: Int = 0,
21+
A1AAbBBBBBbCCcc: Int = 0,
22+
AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA: Int = 0,
23+
x: Int = 0,
24+
Y: Int = 0,
25+
`^There-arE_Non_(Alphabetic))_character!S`: Int = 0,
26+
`tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s`: Int = 0,
27+
` There are space character `: Int = 0,
28+
` これは マルチバイトAbcのaBc確認です。`: Int = 0,
29+
aa: Int = 0,
30+
AA: Int = 0,
31+
Aa: Int = 0,
32+
aA: Int = 0,
33+
AAA: Int = 0,
34+
AAa: Int = 0,
35+
AaA: Int = 0,
36+
aAA: Int = 0,
37+
aaA: Int = 0,
38+
aAa: Int = 0,
39+
Aaa: Int = 0,
40+
aaa: Int = 0
41+
)
42+
implicit val jsonFormat: RootJsonFormat[SampleFields] =
43+
jsonFormat21(SampleFields.apply)
44+
// When
45+
val sampleFields = SampleFields().toJson.asJsObject
46+
// Then
47+
val fields = sampleFields.fields
48+
fields.contains(snakify("HelloWorld")) mustEqual true
49+
fields.contains(snakify("A1AAbBBBBBbCCcc")) mustEqual true
50+
fields.contains(snakify("AbAAbbAAAbbb1AA2bb1AA2AAAAbb1bAA")) mustEqual true
51+
fields.contains(snakify("x")) mustEqual true
52+
fields.contains(snakify("Y")) mustEqual true
53+
fields.contains(snakify("^There-arE_Non_(Alphabetic))_character!S")) mustEqual true
54+
fields.contains(snakify("tHere-Are_(AlsO)_noN_(alphaBetic))_CharaCter!s")) mustEqual true
55+
fields.contains(snakify(" There are space character ")) mustEqual true
56+
fields.contains(snakify(" これは マルチバイトAbcのaBc確認です。")) mustEqual true
57+
fields.contains(snakify("aa")) mustEqual true
58+
fields.contains(snakify("aA")) mustEqual true
59+
fields.contains(snakify("Aa")) mustEqual true
60+
fields.contains(snakify("AA")) mustEqual true
61+
fields.contains(snakify("AAA")) mustEqual true
62+
fields.contains(snakify("aaA")) mustEqual true
63+
fields.contains(snakify("aAa")) mustEqual true
64+
fields.contains(snakify("Aaa")) mustEqual true
65+
fields.contains(snakify("AAa")) mustEqual true
66+
fields.contains(snakify("AaA")) mustEqual true
67+
fields.contains(snakify("aAA")) mustEqual true
68+
fields.contains(snakify("aaa")) mustEqual true
69+
}
70+
"deserialize snake cased json" in {
71+
// Given
72+
case class SampleFields(
73+
a: Int,
74+
aA: Int,
75+
helloWorld: Int,
76+
HelloTheWorld: Int
77+
)
78+
implicit val jsonFormat: RootJsonFormat[SampleFields] =
79+
jsonFormat4(SampleFields.apply)
80+
val json = JsObject(
81+
"hello_the_world" -> 4.toJson,
82+
"a_a" -> 2.toJson,
83+
"a" -> 1.toJson,
84+
"hello_world" -> 3.toJson
85+
)
86+
// When
87+
val result = jsonFormat.read(json)
88+
// Then
89+
result mustEqual SampleFields(
90+
a = 1, aA = 2, helloWorld = 3, HelloTheWorld = 4
91+
)
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)