Change the directory structure to match our modules (#1451)

This commit is contained in:
Jesse Wilson
2021-12-08 23:52:51 -05:00
committed by GitHub
parent d5d172c3bb
commit 7578984f25
68 changed files with 24 additions and 265 deletions

View File

@@ -0,0 +1,71 @@
import Build_gradle.TestMode.KAPT
import Build_gradle.TestMode.KSP
import Build_gradle.TestMode.REFLECT
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt") apply false
id("com.google.devtools.ksp") apply false
}
enum class TestMode {
REFLECT, KAPT, KSP
}
val testMode = findProperty("kotlinTestMode")?.toString()
?.let(TestMode::valueOf)
?: KSP
when (testMode) {
REFLECT -> {
// Default to KSP. This is a CI-only thing
apply(plugin = "com.google.devtools.ksp")
}
KAPT -> {
apply(plugin = "org.jetbrains.kotlin.kapt")
}
KSP -> {
apply(plugin = "com.google.devtools.ksp")
}
}
tasks.withType<Test>().configureEach {
// ExtendsPlatformClassWithProtectedField tests a case where we set a protected ByteArrayOutputStream.buf field
jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED")
}
val useWError = findProperty("kotlinLanguageVersion")?.toString()
?.startsWith("1.5")
?: false
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
allWarningsAsErrors = useWError
@Suppress("SuspiciousCollectionReassignment")
freeCompilerArgs += listOf(
"-Xopt-in=kotlin.ExperimentalStdlibApi"
)
}
}
dependencies {
when (testMode) {
REFLECT -> {
// Default to KSP in this case, this is a CI-only thing
"kspTest"(project(":moshi-kotlin-codegen"))
}
KAPT -> {
"kaptTest"(project(":moshi-kotlin-codegen"))
}
KSP -> {
"kspTest"(project(":moshi-kotlin-codegen"))
}
}
testImplementation(project(":moshi"))
testImplementation(project(":moshi-kotlin"))
testImplementation(project(":moshi-kotlin-tests:extra-moshi-test-module"))
testImplementation(kotlin("reflect"))
testImplementation(libs.junit)
testImplementation(libs.assertj)
testImplementation(libs.truth)
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.test.extra.AbstractClassInModuleA
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.TYPE
/*
* These are classes that need only compile.
*/
// Regression test for https://github.com/square/moshi/issues/905
@JsonClass(generateAdapter = true)
data class GenericTestClassWithDefaults<T>(
val input: String = "",
val genericInput: T
)
@Target(TYPE)
annotation class TypeAnnotation
/**
* Compilation-only test to ensure we don't render types with their annotations.
* Regression test for https://github.com/square/moshi/issues/1033
*/
@JsonClass(generateAdapter = true)
data class TypeAnnotationClass(
val propertyWithAnnotatedType: @TypeAnnotation String = "",
val generic: List<@TypeAnnotation String>
)
// Regression test for https://github.com/square/moshi/issues/1277
@JsonClass(generateAdapter = true)
data class OtherTestModel(val TestModel: TestModel? = null)
@JsonClass(generateAdapter = true)
data class TestModel(
val someVariable: Int,
val anotherVariable: String
)
// Regression test for https://github.com/square/moshi/issues/1022
@JsonClass(generateAdapter = true)
internal data class MismatchParentAndNestedClassVisibility(
val type: Int,
val name: String? = null
) {
@JsonClass(generateAdapter = true)
data class NestedClass(
val nestedProperty: String
)
}
// Regression test for https://github.com/square/moshi/issues/1052
@JsonClass(generateAdapter = true)
data class KeysWithSpaces(
@Json(name = "1. Information") val information: String,
@Json(name = "2. Symbol") val symbol: String,
@Json(name = "3. Last Refreshed") val lastRefreshed: String,
@Json(name = "4. Interval") val interval: String,
@Json(name = "5. Output Size") val size: String,
@Json(name = "6. Time Zone") val timeZone: String
)
// Regression test for https://github.com/square/moshi/issues/848
@JsonClass(generateAdapter = true)
data class Hotwords(
val `class`: List<String>?
)
/**
* This is here mostly just to ensure it still compiles. Covers variance, @Json, default values,
* nullability, primitive arrays, and some wacky generics.
*/
@JsonClass(generateAdapter = true)
data class SmokeTestType(
@Json(name = "first_name") val firstName: String,
@Json(name = "last_name") val lastName: String,
val age: Int,
val nationalities: List<String> = emptyList(),
val weight: Float,
val tattoos: Boolean = false,
val race: String?,
val hasChildren: Boolean = false,
val favoriteFood: String? = null,
val favoriteDrink: String? = "Water",
val wildcardOut: MutableList<out String> = mutableListOf(),
val nullableWildcardOut: MutableList<out String?> = mutableListOf(),
val wildcardIn: Array<in String>,
val any: List<*>,
val anyTwo: List<Any>,
val anyOut: MutableList<out Any>,
val nullableAnyOut: MutableList<out Any?>,
val favoriteThreeNumbers: IntArray,
val favoriteArrayValues: Array<String>,
val favoriteNullableArrayValues: Array<String?>,
val nullableSetListMapArrayNullableIntWithDefault: Set<List<Map<String, Array<IntArray?>>>>? = null,
val aliasedName: TypeAliasName = "Woah",
val genericAlias: GenericTypeAlias = listOf("Woah"),
// Regression test for https://github.com/square/moshi/issues/1272
val nestedArray: Array<Map<String, Any>>? = null
)
typealias TypeAliasName = String
typealias GenericTypeAlias = List<String>
// Regression test for enum constants in annotations and array types
// https://github.com/ZacSweers/MoshiX/issues/103
@Retention(RUNTIME)
@JsonQualifier
annotation class UpperCase(val foo: Array<Foo>)
enum class Foo { BAR }
@JsonClass(generateAdapter = true)
data class ClassWithQualifier(
@UpperCase(foo = [Foo.BAR])
val a: Int
)
// Regression for https://github.com/ZacSweers/MoshiX/issues/120
@JsonClass(generateAdapter = true)
data class DataClassInModuleB(
val id: String
) : AbstractClassInModuleA()

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UNUSED", "UNUSED_PARAMETER")
package com.squareup.moshi.kotlin.codegen
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.intellij.lang.annotations.Language
import org.junit.Test
class ComplexGenericsInheritanceTest {
private val moshi = Moshi.Builder().build()
@Test
fun simple() {
val adapter = moshi.adapter<PersonResponse>()
@Language("JSON")
val json =
"""{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = PersonResponse().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun nested() {
val adapter = moshi.adapter<NestedPersonResponse>()
@Language("JSON")
val json =
"""{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = NestedPersonResponse().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun untyped() {
val adapter = moshi.adapter<UntypedNestedPersonResponse<Person>>()
@Language("JSON")
val json =
"""{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = UntypedNestedPersonResponse<Person>().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun complex() {
val adapter = moshi.adapter<Layer4<Person, UntypedNestedPersonResponse<Person>>>()
@Language("JSON")
val json =
"""{"layer4E":{"name":"layer4E"},"layer4F":{"data":{"name":"layer4F"},"data2":"layer4F","data3":"layer4F"},"layer3C":[1,2,3],"layer3D":"layer3D","layer2":"layer2","layer1":"layer1"}"""
val instance = adapter.fromJson(json)!!
val testInstance = Layer4(
layer4E = Person("layer4E"),
layer4F = UntypedNestedPersonResponse<Person>().apply {
data = Person("layer4F")
data2 = "layer4F"
data3 = "layer4F"
}
).apply {
layer3C = listOf(1, 2, 3)
layer3D = "layer3D"
layer2 = "layer2"
layer1 = "layer1"
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(testInstance)).isEqualTo(json)
}
}
open class ResponseWithSettableProperty<T, R> {
var data: T? = null
var data2: R? = null
var data3: R? = null
}
interface Personable
@JsonClass(generateAdapter = true)
data class Person(val name: String) : Personable
@JsonClass(generateAdapter = true)
data class PersonResponse(
val extra: String? = null
) : ResponseWithSettableProperty<Person, String>()
abstract class NestedResponse<T : Personable> : ResponseWithSettableProperty<T, String>()
@JsonClass(generateAdapter = true)
data class NestedPersonResponse(val extra: String? = null) : NestedResponse<Person>()
@JsonClass(generateAdapter = true)
data class UntypedNestedPersonResponse<T : Personable>(
val extra: String? = null
) : NestedResponse<T>()
interface LayerInterface<I>
abstract class Layer1<A> {
var layer1: A? = null
}
abstract class Layer2<B> : Layer1<B>(), LayerInterface<B> {
var layer2: B? = null
}
abstract class Layer3<C, D> : Layer2<D>() {
var layer3C: C? = null
var layer3D: D? = null
}
@JsonClass(generateAdapter = true)
data class Layer4<E : Personable, F>(
val layer4E: E,
val layer4F: F? = null
) : Layer3<List<Int>, String>(), LayerInterface<String>

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.junit.Test
class DefaultConstructorTest {
@Test fun minimal() {
val expected = TestClass("requiredClass")
val json =
"""{"required":"requiredClass"}"""
val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!!
check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected"
}
}
@Test fun allSet() {
val expected = TestClass("requiredClass", "customOptional", 4, "setDynamic", 5, 6)
val json =
"""{"required":"requiredClass","optional":"customOptional","optional2":4,"dynamicSelfReferenceOptional":"setDynamic","dynamicOptional":5,"dynamicInlineOptional":6}"""
val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!!
check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected"
}
}
@Test fun customDynamic() {
val expected = TestClass("requiredClass", "customOptional")
val json =
"""{"required":"requiredClass","optional":"customOptional"}"""
val instance = Moshi.Builder().build().adapter<TestClass>()
.fromJson(json)!!
check(instance == expected) {
"No match:\nActual : $instance\nExpected: $expected"
}
}
}
@JsonClass(generateAdapter = true)
data class TestClass(
val required: String,
val optional: String = "optional",
val optional2: Int = 2,
val dynamicSelfReferenceOptional: String = required,
val dynamicOptional: Int = createInt(),
val dynamicInlineOptional: Int = createInlineInt()
)
private fun createInt(): Int {
return 3
}
@Suppress("NOTHING_TO_INLINE")
private inline fun createInlineInt(): Int {
return 3
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.kotlin.codegen.GeneratedAdaptersTest.CustomGeneratedClass
// This also tests custom generated types with no moshi constructor
class GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter : JsonAdapter<CustomGeneratedClass>() {
override fun fromJson(reader: JsonReader): CustomGeneratedClass? {
TODO()
}
override fun toJson(writer: JsonWriter, value: CustomGeneratedClass?) {
TODO()
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen.LooksLikeAClass
import com.squareup.moshi.JsonClass
/**
* https://github.com/square/moshi/issues/783
*/
@JsonClass(generateAdapter = true)
data class ClassInPackageThatLooksLikeAClass(val foo: String)

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import org.junit.Test
class MixingReflectAndCodeGen {
@Test
fun mixingReflectionAndCodegen() {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val generatedAdapter = moshi.adapter<UsesGeneratedAdapter>()
val reflectionAdapter = moshi.adapter<UsesReflectionAdapter>()
assertThat(generatedAdapter.toString())
.isEqualTo("GeneratedJsonAdapter(MixingReflectAndCodeGen.UsesGeneratedAdapter).nullSafe()")
assertThat(reflectionAdapter.toString())
.isEqualTo(
"KotlinJsonAdapter(com.squareup.moshi.kotlin.codegen.MixingReflectAndCodeGen" +
".UsesReflectionAdapter).nullSafe()"
)
}
@JsonClass(generateAdapter = true)
class UsesGeneratedAdapter(var a: Int, var b: Int)
@JsonClass(generateAdapter = false)
class UsesReflectionAdapter(var a: Int, var b: Int)
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.junit.Test
// Regression tests specific to Moshi-KSP
class MoshiKspTest {
private val moshi = Moshi.Builder().build()
// Regression test for https://github.com/ZacSweers/MoshiX/issues/44
@Test
fun onlyInterfaceSupertypes() {
val adapter = moshi.adapter<SimpleImpl>()
//language=JSON
val json = """{"a":"aValue","b":"bValue"}"""
val expected = SimpleImpl("aValue", "bValue")
val instance = adapter.fromJson(json)!!
assertThat(instance).isEqualTo(expected)
val encoded = adapter.toJson(instance)
assertThat(encoded).isEqualTo(json)
}
interface SimpleInterface {
val a: String
}
// NOTE the Any() superclass is important to test that we're detecting the farthest parent class
// correct.y
@JsonClass(generateAdapter = true)
data class SimpleImpl(override val a: String, val b: String) : Any(), SimpleInterface
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.intellij.lang.annotations.Language
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* This test explicitly tests mask generation for classes with more than 32 parameters. Each mask
* can only indicate up to 32 parameters, so constructors with more than 32 parameters have to use
* multiple masks.
*
* This covers a few cases of this:
* - Ensuring values from json are matched to properties correctly
* - Some `@Transient` parameters (which participate in the constructor signature and mask indices)
* - This example has 3 total masks generated.
*
* Regression test for https://github.com/square/moshi/issues/977
*/
class MultipleMasksTest {
@Test fun testMultipleMasks() {
// Set some arbitrary values to make sure offsets are aligning correctly
@Language("JSON")
val json =
"""{"arg50":500,"arg3":34,"arg11":11,"arg65":67}"""
val instance = Moshi.Builder().build().adapter<MultipleMasks>()
.fromJson(json)!!
assertEquals(instance.arg2, 2)
assertEquals(instance.arg3, 34)
assertEquals(instance.arg11, 11)
assertEquals(instance.arg49, 49)
assertEquals(instance.arg50, 500)
assertEquals(instance.arg65, 67)
assertEquals(instance.arg64, 64)
}
}
@JsonClass(generateAdapter = true)
class MultipleMasks(
val arg0: Long = 0,
val arg1: Long = 1,
val arg2: Long = 2,
val arg3: Long = 3,
val arg4: Long = 4,
val arg5: Long = 5,
val arg6: Long = 6,
val arg7: Long = 7,
val arg8: Long = 8,
val arg9: Long = 9,
val arg10: Long = 10,
val arg11: Long,
val arg12: Long = 12,
val arg13: Long = 13,
val arg14: Long = 14,
val arg15: Long = 15,
val arg16: Long = 16,
val arg17: Long = 17,
val arg18: Long = 18,
val arg19: Long = 19,
@Suppress("UNUSED_PARAMETER") arg20: Long = 20,
val arg21: Long = 21,
val arg22: Long = 22,
val arg23: Long = 23,
val arg24: Long = 24,
val arg25: Long = 25,
val arg26: Long = 26,
val arg27: Long = 27,
val arg28: Long = 28,
val arg29: Long = 29,
val arg30: Long = 30,
val arg31: Long = 31,
val arg32: Long = 32,
val arg33: Long = 33,
val arg34: Long = 34,
val arg35: Long = 35,
val arg36: Long = 36,
val arg37: Long = 37,
val arg38: Long = 38,
@Transient val arg39: Long = 39,
val arg40: Long = 40,
val arg41: Long = 41,
val arg42: Long = 42,
val arg43: Long = 43,
val arg44: Long = 44,
val arg45: Long = 45,
val arg46: Long = 46,
val arg47: Long = 47,
val arg48: Long = 48,
val arg49: Long = 49,
val arg50: Long = 50,
val arg51: Long = 51,
val arg52: Long = 52,
@Transient val arg53: Long = 53,
val arg54: Long = 54,
val arg55: Long = 55,
val arg56: Long = 56,
val arg57: Long = 57,
val arg58: Long = 58,
val arg59: Long = 59,
val arg60: Long = 60,
val arg61: Long = 61,
val arg62: Long = 62,
val arg63: Long = 63,
val arg64: Long = 64,
val arg65: Long = 65
)

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen.annotation
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import java.util.Locale
@JsonQualifier
annotation class UppercaseInAnnotationPackage
class UppercaseInAnnotationPackageJsonAdapter {
@ToJson
fun toJson(@UppercaseInAnnotationPackage s: String): String {
return s.uppercase(Locale.US)
}
@FromJson
@UppercaseInAnnotationPackage
fun fromJson(s: String): String {
return s.lowercase(Locale.US)
}
}