Skip to content

Commit 388860e

Browse files
committed
Add an option for configs to return provided keys
1 parent 3acd938 commit 388860e

File tree

10 files changed

+206
-26
lines changed

10 files changed

+206
-26
lines changed

annotations/src/main/java/org/neo4j/gds/annotation/Configuration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@
133133
@interface CollectKeys {
134134
}
135135

136+
/**
137+
* Use this annotation on a method to return the keys that have benn provided at runtime.
138+
* <p>
139+
* This is different from {@link org.neo4j.gds.annotation.Configuration.CollectKeys} in that it collects the keys provided at runtime, not the keys that could have been proviced.
140+
*/
141+
@Documented
142+
@Target(ElementType.METHOD)
143+
@Retention(RetentionPolicy.RUNTIME)
144+
@interface CollectProvidedKeys {
145+
}
146+
136147
/**
137148
* Annotated function will return the map representation of the configuration.
138149
* The return type of the method must be of type Map&lt;String, Object&gt;.

config-generator/src/main/java/org/neo4j/gds/proc/ConfigParser.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ private Optional<Spec.Member> validateMember(
212212
.method(method);
213213

214214
validateCollectKeys(method, memberBuilder);
215+
validateCollectProvidedKeys(method, memberBuilder);
215216
validateToMap(method, memberBuilder);
216217

217218
memberBuilder.validatesIntegerRange(isAnnotationPresent(method, Configuration.IntegerRange.class));
@@ -293,6 +294,24 @@ private void validateCollectKeys(ExecutableElement method, MemberBuilder memberB
293294
}
294295
}
295296

297+
private void validateCollectProvidedKeys(ExecutableElement method, MemberBuilder memberBuilder) {
298+
if (isAnnotationPresent(method, Configuration.CollectProvidedKeys.class)) {
299+
TypeElement collectionType = elementUtils.getTypeElement(Collection.class.getTypeName());
300+
TypeMirror stringType = elementUtils.getTypeElement(String.class.getTypeName()).asType();
301+
DeclaredType collectionOfStringType = typeUtils.getDeclaredType(collectionType, stringType);
302+
303+
if (!typeUtils.isSubtype(method.getReturnType(), collectionOfStringType)) {
304+
messager.printMessage(
305+
Diagnostic.Kind.ERROR,
306+
"Method must return Collection<String>",
307+
method
308+
);
309+
}
310+
311+
memberBuilder.collectsProvidedKeys(true);
312+
}
313+
}
314+
296315
private void validateGraphStoreValidationAndChecks(
297316
ExecutableElement method,
298317
MemberBuilder memberBuilder

config-generator/src/main/java/org/neo4j/gds/proc/GenerateConfiguration.java

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import javax.lang.model.element.PackageElement;
4848
import javax.lang.model.element.TypeElement;
4949
import javax.lang.model.type.DeclaredType;
50+
import javax.lang.model.type.TypeKind;
5051
import javax.lang.model.type.TypeMirror;
5152
import javax.lang.model.util.Elements;
5253
import javax.lang.model.util.Types;
@@ -181,13 +182,20 @@ private static FieldDefinitions defineFields(Spec config) {
181182
NameAllocator names = new NameAllocator();
182183

183184
ImmutableFieldDefinitions.Builder builder = ImmutableFieldDefinitions.builder().names(names);
184-
config.members().stream().filter(Spec.Member::isConfigValue).map(member ->
185-
FieldSpec.builder(
186-
member.typeSpecWithAnnotation(Nullable.class),
187-
names.newName(member.methodName(), member),
188-
Modifier.PRIVATE
189-
).build()
190-
).forEach(builder::addField);
185+
config.members().stream()
186+
.filter(m -> m.isConfigValue() || m.collectsProvidedKeys())
187+
.map(member -> {
188+
var fieldBuilder = FieldSpec.builder(
189+
member.typeSpecWithAnnotation(Nullable.class),
190+
names.newName(member.methodName(), member),
191+
Modifier.PRIVATE
192+
);
193+
if (member.collectsProvidedKeys()) {
194+
fieldBuilder.addModifiers(Modifier.FINAL);
195+
}
196+
return fieldBuilder.build();
197+
})
198+
.forEach(builder::addField);
191199
return builder.build();
192200
}
193201

@@ -202,6 +210,19 @@ private MethodSpec defineConstructor(
202210

203211
boolean requiredMapParameter = false;
204212

213+
for (var implMember : implMembers) {
214+
if (implMember.member().collectsProvidedKeys()) {
215+
requiredMapParameter = true;
216+
constructorMethodBuilder.addStatement(
217+
"this.$N = $T.copyOf($N.$N())",
218+
implMember.fieldName(),
219+
Set.class,
220+
implMember.configParamName(),
221+
implMember.methodName()
222+
);
223+
}
224+
}
225+
205226
String errorsVarName = names.newName("errors");
206227
if (!implMembers.isEmpty()) {
207228
constructorMethodBuilder.addStatement(
@@ -218,7 +239,7 @@ private MethodSpec defineConstructor(
218239
if (parsedMember.isConfigMapEntry()) {
219240
requiredMapParameter = true;
220241
addConfigFieldToConstructor(constructorMethodBuilder, implMember, errorsVarName);
221-
} else {
242+
} else if (parsedMember.isConfigParameter()) {
222243
addParameterToConstructor(
223244
constructorMethodBuilder,
224245
implMember,
@@ -452,6 +473,20 @@ private void addParameterToConstructor(
452473
}
453474

454475
private Optional<MemberDefinition> memberDefinition(NameAllocator names, Spec.Member member) {
476+
if (member.collectsProvidedKeys()) {
477+
return Optional.of(ImmutableMemberDefinition
478+
.builder()
479+
.member(member)
480+
.fieldName(names.get(member))
481+
.configParamName(names.get(CONFIG_VAR))
482+
.configKey(member.lookupKey())
483+
.fieldType(member.method().getReturnType())
484+
.methodName("keySet")
485+
.parameterType(this.typeUtils.getNoType(TypeKind.VOID))
486+
.methodPrefix("")
487+
.build());
488+
}
489+
455490
if (!member.isConfigValue()) {
456491
return Optional.empty();
457492
}
@@ -717,7 +752,7 @@ private Optional<MemberDefinition> memberDefinition(
717752
member.method()
718753
);
719754
} else {
720-
maybeInnerType = Optional.of(ClassName.get(asTypeElement(typeArguments.get(0))));
755+
maybeInnerType = Optional.of(ClassName.get(asTypeElement(typeArguments.getFirst())));
721756
}
722757
}
723758

@@ -755,9 +790,9 @@ private Optional<MemberDefinition> memberDefinition(
755790
builder
756791
.methodPrefix("get")
757792
.defaultProvider(CodeBlock.of(
758-
"$T.super.$N()",
759-
member.owner().asType(),
760-
member.methodName()
793+
"$T.super.$N()",
794+
member.owner().asType(),
795+
member.methodName()
761796
)
762797
);
763798
}

config-generator/src/main/java/org/neo4j/gds/proc/GenerateConfigurationMethods.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static Optional<MethodSpec> generateMethodCode(
6666
GenerateConfigurationMethods.injectToMapCode(config, builder);
6767
} else if (member.graphStoreValidation()) {
6868
GenerateConfigurationMethods.graphStoreValidationCode(member, config, names, builder);
69-
} else if (member.isConfigValue()) {
69+
} else if (member.isConfigValue() || member.collectsProvidedKeys()) {
7070
builder.addStatement("return this.$N", names.get(member));
7171
} else {
7272
return Optional.empty();
@@ -79,7 +79,7 @@ private static void injectToMapCode(Spec config, MethodSpec.Builder builder) {
7979
.members()
8080
.stream()
8181
.filter(Spec.Member::isConfigMapEntry)
82-
.collect(Collectors.toList());
82+
.toList();
8383

8484
switch (configMembers.size()) {
8585
case 0:
@@ -150,18 +150,17 @@ private static CodeBlock collectKeysCode(Spec config) {
150150
.map(Spec.Member::lookupKey)
151151
.collect(Collectors.toCollection(LinkedHashSet::new));
152152

153-
switch (configKeys.size()) {
154-
case 0:
155-
return CodeBlock.of("return $T.emptyList()", Collections.class);
156-
case 1:
157-
return CodeBlock.of("return $T.singleton($S)", Collections.class, configKeys.iterator().next());
158-
default:
153+
return switch (configKeys.size()) {
154+
case 0 -> CodeBlock.of("return $T.emptyList()", Collections.class);
155+
case 1 -> CodeBlock.of("return $T.singleton($S)", Collections.class, configKeys.iterator().next());
156+
default -> {
159157
CodeBlock keys = configKeys
160158
.stream()
161159
.map(name -> CodeBlock.of("$S", name))
162160
.collect(CodeBlock.joining(", "));
163-
return CodeBlock.of("return $T.asList($L)", Arrays.class, keys);
164-
}
161+
yield CodeBlock.of("return $T.asList($L)", Arrays.class, keys);
162+
}
163+
};
165164
}
166165

167166
private static void graphStoreValidationCode(
@@ -172,7 +171,7 @@ private static void graphStoreValidationCode(
172171
) {
173172
var graphStoreValidationMethods = config.members().stream()
174173
.filter(Spec.Member::graphStoreValidationCheck)
175-
.collect(Collectors.toList());
174+
.toList();
176175
var parameters = validationMethod.method().getParameters();
177176

178177
String errorsVarName = names.newName("errors");

config-generator/src/main/java/org/neo4j/gds/proc/Spec.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ record Member(
5252
@NotNull ExecutableElement method,
5353
@Nullable String lookupKey,
5454
boolean collectsKeys,
55+
boolean collectsProvidedKeys,
5556
boolean toMap,
5657
boolean validatesIntegerRange,
5758
boolean validatesLongRange,
@@ -71,7 +72,7 @@ record Member(
7172
if (!trimmedKey.equals(resolvedKey)) {
7273
throw new InvalidMemberException("The key must not be surrounded by whitespace");
7374
}
74-
if (collectsKeys && (validates || normalizes)) {
75+
if ((collectsKeys || collectsProvidedKeys) && (validates || normalizes)) {
7576
throw new InvalidMemberException(String.format(
7677
Locale.ENGLISH,
7778
"Cannot combine @%s with @%s",
@@ -94,7 +95,7 @@ public String lookupKey() {
9495
}
9596

9697
boolean isConfigValue() {
97-
return !collectsKeys() && !toMap() && !validates() && !normalizes() && !graphStoreValidation() && !graphStoreValidationCheck();
98+
return !collectsKeys() && !collectsProvidedKeys() && !toMap() && !validates() && !normalizes() && !graphStoreValidation() && !graphStoreValidationCheck();
9899
}
99100

100101
boolean isConfigMapEntry() {

config-generator/src/test/java/org/neo4j/gds/proc/ConfigurationProcessorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ class ConfigurationProcessorTest {
5959
"Conversions",
6060
"ConvertingParameters",
6161
"CollectingKeys",
62+
"CollectingProvidedKeys",
6263
"ToMap",
6364
"Validation",
6465
"RangeValidation",
65-
"GSValidation"
66+
"GSValidation",
6667
})
6768
void positiveTest(String className) {
6869
assertContentEquals(className);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Neo4j is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
package positive;
21+
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
27+
import javax.annotation.processing.Generated;
28+
import org.jetbrains.annotations.NotNull;
29+
import org.neo4j.gds.core.CypherMapAccess;
30+
import org.neo4j.gds.core.CypherMapWrapper;
31+
32+
@Generated("org.neo4j.gds.proc.ConfigurationProcessor")
33+
public final class CollectingProvidedKeysConfig implements CollectingProvidedKeys {
34+
private final Set<String> providedKeys;
35+
36+
public CollectingKeysConfig(@NotNull CypherMapAccess config) {
37+
this.providedKeys = Set.copyOf(config.keySet());
38+
ArrayList<IllegalArgumentException> errors = new ArrayList<>();
39+
if (!errors.isEmpty()) {
40+
if (errors.size() == 1) {
41+
throw errors.get(0);
42+
} else {
43+
String combinedErrorMsg = errors
44+
.stream()
45+
.map(IllegalArgumentException::getMessage)
46+
.collect(Collectors.joining(System.lineSeparator() + "\t\t\t\t",
47+
"Multiple errors in configuration arguments:" + System.lineSeparator() + "\t\t\t\t",
48+
""
49+
));
50+
IllegalArgumentException combinedError = new IllegalArgumentException(combinedErrorMsg);
51+
errors.forEach(error -> combinedError.addSuppressed(error));
52+
throw combinedError;
53+
}
54+
}
55+
}
56+
57+
@Override
58+
public Set<String> providedKeys() {
59+
return this.providedKeys;
60+
}
61+
62+
public static CollectingProvidedKeysConfig.Builder builder() {
63+
return new CollectingProvidedKeysConfig.Builder();
64+
}
65+
66+
public static final class Builder {
67+
private final Map<String, Object> config;
68+
69+
public Builder() {
70+
this.config = new HashMap<>();
71+
}
72+
73+
public static CollectingProvidedKeysConfig.Builder from(CollectingProvidedKeys baseConfig) {
74+
var builder = new CollectingProvidedKeysConfig.Builder();
75+
return builder;
76+
}
77+
78+
public CollectingProvidedKeys build() {
79+
CypherMapWrapper config = CypherMapWrapper.create(this.config);
80+
return new CollectingProvidedKeysConfig(config);
81+
}
82+
}
83+
}

config-generator/src/test/resources/expected/Requires.java

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Neo4j is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
package positive;
21+
22+
import org.neo4j.gds.annotation.Configuration;
23+
24+
import java.util.Set;
25+
26+
@Configuration("CollectingProvidedKeysConfig")
27+
public interface CollectingProvidedKeys {
28+
29+
@Configuration.CollectProvidedKeys
30+
Set<String> providedKeys();
31+
}

config-generator/src/test/resources/positive/Requires.java

Whitespace-only changes.

0 commit comments

Comments
 (0)