Skip to content

Commit 6fa9145

Browse files
committed
Add composite config options
1 parent 5902e29 commit 6fa9145

File tree

12 files changed

+1189
-2
lines changed

12 files changed

+1189
-2
lines changed

config-api/build.gradle

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,161 @@
1+
import java.util.stream.Collectors
2+
import java.util.stream.IntStream
3+
14
plugins {
25
id 'java-library'
36
}
47

5-
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
8+
java.toolchain.languageVersion = JavaLanguageVersion.of(25)
69

710
dependencies {
811
api(libs.jda)
912
api(libs.bundles.database)
1013
implementation(libs.caffeine)
1114
implementation(libs.json)
1215
compileOnly(libs.annotations)
16+
17+
testRuntimeOnly(libs.logback)
18+
}
19+
20+
tasks.register('createObjectOption') {
21+
doLast {
22+
final int maxArgs = 10
23+
24+
final file = project.file('src/main/java/net/neoforged/camelot/api/config/type/ObjectOptionBuilders.java')
25+
final interfaceBuilder = new StringBuilder("""
26+
package net.neoforged.camelot.api.config.type;
27+
28+
import java.util.List;
29+
import java.util.function.Function;
30+
import java.util.function.UnaryOperator;
31+
32+
// This file is autogenerated. DO NOT MODIFY IT MANUALLY
33+
public interface ObjectOptionBuilders {""".trim())
34+
35+
for (int i = 1; i <= maxArgs; i++) {
36+
var genericList = argList(i)
37+
String moreArgs = ""
38+
if (i < maxArgs) {
39+
moreArgs = """
40+
/**
41+
* Add a field to this composite object.
42+
*
43+
* @param id the id of the field
44+
* @param extractor the function that extracts the field value from the object
45+
* @param fieldFactory the option factory to create the field's option
46+
* @param optionConfigurator a unary operator used to configure the field option
47+
* @param <NEW> the type of the field to add
48+
* @param <FROM> the type of the option factory. This can be different to facilitate eventual mapping
49+
* @param <FB> the type of the field's builder
50+
* @return a new builder with the added field
51+
*/
52+
<NEW, FROM, FB extends OptionBuilder<TGT, FROM, FB>> Builder${i + 1}<TGT, OBJ, ${genericList}, NEW> field(String id, Function<OBJ, NEW> extractor, OptionBuilderFactory<TGT, FROM, FB> fieldFactory, Function<FB, ? extends OptionBuilder<TGT, NEW, ?>> optionConfigurator);
53+
54+
/**
55+
* Add a field to this composite object.
56+
*
57+
* @param id the id of the field
58+
* @param extractor the function that extracts the field value from the object
59+
* @param fieldFactory the option factory to create the field's option
60+
* @param displayName the display name of the field
61+
* @param description the description of the field
62+
* @param defaultValue the default value of the field
63+
* @param <NEW> the type of the field to add
64+
* @return a new builder with the added field
65+
*/
66+
<NEW> Builder${i + 1}<TGT, OBJ, ${genericList}, NEW> field(String id, Function<OBJ, NEW> extractor, OptionBuilderFactory<TGT, NEW, ?> fieldFactory, String displayName, String description, NEW defaultValue);""".trim()
67+
}
68+
interfaceBuilder.append("""
69+
/**
70+
* A builder for composite objects with ${i} fields.
71+
*/
72+
interface Builder${i}<TGT, OBJ, ${genericList}> {
73+
${moreArgs}
74+
/**
75+
* {@return an option builder factory for object options of this type}
76+
* The fields will be turned into the object using the given {@code constructor}.
77+
*
78+
* @param constructor the function used to construct the object from the fields
79+
*/
80+
OptionBuilderFactory<TGT, OBJ, OptionBuilder.Composite<TGT, OBJ>> construct(Constructor${i}<${genericList}, OBJ> constructor);
81+
}
82+
""")
83+
}
84+
85+
for (int i = 1; i <= maxArgs; i++) {
86+
interfaceBuilder.append("""
87+
/**
88+
* A constructor function with ${i} argument${i == 1 ? '' : 's'}.
89+
*/
90+
interface Constructor${i}<${argList(i)}, RESULT> {
91+
/**
92+
* {@return the object constructed with the given arguments}
93+
*/
94+
RESULT create(${IntStream.range(0, i).mapToObj { "${genericArg(it)} arg${it}"}.collect(Collectors.joining(', '))});
95+
96+
/**
97+
* {@return the object constructed with the given arguments}
98+
*
99+
* @param arguments the arguments as an ordered list
100+
*/
101+
@SuppressWarnings("unchecked")
102+
default RESULT createFromList(List<Object> arguments) {
103+
return create(${IntStream.range(0, i).mapToObj { "(${genericArg(it)}) arguments.get($it)"}.collect(Collectors.joining(', '))});
104+
}
105+
}""")
106+
}
107+
108+
interfaceBuilder.append('\n}')
109+
110+
file.delete()
111+
file << interfaceBuilder
112+
113+
final builderFile = project.file('src/main/java/net/neoforged/camelot/api/config/impl/ObjectOptionBuilder.java')
114+
builderFile.delete()
115+
builderFile << ("""
116+
package net.neoforged.camelot.api.config.impl;
117+
118+
import net.neoforged.camelot.api.config.type.ObjectOptionBuilders;
119+
import net.neoforged.camelot.api.config.type.OptionBuilder;
120+
import net.neoforged.camelot.api.config.type.OptionBuilderFactory;
121+
122+
import java.util.ArrayList;
123+
import java.util.List;
124+
import java.util.function.Function;
125+
126+
// This file is autogenerated. DO NOT MODIFY IT MANUALLY
127+
@SuppressWarnings({"rawtypes", "unchecked"})
128+
record ObjectOptionBuilder<G, O>(List<ObjectOption.BuilderOption<G, O, ?, ?>> options) implements ${IntStream.rangeClosed(1, maxArgs).mapToObj { 'ObjectOptionBuilders.Builder' + it }.collect(Collectors.joining(', '))} {
129+
ObjectOptionBuilder(List<ObjectOption.BuilderOption<G, O, ?, ?>> previous, ObjectOption.BuilderOption<G, O, ?, ?> extra) {
130+
var newOptions = new ArrayList<>(previous);
131+
newOptions.add(extra);
132+
this(newOptions);
133+
}
134+
135+
@Override
136+
public ObjectOptionBuilder<G, O> field(String id, Function extractor, OptionBuilderFactory factory, Function configurator) {
137+
return new ObjectOptionBuilder<>(this.options, new ObjectOption.BuilderOption<>(id, extractor, factory, configurator));
138+
}
139+
140+
@Override
141+
public ObjectOptionBuilder<G, O> field(String id, Function extractor, OptionBuilderFactory fieldFactory, String displayName, String description, Object defaultValue) {
142+
return field(id, extractor, fieldFactory, b -> ((OptionBuilder) b).displayName(displayName).description(description).defaultValue(defaultValue));
143+
}
144+
${IntStream.rangeClosed(1, maxArgs).mapToObj {"""
145+
@Override
146+
public OptionBuilderFactory construct(ObjectOptionBuilders.Constructor$it constructor) {
147+
return new ObjectOption.Factory(this.options, constructor::createFromList);
148+
}
149+
"""}.collect(Collectors.joining())}
150+
}
151+
""".trim())
152+
}
153+
}
154+
155+
static String argList(int count) {
156+
IntStream.range(0, count).mapToObj(this.&genericArg).collect(Collectors.joining(', '))
157+
}
158+
159+
static String genericArg(int nr) {
160+
return String.valueOf((char)(((int)(char)'A') + nr))
13161
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package net.neoforged.camelot.api.config.impl;
2+
3+
import net.dv8tion.jda.api.components.buttons.Button;
4+
import net.dv8tion.jda.api.components.container.Container;
5+
import net.dv8tion.jda.api.components.container.ContainerChildComponent;
6+
import net.dv8tion.jda.api.components.section.Section;
7+
import net.dv8tion.jda.api.components.textdisplay.TextDisplay;
8+
import net.dv8tion.jda.api.entities.Message;
9+
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
10+
import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
11+
import net.dv8tion.jda.api.utils.messages.MessageEditData;
12+
import net.neoforged.camelot.api.config.ConfigManager;
13+
import net.neoforged.camelot.api.config.type.OptionBuilder;
14+
import net.neoforged.camelot.api.config.type.OptionBuilderFactory;
15+
import net.neoforged.camelot.api.config.type.OptionType;
16+
import org.jetbrains.annotations.Nullable;
17+
import org.json.JSONException;
18+
import org.json.JSONObject;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.function.Function;
23+
import java.util.stream.Collectors;
24+
25+
final class ObjectOption<O> implements OptionType<O> {
26+
private final List<OptionInfo<O, ?>> options;
27+
private final Creator<O> creator;
28+
@Nullable
29+
private final Function<O, String> formatter;
30+
31+
ObjectOption(List<OptionInfo<O, ?>> options, Creator<O> creator, @Nullable Function<O, String> formatter) {
32+
this.options = options;
33+
this.creator = creator;
34+
this.formatter = formatter;
35+
}
36+
37+
@Override
38+
public String serialise(O value) {
39+
var o = new JSONObject();
40+
for (OptionInfo<O, ?> option : options) {
41+
o.put(option.id(), option.serialise(value));
42+
}
43+
return o.toString();
44+
}
45+
46+
@Override
47+
public O deserialize(String value) {
48+
var obj = new JSONObject(value);
49+
return creator.create(options.stream()
50+
.map(i -> {
51+
try {
52+
return i.type().deserialize(obj.getString(i.id()));
53+
} catch (JSONException ex) {
54+
return i.defaultValue();
55+
}
56+
})
57+
.toList());
58+
}
59+
60+
@Override
61+
public Button createUpdateButton(O currentValue, Function<O, MessageEditData> updater, ComponentCreator components) {
62+
return components.button(event -> event.reply(MessageCreateData.fromEditData(objectView(event.getMessage(), currentValue, updater, components)))
63+
.setEphemeral(true).queue()).withLabel("Modify");
64+
}
65+
66+
private MessageEditData objectView(Message topLevelMessage, O currentValue, Function<O, MessageEditData> updater, ComponentCreator components) {
67+
var msgComponents = new ArrayList<ContainerChildComponent>();
68+
msgComponents.add(TextDisplay.of("## Configure object"));
69+
for (int i = 0; i < options.size(); i++) {
70+
msgComponents.add(section(topLevelMessage, i, options.get(i), currentValue, updater, components));
71+
}
72+
return new MessageEditBuilder()
73+
.setComponents(List.of(Container.of(msgComponents)))
74+
.useComponentsV2()
75+
.build();
76+
}
77+
78+
private <T> Section section(Message topLevelMessage, int idx, OptionInfo<O, T> info, O value, Function<O, MessageEditData> updater, ComponentCreator components) {
79+
var current = value == null ? info.defaultValue : info.extractor().apply(value);
80+
return Section.of(
81+
info.type().createUpdateButton(
82+
current,
83+
newValue -> {
84+
var newObject = newObject(value, idx, newValue);
85+
topLevelMessage.editMessage(updater.apply(newObject)).queue();
86+
return objectView(topLevelMessage, newObject, updater, components);
87+
},
88+
components
89+
),
90+
TextDisplay.ofFormat(
91+
"**" + info.displayName() + "**\n"
92+
+ info.desc() + "\n\n"
93+
+ "Curent value: " + (current == null ? "*none*" : info.type().formatFullPageView(current))
94+
)
95+
);
96+
}
97+
98+
private O newObject(O current, int index, Object newValue) {
99+
var newObjects = new ArrayList<>(options.size());
100+
for (var option : options) {
101+
newObjects.add(current == null ? option.defaultValue : option.extractor().apply(current));
102+
}
103+
newObjects.set(index, newValue);
104+
return creator.create(newObjects);
105+
}
106+
107+
@Override
108+
public String format(O value) {
109+
if (formatter != null) return formatter.apply(value);
110+
return options.stream().map(o -> o.displayName() + ": " + o.format(value)).collect(Collectors.joining(", "));
111+
}
112+
113+
private record OptionInfo<O, T>(String id, String displayName, String desc, OptionType<T> type, T defaultValue,
114+
Function<O, T> extractor) {
115+
private String format(O value) {
116+
return type().format(extractor().apply(value));
117+
}
118+
119+
private String serialise(O value) {
120+
return type.serialise(extractor.apply(value));
121+
}
122+
}
123+
124+
static class Builder<G, O> extends OptionBuilderImpl<G, O, OptionBuilder.Composite<G, O>> implements OptionBuilder.Composite<G, O> {
125+
private final java.util.List<BuilderOption<G, O, ?, ?>> options;
126+
private final Creator<O> creator;
127+
128+
private Function<O, String> formatter;
129+
130+
protected Builder(ConfigManager<G> manager, String path, String id, java.util.List<BuilderOption<G, O, ?, ?>> options, Creator<O> creator) {
131+
super(manager, path, id);
132+
this.options = options;
133+
this.creator = creator;
134+
}
135+
136+
@Override
137+
@SuppressWarnings({"rawtypes", "unchecked"})
138+
protected OptionType<O> createType() {
139+
return new ObjectOption(
140+
options.stream()
141+
.map(o -> {
142+
var b = (OptionBuilderImpl) o.builder(manager);
143+
return new OptionInfo(
144+
b.id, b.name, b.description, b.createType(), b.defaultValue, o.extractor
145+
);
146+
})
147+
.toList(),
148+
creator,
149+
formatter
150+
);
151+
}
152+
153+
@Override
154+
public Composite<G, O> formatter(Function<O, String> formatter) {
155+
this.formatter = formatter;
156+
return this;
157+
}
158+
}
159+
160+
record Factory<G, T>(List<BuilderOption<G, T, ?, ?>> options,
161+
Creator<T> creator) implements OptionBuilderFactory<G, T, OptionBuilder.Composite<G, T>> {
162+
@Override
163+
public Builder<G, T> create(ConfigManager<G> manager, String path, String id) {
164+
return new Builder<>(manager, path, id, options, creator);
165+
}
166+
}
167+
168+
record BuilderOption<G, O, F, Z>(String id, Function<O, Z> extractor, OptionBuilderFactory<G, F, ?> factory,
169+
Function<OptionBuilder<G, F, ?>, OptionBuilder<G, Z, ?>> configurator) {
170+
public OptionBuilder<G, Z, ?> builder(ConfigManager<G> manager) {
171+
var builder = factory.create(manager, "_", this.id);
172+
if (builder instanceof OptionBuilderImpl<G, F, ? extends OptionBuilder<G, F, ?>> obi) {
173+
builder = obi.setCannotBeRegistered();
174+
}
175+
return configurator.apply(builder);
176+
}
177+
}
178+
179+
@FunctionalInterface
180+
interface Creator<T> {
181+
T create(List<Object> args);
182+
}
183+
}

0 commit comments

Comments
 (0)