Skip to content

Commit

Permalink
GH-155 Optional values and sections support (#155)
Browse files Browse the repository at this point in the history
* Add optional values and sections support. Add unit tests for optional values and sections.

* (cleanup) Use no deprecated method. Use only CdnDeserializer#createInstance() method to create instance.
  • Loading branch information
Rollczi authored Aug 3, 2022
1 parent 87ae82e commit 946d172
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package net.dzikoysk.cdn.feature

import net.dzikoysk.cdn.CdnSpec
import net.dzikoysk.cdn.entity.Contextual
import net.dzikoysk.cdn.entity.Description
import net.dzikoysk.cdn.loadAs
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import panda.std.ResultAssertions.assertOk

class OptionalSectionTest : CdnSpec() {

class ConfigurationWithOptionalValues {

@Description("// Normal value")
var normal = "value"

@Description("// Optional section")
var optionalSection: Section? = null

@Contextual
class Section {
@Description("// Other value in section")
var other = "value"
@Description("// Optional value")
var optional: String? = null;
}

}

@Test
fun `should render without optional section`() {
val renderResult = standard.render(ConfigurationWithOptionalValues())
val render = assertOk(renderResult)

assertEquals("""
// Normal value
normal: value
""".trimIndent(), render)
}

@Test
fun `should render section after without optional value`() {
val modified = ConfigurationWithOptionalValues()

modified.optionalSection = ConfigurationWithOptionalValues.Section()

val renderResult = standard.render(modified)
val render = assertOk(renderResult)

assertEquals("""
// Normal value
normal: value
// Optional section
optionalSection {
// Other value in section
other: value
}
""".trimIndent(), render)
}

@Test
fun `should load without optional value`() {
val loadResult = standard.loadAs<ConfigurationWithOptionalValues> { """
// Normal value
normal: value
""".trimIndent() }

val load = assertOk(loadResult)

assertEquals("value", load.normal)
assertNull(load.optionalSection)
}

@Test
fun `should load all values`() {
val loadResult = standard.loadAs<ConfigurationWithOptionalValues> { """
// Normal value
normal: value
// Optional section
optionalSection {
// Other value in section
other: otherValue
// Optional value
optional: optionalValue
}
""".trimIndent() }

val load = assertOk(loadResult)

assertEquals("value", load.normal)
assertNotNull(load.optionalSection)
assertEquals("otherValue", load.optionalSection?.other)
assertEquals("optionalValue", load.optionalSection?.optional)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package net.dzikoysk.cdn.feature

import net.dzikoysk.cdn.CdnSpec
import net.dzikoysk.cdn.entity.Description
import net.dzikoysk.cdn.loadAs
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import panda.std.ResultAssertions.assertOk

class OptionalValueTest : CdnSpec() {

class ConfigurationWithOptionalValues {

@Description("// Normal value")
var normal = "value"

@Description("// Optional value")
var optional: String? = null

}

@Test
fun `should render without optional value`() {
val renderResult = standard.render(ConfigurationWithOptionalValues())
val render = assertOk(renderResult)

assertEquals("""
// Normal value
normal: value
""".trimIndent(), render)
}

@Test
fun `should render with all values after modification`() {
val modified = ConfigurationWithOptionalValues()

modified.optional = "newValue"

val renderResult = standard.render(modified)
val render = assertOk(renderResult)

assertEquals("""
// Normal value
normal: value
// Optional value
optional: newValue
""".trimIndent(), render)
}

@Test
fun `should load without optional value`() {
val loadResult = standard.loadAs<ConfigurationWithOptionalValues> { """
// Normal value
normal: value
""".trimIndent() }

val load = assertOk(loadResult)

assertEquals("value", load.normal)
assertNull(load.optional)
}

@Test
fun `should load all values`() {
val loadResult = standard.loadAs<ConfigurationWithOptionalValues> { """
// Normal value
normal: value
// Optional value
optional: newValue
""".trimIndent() }

val load = assertOk(loadResult)

assertEquals("value", load.normal)
assertEquals("newValue", load.optional)
}

}
87 changes: 47 additions & 40 deletions cdn/src/main/java/net/dzikoysk/cdn/serdes/CdnDeserializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,9 @@ public CdnDeserializer(CdnSettings settings) {
}

public Result<T, ? extends CdnException> deserialize(Section source, Class<T> template) {
return Result.<T, Exception>attempt(ReflectiveOperationException.class, () -> {
if (!settings.getAnnotationResolver().getVisibilityToMatch().isAccessible()) {
Constructor<T> constructor = template.getDeclaredConstructor();

constructor.setAccessible(true);
return constructor.newInstance();
}

return template.getConstructor().newInstance();
}).flatMap(instance -> deserialize(source, instance))
.mapErr(CdnException::new);
return this.createInstance(template)
.mapErr(exception -> new CdnException("Failed to create instance of " + template.getName(), exception))
.flatMap(instance -> deserialize(source, instance));
}

public Result<T, ? extends CdnException> deserialize(Section source, T instance) {
Expand Down Expand Up @@ -88,35 +80,22 @@ public CdnDeserializer(CdnSettings settings) {
}

Object argumentValue = result.get()
.orElse(() -> annotatedMember.getValue(instance).orElseThrow(RuntimeException::new))
.orElse(() -> annotatedMember.getValue(instance).orThrow(RuntimeException::new))
.orNull();

args.add(new Pair<>(annotatedMember.getType(), argumentValue));
}

if (immutable) {
return Result.attempt(ReflectiveOperationException.class, () -> {
Class<?>[] argsTypes = args.stream()
.map(Pair::getFirst)
.toArray(Class[]::new);

Object[] values = args.stream()
.map(Pair::getSecond)
.toArray();
Class<?>[] argsTypes = args.stream()
.map(Pair::getFirst)
.toArray(Class[]::new);

if (!settings.getAnnotationResolver().getVisibilityToMatch().isAccessible()) {
Constructor<?> constructor = instance.getClass().getDeclaredConstructor(argsTypes);
Object[] values = args.stream()
.map(Pair::getSecond)
.toArray();

constructor.setAccessible(true);
//noinspection unchecked
return (I) constructor.newInstance(values);
}

//noinspection unchecked
return (I) instance.getClass()
.getConstructor(argsTypes)
.newInstance(values);
});
return this.createInstance(instance.getClass(), argsTypes, values).projectToError();
}

return ok(instance);
Expand All @@ -140,26 +119,54 @@ public CdnDeserializer(CdnSettings settings) {
return defaultValueResult.projectToError();
}

Option<Object> defaultValue = defaultValueResult.get();

if (defaultValue.isEmpty()) {
return ok(none());
}
Option<Object> optionDefaultValue = defaultValueResult.get();

if (member.isAnnotationPresent(Contextual.class)) {
return deserializeToSection((Section) element, immutable, defaultValue.get()).map(Option::of);
Section section = (Section) element;
Object defaultValue;

if (optionDefaultValue.isEmpty()) {
Result<?, ReflectiveOperationException> sectionInstance = createInstance(member.getType());

if (sectionInstance.isErr()) {
return sectionInstance.projectToError();
}

defaultValue = sectionInstance.get();
} else {
defaultValue = optionDefaultValue.get();
}

return deserializeToSection(section, immutable, defaultValue).map(Option::of);
}

TargetType targetType = member.getTargetType();

return CdnUtils.findComposer(settings, targetType, member)
.flatMap(deserializer -> deserializer.deserialize(settings, element, targetType, defaultValue.get(), false))
.flatMap(deserializer -> deserializer.deserialize(settings, element, targetType, optionDefaultValue.orNull(), false))
.peek(value -> {
if (!immutable && value != Composer.MEMBER_ALREADY_PROCESSED) {
member.setValue(instance, value).orElseThrow(IllegalStateException::new);
member.setValue(instance, value).orThrow(IllegalStateException::new);
}
})
.map(Option::of);
}

private <TYPE> Result<TYPE, ReflectiveOperationException> createInstance(Class<TYPE> type) {
return createInstance(type, new Class[0], new Object[0]);
}

private <TYPE> Result<TYPE, ReflectiveOperationException> createInstance(Class<TYPE> type, Class<?>[] argsTypes, Object[] args) {
return Result.attempt(ReflectiveOperationException.class, () -> {
if (!settings.getAnnotationResolver().getVisibilityToMatch().isAccessible()) {
Constructor<TYPE> constructor = type.getDeclaredConstructor(argsTypes);

constructor.setAccessible(true);
return constructor.newInstance(args);
}

return type.getConstructor(argsTypes).newInstance(args);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package net.dzikoysk.cdn.serdes.composers;

import net.dzikoysk.cdn.CdnException;
import net.dzikoysk.cdn.CdnSettings;
import net.dzikoysk.cdn.CdnUtils;
import net.dzikoysk.cdn.model.Element;
Expand All @@ -31,6 +32,10 @@ public final class ReferenceComposer<T> implements Composer<T> {

@Override
public Result<T, Exception> deserialize(CdnSettings settings, Element<?> source, TargetType type, T defaultValue, boolean entryAsRecord) {
if (defaultValue == null) {
return Result.error(new CdnException("Default value of reference cannot be null!"));
}

Reference<Object> defaultReference = (Reference<Object>) defaultValue;
TargetType referenceType = type.getAnnotatedActualTypeArguments()[0];

Expand Down

0 comments on commit 946d172

Please sign in to comment.