Resource ConfigHelper | Schema/ConfigDoc

Discussion in 'Spigot Plugin Development' started by anhcraft, Oct 6, 2019.

  1. Hi, today I want to share my latest library: ConfigHelper. Basically, it helps you to manipulate the configuration easier.

    The problem
    I have a plugin that has a huge amount of configuration entries. It has to read all from files and make objects for caching. Everything was fine except for the maintainability. The config is getting expanded and being more complex, while I am too lazy just to update the comments and add duplicated thing like config.getString(), etc
    That's why I need a thing that can give me simpler configuration management. I have searched but I can't find anything suitable. I gave up and made my own. Yeah, own-made stuff is better.

    What is this?
    This is a small library that provides a few custom Java annotations to define the configuration in schemas. Then, you can read and write configuration from/to Java objects with that schema.

    GitHub
    : https://github.com/anhcraft/confighelper/

    Schema
    The first thing is the schema. It is the structure of the config
    Code (Text):
    @Schema
    public class Config {
    }
    The annotation Schema is located at the top of a class.

    To create a schema:
    Code (Text):
    public static final ConfigSchema<Config> SCHEMA = ConfigSchema.of(Config.class);
    You are encouraged to create the schema of a config only one-time and save it somewhere, like a static field.

    Next, about the entry. A field annotated Key represents a config entry.
    Code (Text):
    @Key("name")
    private String name;

    private int age; // this field does not have @Key, not related to the config
    .

    You can give more information to the entry:
    Code (Text):
    @Key("this.is.the.path")
    @Explanation("This entry is used to ....")
    private double chance;

    @Key("this.is.the.path")
    @Explanation({
      "Line 1",
      "Line 2"
    })
    private double chance;
    .

    Built-in validators
    Code (Text):
    @Key("path.to.entry")
    @Validation(notNull = true)
    private String name = "default name";

    @NotNull
    public String name(){
       return name;
    }
    .

    Ignore specific value:
    Code (Text):
    @Key("test")
    @IgnoreValue(ifNull = true)
    public List<String> furniture = new ArrayList<>();
    In the code above, users can't make the field value to be null. It always remains an empty list, until the user overrides with a non-null value.
    .
    PrettyEnum:
    This annotation is used with enum fields. The enum's name will be saved to the config.
    Code (Text):
    @Key("material")
    @PrettyEnum
    private Material material;
    If the enum is not found, null will be returned instead. This annotation can be put together with IgnoreValue!
    Code (Text):
    @Key("material")
    @PrettyEnum
    @IgnoreValue(ifNull = true)
    private Material material = Material.AIR; // always not-null
    .

    Nested schemas support:
    Code (Text):
    @Schema
    public class A {
      @Key("b_class")
      private B b_instance;
    }

    @Schema
    public class B {
      @Key("c_class")
      private List<C> c_instances;

      @Key("num")
      private int number = -1;
    }

    @Schema
    public class C {
       @Key("field")
       private boolean f;
    }
    In this case, each class acts like a type of section. Field values will be serialized and deserialized automatically between normal configuration sections and Java objects.
    What the config would be if I choose class A is the root section?
    Code (Text):
    b_class:
      number: -1
      c_class:
        f: false
    Note: you don't need to make every class for every section since key names can contain dots:
    Code (Text):
    @Key("this.is.an.entry")
    private Location location;
    .

    More: you can use this feature with list and array:
    Code (Text):
    @Key("collections")
    private List<Album> albums;

    // Album class
    @Schema // must have
    public class Album{
       @Key("name")
       private String name;
    }

    Inheritance support:
    Code (Text):
    public class Child extends Parent {
      @Key("name")
      private String name;
    }

    public class Parent {
      @Key("age")
      private int age;
    }
    .

    Load/save config:
    Code (Text):
    ConfigHelper.writeConfig(conf, SCHEMA, instance);
    ConfigHelper.readConfig(conf, SCHEMA);
    Middleware
    I got this name from Express (node.js). This annotation goes along with methods to add extra handlers. It is really useful to change the value in hacky ways.
    The annotation also has an optional field that indicates the direction to listen to. There are two directions: from config to schema, and vice-versa. Defaults to all.

    Code (Text):
    @Key("n.a.m.e");
    private String name;

    @Middleware
    public void handle(ConfigSchema.Entry entry, Object value){
      if(entry.getKey().equals("n.a.m.e")){
         return ((String) value).trim();
      }
      return value; // return the value for the next middleware
    }
    ConfigDoc
    A tool for generating configuration docs (like JavaDoc).
    ConfigDoc is able to link schemas together and identify Java classes to create links to JavaDocs (if provided). The Javadocs of Bukkit/Spigot and Paper are supported by default.

    Example code:
    Code (Text):
        public void generateDoc(){
            new ConfigDoc()
                    .withSchemaOf(Schema1.class)
                    .withSchemaOf(Schema2.class)
                    .withSchemaOf(Schema3.class)
                    .withSchema(SCHEMA) // if you already have the schema
                    .generate(new File(getDataFolder(), "docs"));
        }
    [​IMG]
    Demo:
    https://anhcraft.dev/cd/confighelper/

    FAQ
    1. Does this library impact the server's performance?
    Likely yes because it uses reflection a lot. But eh do not worry because we rarely load the config again after startup, except a few special cases like executing the reloading command.

    2. What if I don't put Schema at the top of a class?
    It will be handled by SnakeYaml like normal.

    3. Primitive support?
    Already included. If an object is null, it will be skipped before unboxing.

    Examples
    https://github.com/anhcraft/confighelper/tree/master/bukkit/src/test/java/dev/anhcraft/confighelper

    That's all. Feel free to give your opinions.
    I just created it recently with only a few tests so I really need your advice to improve the library :D
     
    #1 anhcraft, Oct 6, 2019
    Last edited: Oct 15, 2019
    • Useful Useful x 6
    • Like Like x 1
    • Creative Creative x 1
  2. Updated v1.0.1:
    - Super-classes must be annotated Schema from now on, so the following code is invalid:
    Code (Text):
    @Schema
    public class Car extends Vehicle{
         
    }

    // Must have @Schema here
    public class Vehicle{
      @Key("price")
      private long price;
    }
    - Reworked on ConfigDoc
    + Added a simple menu on the left side
    + Added footer
    + Can link JavaDocs​
     
    • Like Like x 2
  3. This is cool, I'm sure gonna try it. Thanks
     
    • Like Like x 1
  4. Will definitely give this a go, looks neat :)
     
  5. This looks interesting. Does it also works on other platforms like bungeecord or does it depend on spigot?

    Edit: I see it uses org.bukkit.configuration so most likely no :;(
     
  6. I already planned to support Bungeecord. It may come soon when I have free time.
    But I would like to hear your guys thought first.
     
  7. This reminds me of how json serialisation works.
     
  8. Not depending on any platform so not using any bungee/bukkit API for writing that yaml file would be great :)
     
  9. This is exactly what I've been looking for and more, thanks a lot for creating this.
     
    • Like Like x 1
    • Friendly Friendly x 1
  10. Is the @Explaination annotation supposed to add YAML-comments to the config values or does it only exist for documentation purposes (for the developer)?
     
  11. That annotation is for the documentation only.
    .
    To clarify, the documentation is a thing to replace the comments in config.
    (Also, the documentation is not just for devs only, your users can use it too)
     
  12. Ah, I see. Thanks for the clarification.
     
  13. Updated v1.0.2:
    - I've changed the project structure to have multi-modules
    - Added Bungeecord support (Not tested yet, but I guess it will work)
    - Middleware now has inheritance support
    Code (Java):

    @Schema
    public class ConfigurableObject {
        private String formatColorCodes(String str){
            return ChatColor.translateAlternateColorCodes('&', str);
        }

        private Object color(Object value){
            if(value != null) {
                if (value instanceof String) {
                    return formatColorCodes((String) value);
                } else if (value instanceof List) {
                    List<Object> list = (List<Object>) value;
                    if (!list.isEmpty() && list.get(0) instanceof String) {
                        list.replaceAll(o -> formatColorCodes((String) o));
                        return list;
                    }
                } else if (value instanceof ConfigurationSection) {
                    ConfigurationSection cs = (ConfigurationSection) value;
                    for (String s : cs.getKeys(false)) {
                        Object k = cs.get(s);
                        cs.set(s, color(k));
                    }
                    return cs;
                }
            }
            return value;
        }

        @Middleware(Middleware.Direction.CONFIG_TO_SCHEMA)
        private Object c2s(ConfigSchema.Entry entry, Object value){
            return fromConfig(color(value), entry);
        }

        @Middleware(Middleware.Direction.SCHEMA_TO_CONFIG)
        private Object s2c(ConfigSchema.Entry entry, Object value){
            return toConfig(value, entry);
        }

        /**
            All classes that inherit ConfigurableObject can override two following methods
        **/


        @Nullable
        protected Object fromConfig(@Nullable Object value, ConfigSchema.Entry entry){
            return value;
        }

        @Nullable
        protected Object toConfig(@Nullable Object value, ConfigSchema.Entry entry){
            return value;
        }
    }