Closed Silthus closed 2 years ago
Could you please provide example OkaeriConfig where such elements are used with current and/or expected result file?
Could you please provide example OkaeriConfig where such elements are used and current + expected result file?
Wow that was a quick reply :) Sure!
This is the part where I want to use the Map. If it doesn't work I would fall back to a List<Channel>
.
@Comment("Configure your channels here.")
private Map<String, Channel> channels = new HashMap<>(Map.of(
"global", Channel.channel()
.identifier("global")
.displayName(text("Global"))
.setting(Channel.PRIVATE, false)
.setting(Channel.FORCE, true)
.setting(Channel.AUTO_JOIN, true)
.setting(Channel.CONSOLE, true)
.build()
));
And this is the section from the config that should be parsed.
channels:
global: # <-- identifier of the channel
name: Global
private: false
force: true
auto_join: true
console: true
format: channel
trade:
name: Trade
auto_join: true
Also another problem seems to be that the Object serializer does not use any other serializers I registered. I have one for the displayName
which is a Component
. The pure serialization here works fine:
@Comment("Fine-tune the behaviour of the console chat in this section.")
private Console console = new Console();
/**
* Configuration for the console.
*
* @since next
*/
@Getter
@Setter
@Accessors(fluent = true)
public static class Console extends OkaeriConfig {
@Comment("The name players see when chatting from the console.")
private Component name = text("Console", RED);
@Comment({
"The channel the console will send chat messages to.",
"Is silently ignored if the channel does not exist."
})
private String defaultChannel = "global";
}
This is the serializer for Component
:
final class MiniMessageComponentSerializer extends BidirectionalTransformer<String, Component> {
private final MiniMessage parser = MiniMessage.miniMessage();
@Override
public GenericsPair<String, Component> getPair() {
return new GenericsPair<>(GenericsDeclaration.of(String.class), GenericsDeclaration.of(Component.class));
}
@Override
public Component leftToRight(final @NonNull String data, final @NonNull SerdesContext serdesContext) {
return parser.deserialize(data);
}
@Override
public String rightToLeft(final @NonNull Component data, final @NonNull SerdesContext serdesContext) {
return parser.serialize(data);
}
}
But in the Channel
serializer the following error is thrown:
failed to initialize dev.svoxel.chat.config.PluginConfig [class eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer]
eu.okaeri.configs.exception.OkaeriException: failed to initialize dev.svoxel.chat.config.PluginConfig [class eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer]
at app//eu.okaeri.configs.ConfigManager.create(ConfigManager.java:55)
at app//dev.svoxel.chat.SChatPlugin.setupAndLoadConfig(SChatPlugin.java:89)
at app//dev.svoxel.chat.SChatPlugin.onEnable(SChatPlugin.java:82)
at app//org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:264)
at app//org.bukkit.plugin.java.JavaPluginUtils.setEnabled(JavaPluginUtils.java:25)
at app//be.seeseemelk.mockbukkit.plugin.PluginManagerMock.enablePlugin(PluginManagerMock.java:506)
at app//be.seeseemelk.mockbukkit.MockBukkit.load(MockBukkit.java:179)
at app//be.seeseemelk.mockbukkit.MockBukkit.load(MockBukkit.java:163)
at app//dev.svoxel.chat.BukkitTestBase.setup(BukkitTestBase.java:47)
at java.base@16.0.2/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@16.0.2/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base@16.0.2/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@16.0.2/java.lang.reflect.Method.invoke(Method.java:567)
at app//org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
at app//org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptLifecycleMethod(TimeoutExtension.java:126)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptBeforeEachMethod(TimeoutExtension.java:76)
at app//org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at app//org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at app//org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at app//org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeMethodInExtensionContext(ClassBasedTestDescriptor.java:506)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$synthesizeBeforeEachMethodAdapter$21(ClassBasedTestDescriptor.java:491)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachMethods$3(TestMethodTestDescriptor.java:171)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:199)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:199)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachMethods(TestMethodTestDescriptor.java:168)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base@16.0.2/java.util.ArrayList.forEach(ArrayList.java:1511)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base@16.0.2/java.util.ArrayList.forEach(ArrayList.java:1511)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
at java.base@16.0.2/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base@16.0.2/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base@16.0.2/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base@16.0.2/java.lang.reflect.Method.invoke(Method.java:567)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at jdk.proxy2/jdk.proxy2.$Proxy5.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: eu.okaeri.configs.exception.OkaeriException: failed to #setValue for channels
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:298)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:249)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:263)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:235)
at app//eu.okaeri.configs.OkaeriConfig.load(OkaeriConfig.java:366)
at app//dev.svoxel.chat.SChatPlugin.lambda$setupAndLoadConfig$0(SChatPlugin.java:93)
at app//eu.okaeri.configs.ConfigManager.create(ConfigManager.java:52)
... 95 more
Caused by: eu.okaeri.configs.exception.OkaeriException: cannot simplify type class net.kyori.adventure.text.TextComponentImpl (null): 'TextComponentImpl{content="Global", style=StyleImpl{color=null, obfuscated=not_set, bold=not_set, strikethrough=not_set, underlined=not_set, italic=not_set, clickEvent=null, hoverEvent=null, insertion=null, font=null}, children=[]}' [class net.kyori.adventure.text.TextComponentImpl]
at app//eu.okaeri.configs.configurer.Configurer.simplify(Configurer.java:132)
at app//eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.simplify(YamlBukkitConfigurer.java:58)
at app//eu.okaeri.configs.serdes.SerializationData.add(SerializationData.java:33)
at app//dev.svoxel.chat.config.serializer.ChannelSerializer.serialize(ChannelSerializer.java:53)
at app//dev.svoxel.chat.config.serializer.ChannelSerializer.serialize(ChannelSerializer.java:36)
at app//eu.okaeri.configs.configurer.Configurer.simplify(Configurer.java:136)
at app//eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.simplify(YamlBukkitConfigurer.java:58)
at app//eu.okaeri.configs.configurer.Configurer.simplifyMap(Configurer.java:80)
at app//eu.okaeri.configs.configurer.Configurer.simplify(Configurer.java:129)
at app//eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.simplify(YamlBukkitConfigurer.java:58)
at app//eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.setValue(YamlBukkitConfigurer.java:75)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:296)
... 101 more
Oh and the config object itself is loaded like this:
this.config = ConfigManager.create(PluginConfig.class, config -> config
.withConfigurer(new YamlBukkitConfigurer(), new SerdesBukkit(), new SerdesCommons(), new SerdesSChatPack())
.withBindFile("config.yml")
.saveDefaults()
.load(true));
I see where such access to the parent object would be useful and may be a tempting design decision. However, it seems like this would make such serializer highly dependent on the parent context. Currently, serialized entities are considered as entirely separate, encapsulated map representations. Did you know that you can use whole object (map) as key in YAML?
The collection way is highly preferred instead of dynamic maps, especially in the cases where order matters. This approach encourages writing software that would be more storage agnostic. See: file backends that for some reason do not support map ordering, MySQL's JSON field.
If you still want to go with storing the data in the parent context, I think the best (and still nasty) way to approach this is through extending OkaeriConfig#load in the specific OkaeriConfig. By the way, I'm still open for suggestions of more clean, context-agnostic solutions if something comes to your mind.
@Override
public OkaeriConfig load() throws OkaeriException {
super.load();
this.getChannels().forEach((key, value) -> value.setIdentifier(key)); // or directly #set(key, val)
return this;
}
For the transformer problem, I would ask you to create a new issue. Please include examples or preferably share a small demo project for debugging purposes. It seems like inheritance may be the issue (See: Component
interface vs net.kyori.adventure.text.TextComponentImpl
). Note, the issue probably does not affect serializers due to different nature of type matching.
I see where such access to the parent object would be useful and may be a tempting design decision. However, it seems like this would make such serializer highly dependent on the parent context. Currently, serialized entities are considered as entirely separate, encapsulated map representations. Did you know that you can use whole object (map) as key in YAML?
What do you mean by that? I don't really understand. The reason why I want to use a Map<String,Channel>
is that the key of the map should be the identifier of the channel which needs to be unique.
ObjectSerializer is currently not aware of the parent context by design. There is no guarantee, the object would always have a parent of the same type, because object can be in different positions of the structure. The last question about using map as key in YAML reflects this issue: what data would be returned in that case? Key of the section? This makes the API really confusing.
I feel like uniqueness by property would be great with SerdesContextAttachment
mechanism (see: Duration serdes) or other in-house annotation. I will look into that. Seems like good feature to be included in serdes-commons
.
I feel like uniqueness by property would be great with
SerdesContextAttachment
mechanism (see: Duration serdes) or other in-house annotation. I will look into that. Seems like good feature to be included inserdes-commons
.
How about an annotion for maps like KeyFrom("property/getter")
that will automatically use the defined property as the map key.
Sounds like a reasonable solution, but how such map would be defined? I feel like the only possible scenario is that the real (java) type is a list whilst being serialized to map internally in the writing process.
Would be a good extension for TargetType
annotation which is already used for applying specific implementation for the map/collection interface. But this one does not allow changing the type completely in such implicit manner.
If map type is exposed in the POJO how is one expected to mutate the map? Adding new entities would allow data inconsistency - e.g. put("one", new MyData("two", 2))
.
I would also like to take this opportunity to ask you about how you discovered okaeri-configs project if that is not a problem.
I would also like to take this opportunity to ask you about how you discovered okaeri-configs project if that is not a problem.
I was looking for a new solution to load my configs via annotations and not the standard bukkit way. I simply googled and found your project. I find it quite nice to use, however I find it a little bit hard to learn with the completly missing javadcos ;)
Would it be possible to write a serializer for a Map.Entry<String,Channel>
that would solve the mutation problem. Would this maybe already be possible? This would keep the Key and the Value together in the serialization process.
Mmh no with that I cannot set the key in the data object.
I find it a little bit hard to learn with the completly missing javadcos ;)
I don't see much need for javadocs besides of the serdes data stuff, which already has pretty detailed documentation. OkaeriConfig as the main object that is accessed is also documented. Do you have any specific classes in mind?
Mmh no with that I cannot set the key in the data object.
I came to the conclusion that the map inside, list on the outside solution would be actually great and the best way to achieve this is through custom type. Furthermore, I'm going to implement IndexedSet
in okaeri-commons
project and provide serdes-okaeri
for that purposes.
Another immediate solution is to create a custom Channels
object in your codebase, then you would have full control over the data flow and be able to serialize it to map with full control of the keys. I think this is really simple and powerful at the same time.
Do you have a some quick mockup example code? I have a hard time to understand the concepts as I am not that deep into the codebase ;)
This is actually no different from the other serialization/deserialization that you have done before. The Channels
would be a standard class with the Map field for data, and the methods to add/get values.
It would be required to create an ObjectSerializer that accesses the map inside the Channels object and passes it to the SerializationData. This is the very same concept as yours flattening of the channel settings.
Here is my current structure for the IndexedSet
implementation:
@Silthus IndexedSet support is now available as the beta (4.0.0-beta1
) release: https://github.com/OkaeriPoland/okaeri-configs/tree/beta/serdes-okaeri. Please note that upgrading from 3.x to 4.x requires a new parameter in the ObjectSerializer#serialize.
import eu.okaeri.commons.indexedset.IndexedSet;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.CustomKey;
import eu.okaeri.configs.schema.GenericsDeclaration;
import eu.okaeri.configs.serdes.DeserializationData;
import eu.okaeri.configs.serdes.ObjectSerializer;
import eu.okaeri.configs.serdes.SerializationData;
import eu.okaeri.configs.serdes.okaeri.SerdesOkaeri;
import eu.okaeri.configs.serdes.okaeri.indexedset.IndexedSetSpec;
import eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer;
import lombok.*;
import org.junit.jupiter.api.Test;
import java.util.UUID;
public class TestIndexedSetSerializer {
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
static class Player extends OkaeriConfig {
private UUID id;
@CustomKey("test-name")
private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class CustomPlayer {
private int id;
private String name;
}
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
static class TestConfig extends OkaeriConfig {
@IndexedSetSpec(key = "test-name")
private IndexedSet<String, Player> players = IndexedSet.of(Player::getName,
new Player(UUID.randomUUID(), "Player1"),
new Player(UUID.randomUUID(), "Player2")
);
@IndexedSetSpec(key = "id")
private IndexedSet<Integer, CustomPlayer> customPlayers = IndexedSet.of(CustomPlayer::getId,
new CustomPlayer(1, "CPlayer1"),
new CustomPlayer(2, "CPlayer2")
);
}
static class CustomPlayerSerializer implements ObjectSerializer<CustomPlayer> {
@Override
public boolean supports(@NonNull Class<? super CustomPlayer> type) {
return CustomPlayer.class.isAssignableFrom(type);
}
@Override
public void serialize(@NonNull CustomPlayer object, @NonNull SerializationData data, @NonNull GenericsDeclaration generics) {
data.add("id", object.getId());
data.add("name", object.getName());
}
@Override
public CustomPlayer deserialize(@NonNull DeserializationData data, @NonNull GenericsDeclaration generics) {
int id = data.get("id", int.class);
String name = data.get("name", String.class);
return new CustomPlayer(id, name);
}
}
@Test
public void test_serializer() {
TestConfig config = new TestConfig();
config.withConfigurer(new YamlBukkitConfigurer(),
new SerdesOkaeri(),
registry -> registry.register(new CustomPlayerSerializer())
);
String configString = config.saveToString();
System.out.println(config);
System.out.println(configString);
config.load(configString);
System.out.println(config);
}
}
Yields (formatted and added comments for readability):
# object before serialization
TestIndexedSetSerializer.TestConfig(
players={
Player1=TestIndexedSetSerializer.Player(id=f7617d8e-69c7-4407-b6e4-b07993bdec1e, name=Player1),
Player2=TestIndexedSetSerializer.Player(id=597324c8-9e4a-405f-a7b8-03e269725f6b, name=Player2)
},
customPlayers={
1=TestIndexedSetSerializer.CustomPlayer(id=1, name=CPlayer1),
2=TestIndexedSetSerializer.CustomPlayer(id=2, name=CPlayer2)
}
)
# bukkit yaml output
players:
Player1:
id: f7617d8e-69c7-4407-b6e4-b07993bdec1e
Player2:
id: 597324c8-9e4a-405f-a7b8-03e269725f6b
customPlayers:
'1':
name: CPlayer1
'2':
name: CPlayer1
# object from the yaml
TestIndexedSetSerializer.TestConfig(
players={
Player1=TestIndexedSetSerializer.Player(id=f7617d8e-69c7-4407-b6e4-b07993bdec1e, name=Player1),
Player2=TestIndexedSetSerializer.Player(id=597324c8-9e4a-405f-a7b8-03e269725f6b, name=Player2)
},
customPlayers={
1=TestIndexedSetSerializer.CustomPlayer(id=1, name=CPlayer1),
2=TestIndexedSetSerializer.CustomPlayer(id=2, name=CPlayer2)
}
)
Feel free to explore:
Let me know if this resolves your issue.
Thank you!
I tried it out but get the following exception:
Caused by: eu.okaeri.configs.exception.OkaeriException: failed to #setValue for channels
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:298)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:249)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:263)
at app//eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:235)
at app//eu.okaeri.configs.OkaeriConfig.saveDefaults(OkaeriConfig.java:156)
at app//dev.svoxel.chat.SChatPlugin.lambda$loadConfig$0(SChatPlugin.java:190)
at app//eu.okaeri.configs.ConfigManager.create(ConfigManager.java:52)
... 95 more
Caused by: java.lang.ClassCastException: class dev.svoxel.chat.target.ChannelImpl cannot be cast to class java.lang.String (dev.svoxel.chat.target.ChannelImpl is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
at eu.okaeri.configs.serdes.standard.StringToStringTransformer.transform(StringToStringTransformer.java:8)
at eu.okaeri.configs.configurer.Configurer.resolveType(Configurer.java:312)
at eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.resolveType(YamlBukkitConfigurer.java:70)
at eu.okaeri.configs.configurer.Configurer.simplify(Configurer.java:121)
at eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.simplify(YamlBukkitConfigurer.java:58)
at eu.okaeri.configs.configurer.Configurer.simplifyCollection(Configurer.java:65)
at eu.okaeri.configs.configurer.Configurer.simplify(Configurer.java:125)
at eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.simplify(YamlBukkitConfigurer.java:58)
at eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer.setValue(YamlBukkitConfigurer.java:75)
at eu.okaeri.configs.OkaeriConfig.save(OkaeriConfig.java:296)
... 101 more
This is my implementation:
@IndexedSetSpec(key = "alias")
private IndexedLinkedHashSet<String, Channel> channels = new IndexedLinkedHashSet<>(Channel::alias, new Channel[] {Channel.channel()
.alias("global")
.displayName(text("Global"))
.setting(PRIVATE, false)
.setting(FORCE, true)
.setting(AUTO_JOIN, true)
.setting(CONSOLE, true)
.build(),
Channel.channel()
.alias("team")
.displayName(text("Team"))
.setting(PRIVATE, true)
.setting(FORCE, false)
.setting(AUTO_JOIN, true)
.setting(CONSOLE, false)
.build()
});
final class ChannelBuilderSerializer implements ObjectSerializer<Channel> {
private final Channels channels;
ChannelBuilderSerializer(final @NonNull Channels channels) {
this.channels = channels;
}
@Override
public boolean supports(final @NonNull Class<? super Channel> type) {
return Channel.class.isAssignableFrom(type);
}
@Override
public void serialize(final @NonNull Channel object, final @NonNull SerializationData data, final @NonNull GenericsDeclaration generics) {
data.add("id", object.uuid());
data.add("alias", object.alias());
data.add("name", object.displayName(), Component.class);
data.add("format", object.format(), Format.class);
for (final Setting<?> setting : channels.settings()) {
data.add(setting.key().asString().replace(SChat.NAMESPACE + ":", ""), object.getOrNull(setting), setting.type());
}
}
@Override
public Channel deserialize(final @NonNull DeserializationData data, final @NonNull GenericsDeclaration generics) {
final Channel.Builder builder = Channel.channel();
if (data.containsKey("id"))
builder.uuid(data.get("id", UUID.class));
if (data.containsKey("alias"))
builder.alias(data.get("alias", String.class));
if (data.containsKey("name"))
builder.displayName(data.get("name", Component.class));
if (data.containsKey("format"))
builder.messageFormat(Format.miniMessageFormat(data.get("format", String.class)));
for (final Setting<?> setting : channels.settings()) {
addSetting(data, builder, setting.key().asString().replace(SChat.NAMESPACE + ":", ""), setting);
}
return builder.build();
}
private <V> void addSetting(final DeserializationData data, final Channel.Builder builder, final String key, final Setting<V> setting) {
if (data.containsKey(key))
builder.setting(setting, data.get(key, setting.type()));
}
}
I think you may have data.add("xyz", channel, String.class)
somewhere. This method currently does not allow such type conversion and is intended as a type guide for the current value only (as in #21). However, feel free to open a separate issue with your use case.
At the moment, you may want to use Configurer#resolveType API manually:
data.add("xyz", data.getConfigurer().resolveType(
channel, GenericsDeclaration.of(Channel.class),
String.class, GenericsDeclaration.of(String.class),
SerdesContext.of(data.getConfigurer()))
);
Also, is there any particular reason for using IndexedLinkedHashSet
instead of IndexedSet
as the field type? I feel like your config would gain from using the included LinkedSet#builder
too.
@IndexedSetSpec(key = "alias")
private IndexedSet<String, Channel> channels = IndexedSet.builder(String.class, Channel.class)
.keyFunction(Channel::alias)
.add(Channel.channel()
.alias("global")
.displayName(text("Global"))
.setting(PRIVATE, false)
.setting(FORCE, true)
.setting(AUTO_JOIN, true)
.setting(CONSOLE, true)
.build())
.add(Channel.channel()
.alias("team")
.displayName(text("Team"))
.setting(PRIVATE, true)
.setting(FORCE, false)
.setting(AUTO_JOIN, true)
.setting(CONSOLE, false)
.build())
.build()
Thank you for the tip with the builder and IndexSet.
However I am not quite sure what you mean with the first think and resolverType?
This is my current Channel Serialization and I dont really know where I would pass a channel somewhere here.
final class ChannelBuilderSerializer implements ObjectSerializer<Channel> {
private final Channels channels;
ChannelBuilderSerializer(final @NonNull Channels channels) {
this.channels = channels;
}
@Override
public boolean supports(final @NonNull Class<? super Channel> type) {
return Channel.class.isAssignableFrom(type);
}
@Override
public void serialize(final @NonNull Channel object, final @NonNull SerializationData data, final @NonNull GenericsDeclaration generics) {
data.add("id", object.uuid(), UUID.class);
data.add("alias", object.alias(), String.class);
data.add("name", object.displayName(), Component.class);
data.add("format", object.format(), Format.class);
for (final Setting<?> setting : channels.settings()) {
data.add(setting.key().asString().replace(SChat.NAMESPACE + ":", ""), object.getOrNull(setting), setting.type());
}
}
@Override
public Channel deserialize(final @NonNull DeserializationData data, final @NonNull GenericsDeclaration generics) {
final Channel.Builder builder = Channel.channel();
if (data.containsKey("id"))
builder.uuid(data.get("id", UUID.class));
if (data.containsKey("alias"))
builder.alias(data.get("alias", String.class));
if (data.containsKey("name"))
builder.displayName(data.get("name", Component.class));
if (data.containsKey("format"))
builder.messageFormat(Format.miniMessageFormat(data.get("format", String.class)));
for (final Setting<?> setting : channels.settings()) {
addSetting(data, builder, setting.key().asString().replace(SChat.NAMESPACE + ":", ""), setting);
}
return builder.build();
}
private <V> void addSetting(final DeserializationData data, final Channel.Builder builder, final String key, final Setting<V> setting) {
if (data.containsKey(key))
builder.setting(setting, data.get(key, setting.type()));
}
}
The core of your current issue is that somewhere the type of the element is assumed incorrectly. I figured the most likely cause would be incorrect use of SerializationData#add. But after a second look, I see that this is simplifyCollection related.
What other serializers/transformers do you have? It seems like you may have a collection of Channel, but somewhere it is defined as it was a String collection. I feel like there is a small chance that Channel-to-String transformer could mess something like that up.
And that got me thinking… IndexedSet<String, Channel>
is a Set
where the first generic parameter is a String
. Seems like I should have made it IndexedSet<Channel, String> by design. Anyway, just making sure: have you registered SerdesOkaeri
in your configurer?
The issue seems pretty odd but not unlikely, as I was surprised it actually worked with such custom handling for something that is "just a set" from the beginning.
Anyway, just making sure: have you registered
SerdesOkaeri
in your configurer?
That was the problem, now everything works 👍🏻
By the way, version 4.0.0-beta2
is available with the support for other/custom implementations of IndexedSet:
// recommended
@TargetType(IndexedConcurrentHashSet.class)
private IndexedSet<MyData, K> set = ...
// or without annotation
private IndexedConcurrentHashSet<MyData, K> set = ...
Note that builder and fields of type IndexedSet
without @TargetType
still use IndexedLinkedHashSet
by default. If building set of custom type, it is required to use IndexedSetBuilder#type
.
Also note that from beta1
to beta2
generic signature of classes in indexedset
package was changed (from <KV, VT>
like maps to <VT, KT>
) for better compatibility with APIs relying on generic type position like okaeri-configs.
Thanks to the change in the signature, using IndexedSet
without SerdesOkaeri
now yields a lot more appropriate exception and everything without the core knowing about any of those APIs:
eu.okaeri.configs.exception.OkaeriException: failed to #getValue for players
...
1. (with target type) Caused by: eu.okaeri.configs.exception.OkaeriException: failed to create instance of class eu.okaeri.commons.indexedset.IndexedConcurrentHashSet
2. (without target type) Caused by: eu.okaeri.configs.exception.OkaeriException: failed to create instance of interface eu.okaeri.commons.indexedset.IndexedSet
...
How can I access the key of a
Map<String, Object>
when serializing a custom object?I have a
Channel
object where I want to map the key of the map to the identifier of the channel. How can I do this?