-
Notifications
You must be signed in to change notification settings - Fork 3
Config
The Config API is used to load data in a simple way, abstracting the details so that it can have more control over the failures.It's main advantages over the Spigot API are:
- Easier to pass around
- Better failure printing (file and line/column)
- Easier to use
There are two ways that we can look at this API coming from different versions
of the plugin, we decided to keep both of them because they can be useful in
different situations. The first one is an object-based view where the Config
object gets passed around every method. This object represent a yaml node, you
can parse its entries, list them or nagivate them getting sub-configs or lists.
An example speaks 1000 words:
Config cfg = Config.fromYaml(new StringReader(
"str: Text\n" +
"count: 129\n" +
"enum: [hide enchants, hide attributes]\n" +
"type: redstone wire\n" +
"center: [15.0, 30.0, 60.0]\n" +
"center2:\n" +
" x: 1.0\n" +
" y: 2.0\n" +
" z: 3.0\n"
));
Integer optCount = cfg.getInt("opt_count");
// You can add "required" to throw an exception if the key is not present
// Every method that does not contain "required" can return null
int count = cfg.getIntRequired("count");
String str = cfg.getStringRequired("str");
// Can also parse material
Material mat = cfg.getMaterialRequired("type");
updateCenter(cfg.getConfigRequired("center2"));// returns the subconfig "center2"
for all of the supported types you have three methods:
Getter type | Description |
---|---|
getT(String key) | Gets the element with key "key" and parse it as a type "T", if not found returns null or default value if primitive. |
getT(String key, T default) | Gets the element with key "key" and parse it as a type "T", if not found returns the value passed. |
getTRequired(String key) | Gets the element with key "key" and parse it as a type "T", if not found throws a RequiredPropertyNotFoundException . |
In addition to this there are some extra methods including some generic ones (without T) that return a generic Object and some list types. For more complex types (imagine a list of enums) there is another method that is something that uses both config APIs and will be explained later
Quick note: never pass from config to map and back to config, even if this is possible and was widely used in the old config version it is now deprecated as it loses all of the context data, if a parsing exception occurs the user would not be provided with the origin in the config file.
The second way of parsing the config is using data objects, maybe you might be
more familiar with this terminology from other languages (ex. kotlin), in a
nutshell they are custom classes with little to no logic and the only purpose
of holding the data to be parsed. This offers a clearer way to parse the config
as it lets the Config API have more control. With this the API can analyze the
type to parse and build the right parser for complex types automatically.
Let's make an example, if you want to parse a complex object as Map<String, Material>
you don't have to do it manually, the Config API would decompose the type,
get the parser for every class and compose a custom parser that can manage the
type that was requested.
So, how can we do this for custom classes? Let's take a real world example right
from the core:
@ConfigConstructor
public UItem(
@ConfigProperty("type") Material type,
@ConfigProperty(value = "data", optional = true) PlaceholderValue<Short> data,
@ConfigProperty(value = "amount", optional = true) PlaceholderValue<Integer> amount,
@ConfigProperty(value = "name", optional = true) String rawName,
@ConfigProperty(value = "lore", optional = true) List<PlaceholderValue<String>> lore,
@ConfigProperty(value = "flags", optional = true) List<ItemFlag> flags,
@ConfigProperty(value = "enchantments", optional = true) Map<Enchantment, PlaceholderValue<Integer>> enchantments
) {
this.type = type;
this.data = data != null ? data : PlaceholderValue.fake((short)0);
this.amount = amount != null ? amount : PlaceholderValue.fake(1);
this.displayName = rawName == null ? null : PlaceholderValue.stringValue(ChatColor.RESET.toString() + rawName);
this.lore = lore != null ? lore : Collections.emptyList();
this.flags = flags != null ? flags : Collections.emptyList();
if (enchantments != null) {
this.enchantments.putAll(enchantments);
}
}
The only custom logic implemented is to manage optional types. The only parsing
is done in the argument declaration, you just need to write the name of the
parameter, the type and wheter it's optional or not, and that's it. The Config
API will manage complex types such as Map<Enchantment, PlaceholderValue<Integer>>
without any trouble.
This of course comes with some restrictions:
The types used in the parameters need to be either:
- Primitives (int, long, byte...)
- Java Collections
- Bukkit primitives (they are already implemented)
- Other classes with a
@ConfigConstructor
If you want to add other classes that don't fall into those categories you can
either implement the @ConfigConstructor
or do a custom parser (when you don't
have control on the implementation of the class). The latter definition is
an advanced topic so it will have it's own chapter: External Declarators
As a bonus: you can use the Optional
java class instead of optional = true
,
it all comes down to preference.
Let's check the JUnit test to see a full example of the usage:
public static class ConfigLoaderExample {
@ConfigConstructor
public ConfigLoaderExample(
@ConfigProperty("str") String str,
@ConfigProperty("count") int count,
@ConfigProperty("enum") List<ItemFlag> flags,
@ConfigProperty("type") Material type,
@ConfigProperty("center") Position center,
@ConfigProperty("center2") Position center2
) {
assertEquals("Stringa", str);
assertEquals(129, count);
assertEquals(ImmutableList.of(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES), flags);
assertEquals(Material.REDSTONE_WIRE, type);
assertEquals(new Position(15, 30, 60), center);
assertEquals(new Position(1, 2, 3), center2);
}
}
@Test
public void basicTest() {
Config.fromYaml(new StringReader(
"str: Stringa\n" +
"count: 129\n" +
"enum: [hide enchants, hide attributes]\n" +
"type: redstone wire\n" +
"center: [15.0, 30.0, 60.0]\n" +
"center2:\n" +
" x: 1.0\n" +
" y: 2.0\n" +
" z: 3.0\n"
)).get(ConfigLoaderExample.class, plugin);
}
The plugin is needed for some complex types (as GUI interfaces) that might need it,
you can pass null
if you're sure that it doesn't get used.
In practice both of the former definitions are used, you can use a Config
to
parse a @ConfigConstructor
and you can have a Config
as a parameter type of
a @ConfigCostructor
, this enables the user to have the complete control on
how the data should be parsed while he can let the Config work out the
parsing method for the majority of the cases.
You also don't need to create a custom class for all of the types, let's get the former example:
str: Text
count: 129
enum: [hide enchants, hide attributes]
type: redstone wire
center: [15.0, 30.0, 60.0]
center2:
x: 1.0
y: 2.0
z: 3.0
We left the enum
entry unparsed, that would require a parser for List<ItemFlag>
and the Config
object doesn't provide an enum list parser, should we create a
custom class just to parse this single field? We can also use another method in
the Config
object, this is config.get(String key, Type type, Plugin plugin)
.
If we want to parse the example we can write:
List<ItemFlag> enumTest = cfg.get("enum", typeOf(List.class, ItemFlag.class), plugin);
This prevents the construction of a customized class, replacing it with the specification of the type that we want to parse.
This seems all too good until a little detail comes to mind, what about polymorhpism? A library written in Java should certaintly handle polymorhpism in a simple way as most of the complex configs need this (the bukkit config is just an example of this).
To implement a polymorhpic class you just need to create a static method in
the father with the annotation @PolymorphicSelector
, this method will get the
same parameters as a @ConfigConstructor
but it must not parse the class,
it's purpose is to select the right class that will parse the request.
In most cases it will need to multiplex the class based on a type
field or
something similar to it.
Let's see the JUnit tests for a full example:
public static class PolymorphicFather {
@PolymorphicSelector
private static Class<? extends PolymorphicFather> selectChild(@ConfigProperty("type") String type) {
switch (type) {
case "dog": return Dog.class;
case "cat": return Cat.class;
default: return null;
}
}
public static class Dog extends PolymorphicFather {
@Getter
private Color furColor;
@ConfigConstructor
public Dog(@ConfigProperty("furColor") Color furColor) {
super("dog");
this.furColor = furColor;
}
}
public static class Cat extends PolymorphicFather {
@Getter
private String meowMessage;
@ConfigConstructor
public Cat(@ConfigProperty("meowMessage") String meowMessage) {
super("cat");
this.meowMessage = meowMessage;
}
}
}
@Test
public void testPolymorphic() {
// This is the more explicit way to parse the config
// First you query the parser
ConfigParser parser = ConfigParserRegistry.getStandard().getFor(PolymorphicFather.class);
// And then you parse
PolymorphicFather dog = (PolymorphicFather) parser.parse(
plugin,
new StringReader(
"type: dog\n" +
"furColor: blue\n"
)
);
assertEquals(PolymorphicFather.Dog.class, dog.getClass());
assertEquals(Color.BLUE, ((PolymorphicFather.Dog) dog).getFurColor());
PolymorphicFather cat = (PolymorphicFather) parser.parse(
plugin,
new StringReader(
"type: cat\n" +
"meowMessage: meoow\n"
)
);
assertEquals(PolymorphicFather.Cat.class, cat.getClass());
assertEquals("meoow", ((PolymorphicFather.Cat) cat).getMeowMessage());
}
The classes don't need to be inner classes, it was just written like this to reduce the line number.
Sometimes you have types little enough that an entire class is redundant, in those cases you could just explore the inner object and map it's key to real keys, this operation is called "unfolding" and it's not easy to descrive, so as always let's write an example to make it clearer:
Given the yaml:
document: "this"
writer:
name: "Snowy"
power: 90001
We can parse it with this single constructor:
@ConfigConstructor
public ExampleTest(
@ConfigProperty("document") String document,
@ConfigProperty("writer.name") String writerName,
@ConfigProperty("writer.power") int writerPower
) {...}
This also works in the Config objects.
All of the parsers used in the @ConfigConstructor
are registered in a
ConfigParserRegistry
, this is also used as a cache for custom class parser and
complex type parsers. You can both create a new ConfigParserRegistry
(if you
don't want to pollute the standard one with custom parsers) or use the stanrdard
parser with ConfigParserRegistry.getStandard()
. Once you obtain the registry
you can get the parser for any object with registry.getFor(type)
, this will
also build the parser for the object if none is available. The Config
class
uses the standard registry as the default one.
In some cases you can't change your own class to implement a @ConfigConstructor
maybe you need to implement a parser for a class of another library.
For this you can use external declarators!
Most of the built-in parsers are not hard-coded inside of the parsing logic,
they are rather defined in a class called StandardExternalDeclarator
in which
all of the methods with the annotation ConfigConstructor
will be registered as
constructors of the type they return. For example instead of writing an
additional constructor for the Vector
type we could write
@ConfigConstructor(inlineable = true)
private Vector parsePosition(@ConfigProperty("x") double x,
@ConfigProperty("y") double y,
@ConfigProperty("z") double z) {
return new Vector(x, y, z);
}
The inlineable parameter indicates that the object could also be constructed from an array, so writing:
vector:
x: 1.0
y: 2.0
z: 3.0
and
vector: [1.0, 2.0, 3.0]
will be interpreted as the same. You also need to register the external declarators, using the appropriate method:
ConfigParserRegistry.getDefault().registerFromDeclarator(new YourExternalDeclarator());
Sometimes you want full control on the parsing method of your custom constructor
(or external declarator), you can do this by having a single Config
as argument,
this will give to the constructor the config without any automated parsing.
You can also take a single Node
as argument, in that case the raw yaml node
will be provided, this is used in some of the standard object declarations.