DataManipulatorGenerator

Every time I make a DataManipulator, I am struck by how long it takes. It’s the same boilerplate, time after time after time, and it takes FOREVER. And if it takes me this long to write one, imagine how long it takes someone to figure out how to write one if they’ve never done it before. So I decided to just automatically generate them.


I shall anticipate the inevitable comments: This currently works for API 5/6. Completely untested on 7.


DataManipulatorGenerator is a standalone program, which converts a very small HOCON file into a full DataManipulator class and key class. This is the smallest possible configuration:

fields = [
    {
        type = Z
        name = bool
    }
]
plugin-id = "my_plugin_id"

This will generate a full DataManipulator, with methods getBool(), setBool(), bool(), the proper implementation of the entire manipulator, an immutable version, a builder for it, Keys.BOOL, and more.

Here’s the configuration:

The root of the file must have a list of objects called fields[]. Aside from that, there are a few other options.
class sets the name of the class; if absent, the name of the file is used.
key-class sets the name of the key class; if absent, the main class minus “Data” plus “Keys” is used.
package sets the package name.
plugin-id sets the plugin ID; it doesn’t directly modify the output, but it’s used in inferencing key IDs (i.e. <plugin-id>:<fieldname>).
Lastly, there’s the imports[] list, which contains a list of qualified class names to import. This is only necessary for any inner types of generics (e.g. List<ItemType> requires you to import org.spongepowered.api.item.ItemType).

Within a field object:
The two required fields are type, the type of the field, and name, the internal name of the field. It’s encouraged for name to be camelCase, to facilitate name inferencing. As for type, this must be the fully qualified type of the object (e.g. java.lang.String, java.util.UUID, etc.) if it is a class. If it is a primitive, the internal type name is used (the capital first letter of the type, except for boolean which is Z). There are other optional values:

