diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 1049297b6..ed0268093 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -50,6 +50,7 @@ case object CamelCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceCamelOrPascalCase(memberName, toPascal = false) } + case object PascalCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceCamelOrPascalCase(memberName, toPascal = true) } @@ -60,6 +61,18 @@ case object IdentityFormat extends JsonMemberFormat { override def apply(memberName: String): String = memberName } +/** zio-json version 0.3.0 formats. abc123Def -> abc_123_def */ +object ziojson_03 { + case object SnakeCase extends JsonMemberFormat { + override def apply(memberName: String): String = + jsonMemberNames.enforceSnakeOrKebabCaseSeparateNumbers(memberName, '_') + } + case object KebabCase extends JsonMemberFormat { + override def apply(memberName: String): String = + jsonMemberNames.enforceSnakeOrKebabCaseSeparateNumbers(memberName, '-') + } +} + /** * If used on a case class, determines the strategy of member names * transformation during serialization and deserialization. Four common @@ -105,6 +118,29 @@ private[json] object jsonMemberNames { } def enforceSnakeOrKebabCase(s: String, separator: Char): String = { + val len = s.length + val sb = new StringBuilder(len << 1) + var i = 0 + var isPrecedingNotUpperCased = false + while (i < len) isPrecedingNotUpperCased = { + val ch = s.charAt(i) + i += 1 + if (ch == '_' || ch == '-') { + sb.append(separator) + false + } else if (!isUpperCase(ch)) { + sb.append(ch) + true + } else { + if (isPrecedingNotUpperCased || i > 1 && i < len && !isUpperCase(s.charAt(i))) sb.append(separator) + sb.append(toLowerCase(ch)) + false + } + } + sb.toString + } + + def enforceSnakeOrKebabCaseSeparateNumbers(s: String, separator: Char): String = { val len = s.length val sb = new StringBuilder(len << 1) var i = 0 diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 3dc34d338..5f84c00c8 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -60,6 +60,18 @@ case object KebabCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceSnakeOrKebabCase(memberName, '-') } +/** zio-json version 0.3.0 formats. abc123Def -> abc_123_def */ +object ziojson_03 { + case object SnakeCase extends JsonMemberFormat { + override def apply(memberName: String): String = + jsonMemberNames.enforceSnakeOrKebabCaseSeparateNumbers(memberName, '_') + } + case object KebabCase extends JsonMemberFormat { + override def apply(memberName: String): String = + jsonMemberNames.enforceSnakeOrKebabCaseSeparateNumbers(memberName, '-') + } +} + /** * If used on a case class, determines the strategy of member names * transformation during serialization and deserialization. Four common @@ -108,6 +120,29 @@ private[json] object jsonMemberNames { val len = s.length val sb = new StringBuilder(len << 1) var i = 0 + var isPrecedingNotUpperCased = false + while (i < len) isPrecedingNotUpperCased = { + val ch = s.charAt(i) + i += 1 + if (ch == '_' || ch == '-') { + sb.append(separator) + false + } else if (!isUpperCase(ch)) { + sb.append(ch) + true + } else { + if (isPrecedingNotUpperCased || i > 1 && i < len && !isUpperCase(s.charAt(i))) sb.append(separator) + sb.append(toLowerCase(ch)) + false + } + } + sb.toString + } + + def enforceSnakeOrKebabCaseSeparateNumbers(s: String, separator: Char): String = { + val len = s.length + val sb = new StringBuilder(len << 1) + var i = 0 var isPrecedingLowerCased = false while (i < len) isPrecedingLowerCased = { val ch = s.charAt(i) diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 8c0077a91..18afa3537 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -83,23 +83,31 @@ object CodecSpec extends ZIOSpecDefault { }, test("key transformation") { import exampletransformkeys._ - val kebabed = """{"shish-kebab":""}""" - val snaked = """{"indiana_jones":""}""" - val pascaled = """{"AndersHejlsberg":""}""" - val cameled = """{"smallTalk":""}""" - val indianaJones = """{"wHATcASEiStHIS":""}""" - val overrides = """{"not_modified":"","but-this-should-be":0}""" + val kebabed = """{"shish123-kebab":""}""" + val snaked = """{"indiana123_jones":""}""" + val pascaled = """{"Anders123Hejlsberg":""}""" + val cameled = """{"small123Talk":""}""" + val indianaJones = """{"wHATcASEiStHIS":""}""" + val overrides = """{"not_modified":"","but-this-should-be":0}""" + val kebabedLegacy = """{"shish-123-kebab":""}""" + val snakedLegacy = """{"indiana_123_jones":""}""" assert(kebabed.fromJson[Kebabed])(isRight(equalTo(Kebabed("")))) && + assert(kebabedLegacy.fromJson[legacy.Kebabed])(isRight(equalTo(legacy.Kebabed("")))) && assert(snaked.fromJson[Snaked])(isRight(equalTo(Snaked("")))) && + assert(snakedLegacy.fromJson[legacy.Snaked])(isRight(equalTo(legacy.Snaked("")))) && assert(pascaled.fromJson[Pascaled])(isRight(equalTo(Pascaled("")))) && assert(cameled.fromJson[Cameled])(isRight(equalTo(Cameled("")))) && assert(indianaJones.fromJson[Custom])(isRight(equalTo(Custom("")))) && assert(overrides.fromJson[OverridesAlsoWork])(isRight(equalTo(OverridesAlsoWork("", 0)))) && assertTrue(Kebabed("").toJson == kebabed) && assertTrue(Kebabed("").toJsonAST.toOption.get == kebabed.fromJson[Json].toOption.get) && + assertTrue(legacy.Kebabed("").toJson == kebabedLegacy) && + assertTrue(legacy.Kebabed("").toJsonAST.toOption.get == kebabedLegacy.fromJson[Json].toOption.get) && assertTrue(Snaked("").toJson == snaked) && assertTrue(Snaked("").toJsonAST.toOption.get == snaked.fromJson[Json].toOption.get) && + assertTrue(legacy.Snaked("").toJson == snakedLegacy) && + assertTrue(legacy.Snaked("").toJsonAST.toOption.get == snakedLegacy.fromJson[Json].toOption.get) && assertTrue(Pascaled("").toJson == pascaled) && assertTrue(Pascaled("").toJsonAST.toOption.get == pascaled.fromJson[Json].toOption.get) && assertTrue(Cameled("").toJson == cameled) && @@ -237,25 +245,25 @@ object CodecSpec extends ZIOSpecDefault { object exampletransformkeys { @jsonMemberNames(KebabCase) - case class Kebabed(shishKebab: String) + case class Kebabed(shish123Kebab: String) object Kebabed { implicit val codec: JsonCodec[Kebabed] = DeriveJsonCodec.gen[Kebabed] } @jsonMemberNames(SnakeCase) - case class Snaked(indianaJones: String) + case class Snaked(indiana123Jones: String) object Snaked { implicit val codec: JsonCodec[Snaked] = DeriveJsonCodec.gen[Snaked] } @jsonMemberNames(PascalCase) - case class Pascaled(andersHejlsberg: String) + case class Pascaled(anders123Hejlsberg: String) object Pascaled { implicit val codec: JsonCodec[Pascaled] = DeriveJsonCodec.gen[Pascaled] } @jsonMemberNames(CamelCase) - case class Cameled(small_talk: String) + case class Cameled(small123_talk: String) object Cameled { implicit val codec: JsonCodec[Cameled] = DeriveJsonCodec.gen[Cameled] } @@ -277,6 +285,23 @@ object CodecSpec extends ZIOSpecDefault { object OverridesAlsoWork { implicit val codec: JsonCodec[OverridesAlsoWork] = DeriveJsonCodec.gen[OverridesAlsoWork] } + + object legacy { + @jsonMemberNames(ziojson_03.KebabCase) + case class Kebabed(shish123Kebab: String) + + object Kebabed { + implicit val codec: JsonCodec[Kebabed] = DeriveJsonCodec.gen[Kebabed] + } + + @jsonMemberNames(ziojson_03.SnakeCase) + case class Snaked(indiana123Jones: String) + + object Snaked { + implicit val codec: JsonCodec[Snaked] = DeriveJsonCodec.gen[Snaked] + } + + } } object logEvent {