Skip to content

Commit

Permalink
[feature] skeleton to support maps in configuration beans, idea is to…
Browse files Browse the repository at this point in the history
… use properties for primitives (+string) and nested key value list syntax for nested objects
  • Loading branch information
rmannibucau committed Jan 27, 2025
1 parent bf69f9c commit 1f82979
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ public RuntimeContainerImpl(final Beans beans, final Contexts contexts, final Li
this.listeners = listeners;
}

/**
* @deprecated prefer {@link #clearCache()}.
*/
@Deprecated
public void clearLookupCaches() {
slowLookupMatchings.clear();
listMatchings.clear();
clearCache();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ If you want to customize the name of the property you can use `@Property`.
Finally, you can register you own source of values creating a bean of type `ConfigurationSource`.

IMPORTANT: `List<OtherConfig>` are supported, but you must set in the configuration `<prefix for this list>.length` to the length value of the list then the nested instances are configured using `<prefix>.<index>` starting at index 0. Ex: `myconf.mylist.0.name=foo`.
Similarly, `Map<String,X>` are supported. For primitives it uses properties syntax and when X is another configuration model you must use an index based notation with `length` property and `key`, `value` suffixes.

== Create a JSON model [[json_model]]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@
import javax.lang.model.element.QualifiedNameable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
Expand Down Expand Up @@ -140,6 +145,16 @@ private String generateNestedClass(final TypeElement element, final String typeN
" }\n" +
" return list;\n" +
" }\n" +
"\n" +
" private static " + Map.class.getName() + "<String, " + typeName + "> map(final " +
Configuration.class.getName() + " configuration, final String prefix) {\n" +
" final int length = configuration.get(prefix + \".length\").map(Integer::parseInt).orElse(0);\n" +
" final var map = new " + LinkedHashMap.class.getName() + "<String, " + typeName + ">(length);\n" +
" for (int index = 0; index < length; index++) {\n" +
" map.put(configuration.get(prefix + \".\" + index + \".key\").orElseThrow(), new " + name + "(configuration, prefix + \".\" + index + \".value\").get());\n" +
" }\n" +
" return map;\n" +
" }\n" +
" }\n";
} finally {
docStack.removeLast();
Expand Down Expand Up @@ -248,6 +263,62 @@ private String newParamInstance(final Element param, final String propPrefix,
return nestedFactory(itemString) + ".list(configuration, " + name + ")";
}

//
// map<string,x>
//

if (type instanceof DeclaredType dt &&
dt.getTypeArguments().size() == 2 &&
dt.asElement() instanceof TypeElement te &&
te.getQualifiedName().contentEquals(Map.class.getName()) &&
String.class.getName().equals(dt.getTypeArguments().get(0).toString())) {
final var valueType = dt.getTypeArguments().get(1);
final var valueTypeString = valueType.toString();
if (String.class.getName().equals(valueTypeString) || CharSequence.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(""), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (boolean.class.getName().equals(valueTypeString) || Boolean.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(Boolean::parseBoolean)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (int.class.getName().equals(valueTypeString) || Integer.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(Integer::parseInt)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (long.class.getName().equals(valueTypeString) || Long.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(Long::parseLong)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (float.class.getName().equals(valueTypeString) || Float.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(Float::parseFloat)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (double.class.getName().equals(valueTypeString) || Double.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(Double::parseDouble)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (BigInteger.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(" + BigInteger.class.getName() + "::new)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (BigDecimal.class.getName().equals(valueTypeString)) {
return lookup(name, required, mapOf(".map(" + BigDecimal.class.getName() + "::new)"), defaultValue != null ? defaultValue : "null", docName, desc);
}
final var dtElt = processingEnv.getTypeUtils().asElement(valueType);
if (dtElt != null && dtElt.getKind() == ENUM) {
return lookup(name, required, mapOf(".map(" + valueTypeString + "::" + enumValueOf(dtElt) + ")"), defaultValue != null ? defaultValue : "null", docName, desc);
}
if (valueTypeString.startsWith("java.")) { // unsupported
processingEnv.getMessager().printMessage(ERROR, "Type not supported: '" + typeStr + "' (" + element + "." + param.getSimpleName() + ")");
return "null";
}

// nested list of objects, here we need to use prefixed notation
if (!nestedClasses.containsKey(valueTypeString)) {
nestedClasses.put(valueTypeString, generateNestedClass(
(TypeElement) processingEnv.getTypeUtils().asElement(valueType), valueTypeString, null, nestedClasses));
}

this.docStack.getLast().items().addAll(List.of(
new Docs.DocItem(docName + ".$index.key", desc + " (Key).", required, "java.lang.String", "null"),
new Docs.DocItem(docName + ".$index.value", desc + " (Value).", required, valueTypeString, "null")));
return nestedFactory(valueTypeString) + ".map(configuration, " + name + ")";
}

if (typeStr.startsWith("java.")) { // unsupported
processingEnv.getMessager().printMessage(ERROR, "Type not supported: '" + typeStr + "' (" + element + "." + param.getSimpleName() + ")");
return "null";
Expand Down Expand Up @@ -300,6 +371,21 @@ private String listOf(final String valueMapper) {
".toList())";
}

private String mapOf(final String valueMapper) {
// if value starts with sep=x,... then we split on "x" instead of ","
return ".map(value -> {\n" +
" var props = new " + Properties.class.getName() + "();\n" +
" try (final var reader = new " + StringReader.class.getName() + "(value)) {\n" +
" props.load(reader);\n" +
" } catch (Exception e) { /* ignore for now */ }\n" +
" return props;\n" +
" })\n" +
" .map(it -> it.stringPropertyNames().stream()\n" +
" .collect(" + Collectors.class.getName() + ".toMap(\n" +
" " + Function.class.getName() + ".identity()," +
" " + (valueMapper.isEmpty() ? "it::getProperty" : Function.class.getName() + ".identity().andThen(it::getProperty).andThen(" + valueMapper + ")") + ")))";
}

private String nestedFactory(final String typeStr) {
return typeStr.substring(typeStr.lastIndexOf('.') + 1) + "__NestedFactory";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@
import io.yupiik.fusion.framework.api.Instance;
import io.yupiik.fusion.framework.api.RuntimeContainer;
import io.yupiik.fusion.framework.api.configuration.Configuration;
import io.yupiik.fusion.framework.api.configuration.ConfigurationSource;
import io.yupiik.fusion.framework.api.container.FusionBean;
import io.yupiik.fusion.framework.api.container.FusionListener;
import io.yupiik.fusion.framework.api.container.FusionModule;
import io.yupiik.fusion.framework.api.container.RuntimeContainerImpl;
import io.yupiik.fusion.framework.api.container.Types;
import io.yupiik.fusion.framework.api.container.bean.BaseBean;
import io.yupiik.fusion.framework.api.container.bean.ProvidedInstanceBean;
import io.yupiik.fusion.framework.api.container.context.ApplicationFusionContext;
import io.yupiik.fusion.framework.api.container.context.subclass.DelegatingContext;
import io.yupiik.fusion.framework.api.container.context.subclass.SupplierDelegatingContext;
import io.yupiik.fusion.framework.api.main.Args;
import io.yupiik.fusion.framework.api.scope.ApplicationScoped;
import io.yupiik.fusion.framework.api.scope.DefaultScoped;
import io.yupiik.fusion.framework.processor.test.CompilationClassLoader;
import io.yupiik.fusion.framework.processor.test.Compiler;
Expand All @@ -55,6 +59,7 @@
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
Expand Down Expand Up @@ -581,13 +586,78 @@ void configuration(@TempDir final Path work) throws IOException {
" \"test.p.RecordConfiguration\"\n" +
" ]\n" +
"}",
new SimplePrettyFormatter(new JsonMapperImpl(java.util.List.of(), key -> Optional.empty())).apply(new String(in.readAllBytes(), UTF_8)));
simpleFormat(in));
} catch (final IOException e) {
fail(e);
}
});
}


@Test
void configurationMap(@TempDir final Path work) throws IOException {
new Compiler(work, "MapConfiguration")
.compileAndAsserts((loader, container) -> {
// handle the configuration with a custom config source
container.getBeans().doRegister(new BaseBean<ConfigurationSource>(ConfigurationSource.class, DefaultScoped.class, 0, Map.of()) {
@Override
public ConfigurationSource create(RuntimeContainer container, List<Instance<?>> dependents) {
return new ConfigurationSource() {
@Override
public String get(final String key) {
return switch (key) {
case "conf.keyValues" -> """
# properties syntax
a = 1
""";
default -> null;
};
}
};
}
});
// clean the bean to use our custom config source
if (container.getContexts().findContext(ApplicationScoped.class).orElseThrow() instanceof ApplicationFusionContext ac){
ac.clean(container.lookup(Configuration.class).bean());
}
if (container instanceof RuntimeContainerImpl r) {
r.clearCache();
}

try (final var instance = container.lookup(loader.apply("test.p.MapConfiguration"))) {
// model
assertEquals(
"MapConfiguration[keyValues={a=1}]",
instance.instance().toString());

// doc
try (final var in = requireNonNull(instance.instance().getClass().getClassLoader()
.getResourceAsStream("META-INF/fusion/configuration/documentation.json"))) {
assertEquals("""
{
"version": 1,
"classes": {
"test.p.MapConfiguration": [
{
"name": "conf.keyValues",
"documentation": "",
"defaultValue": null,
"required": false
}
]
},
"roots": [
"test.p.MapConfiguration"
]
}""",
simpleFormat(in));
} catch (final IOException e) {
fail(e);
}
}
});
}

@Test
void requiredConfigurationParam(@TempDir final Path work) throws IOException {
final var clazz = "configuration.ConfigurationMissingParamMessage";
Expand Down Expand Up @@ -2290,6 +2360,10 @@ private DynamicTest lockedDynamicTest(final Semaphore semaphore, final AtomicInt
});
}

private String simpleFormat(InputStream in) throws IOException {
return new SimplePrettyFormatter(new JsonMapperImpl(List.of(), key -> Optional.empty())).apply(new String(in.readAllBytes(), UTF_8));
}

private record SimpleRequest(String method, String path, Map<String, Object> attributes,
Flow.Publisher<ByteBuffer> body) implements Request {
private SimpleRequest() {
Expand Down
25 changes: 25 additions & 0 deletions fusion-processor/src/test/resources/test/p/MapConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* 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
*
* http://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 test.p;

import io.yupiik.fusion.framework.build.api.configuration.Property;
import io.yupiik.fusion.framework.build.api.configuration.RootConfiguration;

import java.util.Map;

@RootConfiguration("conf")
public record MapConfiguration(@Property Map<String, String> keyValues) {
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
<release>${maven.compiler.release}</release>
<encoding>UTF-8</encoding>
<parameters>true</parameters>
<proc>full</proc>
<compilerArgs>
<compilerArg>-Xlint:unchecked</compilerArg>
</compilerArgs>
Expand Down

0 comments on commit 1f82979

Please sign in to comment.