full-type is used for generics. This is how the field should appear in type definition (e.g. List<String>, Map<UUID, Integer>. Non-generics and primitives do not need this field.

transient sets whether or not this field should be excluded from serialization. It won’t show up in toContainer() or from(DataView).

optional sets whether or not the field is optional. This should be used instead of using the literal type Optional for optional values.

default sets the default value for this field. I don’t expect you’ll need to use this unless you have something really convoluted, since the program knows the defaults for a bunch of Java types, Sponge types, CatalogTypes, etc. Note also that any field marked transient won’t have a default set for it in the no-arg constructor.

key{} contains the key-specific settings. Again, this doesn’t need to be present.

Within key{}:

name: The name of the static final Key. Ideally should be UNDERSCORE_CAPS. If not present, generated from the field name.

data-query: The path to use for the DataQuery. Dots are the separator. Ideally should be TitleCase. If not present, generated from the field name.

id: The ID of the Key. Ideally should be lowercase, with a <plugin id>: prefix. If not present, generated from the field name and (if present) plugin-id.

display-name: The display name of the Key. Ideally should be Spaced Title Case. If not present, generated from the field name.


Once you’ve got your configuration set, run DataManipulatorGenerator.jar however you like. If you double-click it, it’ll pop up a file select; if you run it from command-line, you can supply a list of files to it either as program arguments or through stdin.

For each config file, it will generate two Java source files. One is the keys file, which has a static final Key for every field. The other is the manipulator class, which has a class correctly implementing DataManipulator as well as static inner classes Immutable and Builder implementing ImmutableDataManipulator and DataManipulatorBuilder respectively.

The classes won’t contain any errors if the configuration doesn’t, so the generated files should be completely ready to import into whatever project you’re using, and the only remaining setup step is to register it during pre-initialization. Happy data-ing!


I lied, there might be one more step. DataContainer doesn’t have any methods to deserialize arbitrary objects inside Maps, unlike Lists and raw values. Any lines marked //TODO in the generated source need to be implemented manually. Aside from that, though, the generated files should be feature-complete.


Download it here.

14 Likes

@pie_flavor I made a data manipulator that used the type of Inventory as it’s value. For some reason, the player class refuses to respond to the data manipulator, never storing it.

Did you register it? And then offer() it?

Register where? I couldn’t find documentation regarding where to register my keys.

In pre-initialization, you must call DataManager#register.

Ah, duh. Thanks pie.

Actually, do you have any example working code with custom data registering? I can’t figure out how to do it properly.

game.getDataManager().register(TestData.class, TestData.Immutable.class, new TestData.Builder());
1 Like

I’ll note that the generated code doesn’t expose the builder constructor.

Additionally, after properly registering my data, it still gets rejected for whatever reason.

A single log message isn’t extremely helpful - can you provide a bit more information (registration code, data insertion code, DMG config)?

And protected is also package access.

Anyone trying to use the example code and failing - Just noticed an incorrect bracket, updated the OP to fix that.

I’d totally send you the code right now but I’m an idiot and haven’t fixed the remote desktop on my PC at home yet. :stuck_out_tongue:

But here’s just a summary of what I’ve done.
I’ve generated an Inventory dstamanipulator with keys and such. I register it with the code snippet you posted with my.class instead of the one you sent. When I offer it to a player, the player automatically fails it for some reason. I’m not sure why.

fields = [
	{
		type=Inventory
		name=spicyStorage
		key {
			name=SPICY_STORAGE
			display-name="Spicy Storage"
		}
	}
]
class=DespiceData
package=io.lokiraut.despice
plugin-id=despice

package io.lokiraut.despice.data;

import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.manipulator.DataManipulatorBuilder;
import org.spongepowered.api.data.manipulator.immutable.common.AbstractImmutableData;
import org.spongepowered.api.data.manipulator.mutable.common.AbstractData;
import org.spongepowered.api.data.merge.MergeFunction;
import org.spongepowered.api.data.persistence.AbstractDataBuilder;
import org.spongepowered.api.data.persistence.InvalidDataException;
import org.spongepowered.api.data.value.immutable.ImmutableValue;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.api.item.inventory.Inventory;

import javax.annotation.Generated;
import java.util.Optional;

@Generated(value = "flavor.pie.generator.data.DataManipulatorGenerator", date = "2017-02-27T03:27:32.565Z")
public class DespiceData extends AbstractData<DespiceData, DespiceData.Immutable> {

    private Inventory spicyStorage;

    {
        registerGettersAndSetters();
    }

    DespiceData() {
    }

    DespiceData(Inventory spicyStorage) {
        this.spicyStorage = spicyStorage;
    }

    @Override
    protected void registerGettersAndSetters() {
        registerFieldGetter(DespiceKeys.SPICY_STORAGE, this::getSpicyStorage);
        registerFieldSetter(DespiceKeys.SPICY_STORAGE, this::setSpicyStorage);
        registerKeyValue(DespiceKeys.SPICY_STORAGE, this::spicyStorage);
    }

    public Inventory getSpicyStorage() {
        return spicyStorage;
    }

    public void setSpicyStorage(Inventory spicyStorage) {
        this.spicyStorage = spicyStorage;
    }

    public Value<Inventory> spicyStorage() {
        return Sponge.getRegistry().getValueFactory().createValue(DespiceKeys.SPICY_STORAGE, spicyStorage);
    }

    @Override
    public Optional<DespiceData> fill(DataHolder dataHolder, MergeFunction overlap) {
        dataHolder.get(DespiceData.class).ifPresent(that -> {
                DespiceData data = overlap.merge(this, that);
                this.spicyStorage = data.spicyStorage;
        });
        return Optional.of(this);
    }

    @Override
    public Optional<DespiceData> from(DataContainer container) {
        return from((DataView) container);
    }

    public Optional<DespiceData> from(DataView container) {
        container.getObject(DespiceKeys.SPICY_STORAGE.getQuery(), Inventory.class).ifPresent(v -> spicyStorage = v);
        return Optional.of(this);
    }

    @Override
    public DespiceData copy() {
        return new DespiceData(spicyStorage);
    }

    @Override
    public Immutable asImmutable() {
        return new Immutable(spicyStorage);
    }

    @Override
    public int getContentVersion() {
        return 1;
    }

    @Override
    public DataContainer toContainer() {
        return super.toContainer()
                .set(DespiceKeys.SPICY_STORAGE.getQuery(), spicyStorage);
    }

    @Generated(value = "flavor.pie.generator.data.DataManipulatorGenerator", date = "2017-02-27T03:27:32.587Z")
    public static class Immutable extends AbstractImmutableData<Immutable, DespiceData> {

        private Inventory spicyStorage;
        {
            registerGetters();
        }

        Immutable() {
        }

        Immutable(Inventory spicyStorage) {
            this.spicyStorage = spicyStorage;
        }

        @Override
        protected void registerGetters() {
            registerFieldGetter(DespiceKeys.SPICY_STORAGE, this::getSpicyStorage);
            registerKeyValue(DespiceKeys.SPICY_STORAGE, this::spicyStorage);
        }

        public Inventory getSpicyStorage() {
            return spicyStorage;
        }

        public ImmutableValue<Inventory> spicyStorage() {
            return Sponge.getRegistry().getValueFactory().createValue(DespiceKeys.SPICY_STORAGE, spicyStorage).asImmutable();
        }

        @Override
        public DespiceData asMutable() {
            return new DespiceData(spicyStorage);
        }

        @Override
        public int getContentVersion() {
            return 1;
        }

        @Override
        public DataContainer toContainer() {
            return super.toContainer()
                    .set(DespiceKeys.SPICY_STORAGE.getQuery(), spicyStorage);
        }

    }

    @Generated(value = "flavor.pie.generator.data.DataManipulatorGenerator", date = "2017-02-27T03:27:32.591Z")
    public static class Builder extends AbstractDataBuilder<DespiceData> implements DataManipulatorBuilder<DespiceData, Immutable> {

        public Builder() {
            super(DespiceData.class, 1);
        }

        @Override
        public DespiceData create() {
            return new DespiceData();
        }

        @Override
        public Optional<DespiceData> createFrom(DataHolder dataHolder) {
            return create().fill(dataHolder);
        }

        @Override
        protected Optional<DespiceData> buildContent(DataView container) throws InvalidDataException {
            return create().from(container);
        }

    }
}

package io.lokiraut.despice.data;

import com.google.common.reflect.TypeToken;
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.key.Key;
import org.spongepowered.api.data.key.KeyFactory;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.api.item.inventory.Inventory;

import javax.annotation.Generated;

@Generated(value = "flavor.pie.generator.data.DataManipulatorGenerator", date = "2017-02-27T03:27:32.592Z")
public class DespiceKeys {

    private DespiceKeys() {}

    public final static Key<Value<Inventory>> SPICY_STORAGE;
    static {
        TypeToken<Inventory> optionalInventoryToken = new TypeToken<Inventory>(){};
        TypeToken<Value<Inventory>> valueOptionalInventoryToken = new TypeToken<Value<Inventory>>(){};
        SPICY_STORAGE = KeyFactory.makeSingleKey(optionalInventoryToken, valueOptionalInventoryToken, DataQuery.of("spicystorage"), "despice:spicystorage", "spicystorage");
    }
}

Sponge.getDataManager().register(DespiceData.class,DespiceData.Immutable.class,new DespiceData.Builder());

You’ve got the single registration line, but[quote=“pie_flavor, post:10, topic:17080”]
data insertion code
[/quote]

meaning the part where you offer it?

For instance, it’s a caveat of the current implementation that the entire data manipulator must be offered to the player before Keys can be used individually. So if you were using a Key, that would be the problem.

Also, I mentioned that the name in type should be fully qualified; this will auto-import it and such.

1 Like

Oh, I had no idea you had to offer the data manipulator lol

For instance, it’s a caveat of the current implementation that the entire data manipulator must be offered to the player before Keys can be used individually.

Motherf… !

That certainly explains a lot of headaches on the long bumpy road.

2 Likes

Yeah, it confused the shit out of me for a while too.

Note that you still have to offer() after getOrCreate(), even if you don’t modify it; it literally just creates it without setting it.

1 Like

We just need a Data API FAQ or troubleshooting section in the docs.

1 Like

Could you provide a full example implementation? It would be immensely helpful.

Thanks.