Skip to content

Commit

Permalink
Support global property naming strategy (#846)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored May 22, 2024
1 parent f26bea5 commit a72282c
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.ConfigurationInject;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.serde.LimitingStream;
import io.micronaut.serde.config.naming.PropertyNamingStrategy;

import java.util.List;
import java.util.Locale;
Expand All @@ -44,6 +46,8 @@ final class DefaultSerdeConfiguration implements SerdeConfiguration {
private final List<String> includedIntrospectionPackages;
private final int maximumNestingDepth;
private final boolean inetAddressAsNumeric;
private final String propertyNamingStrategyName;
private final PropertyNamingStrategy propertyNamingStrategy;

@ConfigurationInject
DefaultSerdeConfiguration(Optional<String> dateFormat,
Expand All @@ -54,7 +58,8 @@ final class DefaultSerdeConfiguration implements SerdeConfiguration {
Optional<TimeZone> timeZone,
@Bindable(defaultValue = "io.micronaut") List<String> includedIntrospectionPackages,
@Bindable(defaultValue = LimitingStream.DEFAULT_MAXIMUM_DEPTH + "") int maximumNestingDepth,
@Bindable(defaultValue = DEFAULT_INET_ADDRESS_AS_NUMERIC + "") boolean inetAddressAsNumeric) {
@Bindable(defaultValue = DEFAULT_INET_ADDRESS_AS_NUMERIC + "") boolean inetAddressAsNumeric,
@Nullable String propertyNamingStrategy) {
this.dateFormat = dateFormat;
this.timeWriteShape = timeWriteShape;
this.numericTimeUnit = numericTimeUnit;
Expand All @@ -64,6 +69,18 @@ final class DefaultSerdeConfiguration implements SerdeConfiguration {
this.includedIntrospectionPackages = includedIntrospectionPackages;
this.maximumNestingDepth = maximumNestingDepth;
this.inetAddressAsNumeric = inetAddressAsNumeric;
this.propertyNamingStrategyName = propertyNamingStrategy;
this.propertyNamingStrategy = PropertyNamingStrategy.forName(propertyNamingStrategy).orElse(null);
}

@Override
public PropertyNamingStrategy getPropertyNamingStrategy() {
return propertyNamingStrategy;
}

@Override
public String getPropertyNamingStrategyName() {
return propertyNamingStrategyName;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package io.micronaut.serde.config;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.serde.LimitingStream;
import io.micronaut.serde.config.naming.PropertyNamingStrategy;

import java.net.InetAddress;
import java.util.List;
Expand Down Expand Up @@ -105,6 +107,24 @@ default boolean isInetAddressAsNumeric() {
@Bindable(defaultValue = LimitingStream.DEFAULT_MAXIMUM_DEPTH + "")
int getMaximumNestingDepth();

/**
* @return The property naming strategy name
* @since 2.10
*/
@Nullable
default String getPropertyNamingStrategyName() {
return null;
}

/**
* @return The property naming strategy
* @since 2.10
*/
@Nullable
default PropertyNamingStrategy getPropertyNamingStrategy() {
return PropertyNamingStrategy.forName(getPropertyNamingStrategyName()).orElse(null);
}

/**
* Shape to use for time serialization.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,34 +84,28 @@ public interface PropertyNamingStrategy {
*/
static Optional<PropertyNamingStrategy> forName(String namingStrategy) {
if (StringUtils.isNotEmpty(namingStrategy)) {
switch (namingStrategy) {
case "KEBAB_CASE":
case "io.micronaut.serde.config.naming.KebabCaseStrategy":
return Optional.of(KEBAB_CASE);
case "IDENTITY":
case "io.micronaut.serde.config.naming.IdentityStrategy":
return Optional.of(IDENTITY);
case "LOWER_CASE":
case "io.micronaut.serde.config.naming.LowerCaseStrategy":
return Optional.of(LOWER_CASE);
case "LOWER_DOT_CASE":
case "io.micronaut.serde.config.naming.LowerDotCaseStrategy":
return Optional.of(LOWER_DOT_CASE);
case "SNAKE_CASE":
case "io.micronaut.serde.config.naming.SnakeCaseStrategy":
return Optional.of(SNAKE_CASE);
case "UPPER_CAMEL_CASE":
case "io.micronaut.serde.config.naming.UpperCamelCaseStrategy":
return Optional.of(UPPER_CAMEL_CASE);
case "LOWER_CAMEL_CASE":
case "io.micronaut.serde.config.naming.LowerCamelCaseStrategy":
return Optional.of(LOWER_CAMEL_CASE);
case "UPPER_CAMEL_CASE_WITH_SPACES":
case "io.micronaut.serde.config.naming.UpperCamelCaseStrategyWithSpaces":
return Optional.of(UPPER_CAMEL_CASE_WITH_SPACES);
default:
return Optional.empty();
}
return switch (namingStrategy) {
case "KEBAB_CASE", "io.micronaut.serde.config.naming.KebabCaseStrategy" ->
Optional.of(KEBAB_CASE);
case "IDENTITY", "io.micronaut.serde.config.naming.IdentityStrategy" ->
Optional.of(IDENTITY);
case "LOWER_CASE", "io.micronaut.serde.config.naming.LowerCaseStrategy" ->
Optional.of(LOWER_CASE);
case "LOWER_DOT_CASE", "io.micronaut.serde.config.naming.LowerDotCaseStrategy" ->
Optional.of(LOWER_DOT_CASE);
case "SNAKE_CASE", "io.micronaut.serde.config.naming.SnakeCaseStrategy" ->
Optional.of(SNAKE_CASE);
case "UPPER_CAMEL_CASE",
"io.micronaut.serde.config.naming.UpperCamelCaseStrategy" ->
Optional.of(UPPER_CAMEL_CASE);
case "LOWER_CAMEL_CASE",
"io.micronaut.serde.config.naming.LowerCamelCaseStrategy" ->
Optional.of(LOWER_CAMEL_CASE);
case "UPPER_CAMEL_CASE_WITH_SPACES",
"io.micronaut.serde.config.naming.UpperCamelCaseStrategyWithSpaces" ->
Optional.of(UPPER_CAMEL_CASE_WITH_SPACES);
default -> Optional.empty();
};
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.micronaut.serde.jackson

import io.micronaut.context.ApplicationContext
import io.micronaut.json.JsonMapper
import spock.lang.Specification

class GlobalPropertyStrategySpec extends Specification {

def 'test default global property strategy'() {
given:
def ctx = ApplicationContext.run()
def mapper = ctx.getBean(JsonMapper)

when:
def val = mapper.writeValueAsString(new MyBean("hello", 123))
then:
val == '{"fooBar":"hello","abcXyz":123}'

when:
def bean = mapper.readValue(val, MyBean)

then:
bean.fooBar() == "hello"
bean.abcXyz() == 123

cleanup:
ctx.close()
}

def 'test global property strategy SNAKE_CASE'() {
given:
def ctx = ApplicationContext.run(['micronaut.serde.property-naming-strategy': "SNAKE_CASE"])
def mapper = ctx.getBean(JsonMapper)

when:
def val = mapper.writeValueAsString(new MyBean("hello", 123))
then:
val == '{"foo_bar":"hello","abc_xyz":123}'

when:
def bean = mapper.readValue(val, MyBean)

then:
bean.fooBar() == "hello"
bean.abcXyz() == 123

cleanup:
ctx.close()
}

def 'test global property strategy LOWER_DOT_CASE'() {
given:
def ctx = ApplicationContext.run(['micronaut.serde.property-naming-strategy': "LOWER_DOT_CASE"])
def mapper = ctx.getBean(JsonMapper)

when:
def val = mapper.writeValueAsString(new MyBean("hello", 123))
then:
val == '{"foo.bar":"hello","abc.xyz":123}'

when:
def bean = mapper.readValue(val, MyBean)

then:
bean.fooBar() == "hello"
bean.abcXyz() == 123

cleanup:
ctx.close()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.micronaut.serde.jackson;

import io.micronaut.serde.annotation.Serdeable;

@Serdeable
record MyBean(String fooBar, int abcXyz) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.micronaut.serde.Decoder;
import io.micronaut.serde.Deserializer;
import io.micronaut.serde.config.DeserializationConfiguration;
import io.micronaut.serde.config.SerdeConfiguration;
import io.micronaut.serde.config.annotation.SerdeConfig;
import io.micronaut.serde.config.naming.PropertyNamingStrategy;
import io.micronaut.serde.exceptions.InvalidFormatException;
Expand Down Expand Up @@ -129,6 +130,7 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio
bounds = Collections.emptyMap();
}

PropertyNamingStrategy defaultPropertyNamingStrategy = decoderContext.getSerdeConfiguration().map(SerdeConfiguration::getPropertyNamingStrategy).orElse(null);
this.conversionService = decoderContext.getConversionService();
this.introspection = introspection;
final SerdeConfig.SerCreatorMode creatorMode = introspection
Expand All @@ -139,7 +141,7 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio
hasBuilder = introspection.hasBuilder();
final Argument<?>[] constructorArguments = hasBuilder ? introspection.builder().getBuildMethodArguments() : introspection.getConstructorArguments();
creatorSize = constructorArguments.length;
PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, decoderContext, null);
PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, decoderContext, defaultPropertyNamingStrategy);

Set<String> ignoredProperties = new HashSet<>();
Set<String> externalProperties = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.micronaut.serde.PropertyFilter;
import io.micronaut.serde.SerdeIntrospections;
import io.micronaut.serde.Serializer;
import io.micronaut.serde.config.SerdeConfiguration;
import io.micronaut.serde.config.SerializationConfiguration;
import io.micronaut.serde.config.annotation.SerdeConfig;
import io.micronaut.serde.config.naming.PropertyNamingStrategy;
Expand Down Expand Up @@ -121,7 +122,8 @@ public int getOrder() {
@Nullable
Predicate<String> argumentPropertyPredicate = serdeArgumentConf == null ? null : serdeArgumentConf.resolveAllowPropertyPredicate(allowIgnoredProperties);

PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, encoderContext, null);
PropertyNamingStrategy defaultPropertyNamingStrategy = encoderContext.getSerdeConfiguration().map(SerdeConfiguration::getPropertyNamingStrategy).orElse(null);
PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, encoderContext, defaultPropertyNamingStrategy);
final Collection<Map.Entry<BeanReadProperty<T, Object>, AnnotationMetadata>> properties =
introspection.getBeanReadProperties().stream()
.filter(this::filterProperty)
Expand Down

0 comments on commit a72282c

Please sign in to comment.