All Downloads are FREE. Search and download functionalities are using the official Maven repository.

personthecat.catlib.serialization.codec.DynamicCodec Maven / Gradle / Ivy

package personthecat.catlib.serialization.codec;

import com.google.common.collect.ImmutableMap;
import com.mojang.datafixers.util.Pair;
import com.mojang.datafixers.util.Unit;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import org.apache.commons.lang3.mutable.MutableObject;
import personthecat.catlib.exception.UnreachableException;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SuppressWarnings("unused")
public class DynamicCodec implements Codec {
    private final Supplier builder;
    private final Function in;
    private final Function out;
    private final Map> fields;
    private final Map> implicitFields;
    private final Map> requiredFields;

    @SafeVarargs
    public DynamicCodec(final Supplier builder, final Function in, final Function out, final DynamicField... fields) {
        this(builder, in, out, createMap(fields));
    }

    protected DynamicCodec(final Supplier builder, final Function in, final Function out, final Map> fields) {
        this.builder = builder;
        this.in = in;
        this.out = out;
        this.fields = fields;
        this.implicitFields = getFields(fields.values(), DynamicField.Type.IMPLICIT);
        this.requiredFields = getFields(fields.values(), DynamicField.Type.NONNULL);
    }

    public static  Builder builder(final Supplier builder, final Function in, final Function out) {
        return new Builder<>(builder, in, out);
    }

    public DynamicCodec withBuilder(final Supplier builder) {
        return new DynamicCodec<>(builder, this.in, this.out, this.fields);
    }

    public DynamicCodec withReader(final Function reader) {
        return new DynamicCodec<>(this.builder, reader, this.out, this.fields);
    }

    public DynamicCodec withWriter(final Function writer) {
        return new DynamicCodec<>(this.builder, this.in, writer, this.fields);
    }

    @SafeVarargs
    public final DynamicCodec withMoreFields(final DynamicField... fields) {
        final ImmutableMap.Builder> map = ImmutableMap.builder();
        map.putAll(this.fields);
        map.putAll(createMap(fields));
        return new DynamicCodec<>(this.builder, this.in, this.out, map.build());
    }

    public DynamicCodec withoutFields(final String... fields) {
        final Map> map = new HashMap<>(this.fields);
        for (final String field : fields) {
            map.remove(field);
        }
        return new DynamicCodec<>(this.builder, this.in, this.out, ImmutableMap.copyOf(map));
    }

    @SafeVarargs
    public final DynamicCodec withoutFields(final DynamicField... fields) {
        final Map> map = new HashMap<>(this.fields);
        for (final DynamicField field : fields) {
            map.remove(field.key);
        }
        return new DynamicCodec<>(this.builder, this.in, this.out, ImmutableMap.copyOf(map));
    }

    private static  Map> createMap(final DynamicField[] fields) {
        final ImmutableMap.Builder> map = ImmutableMap.builder();
        for (final DynamicField field : fields) {
            map.put(field.key, field);
        }
        return map.build();
    }

    private static  Map> getFields(
        final Collection> all, final DynamicField.Type type) {

        final ImmutableMap.Builder> fields = ImmutableMap.builder();
        for (final DynamicField field : all) {
            if (field.type == type) {
                fields.put(field.key, field);
            }
        }
        return fields.build();
    }

    @Override
    @SuppressWarnings("unchecked")
    public  DataResult encode(final A input, final DynamicOps ops, final T prefix) {
        if (input == null) {
            return DataResult.error("Input is null");
        }
        final R reader = this.in.apply(input);
        final Map map = new HashMap<>();
        final List errors = new ArrayList<>();

        for (final DynamicField field : this.fields.values()) {
            Codec type = (Codec) field.codec;
            if (type == null) type = (Codec) this;

            final Object value = field.getter.apply(reader);
            if (value == null) {
                continue;
            }
            if (field.isImplicit()) {
                type.encode(value, ops, prefix)
                    .resultOrPartial(e -> errors.add(ops.createString(e)))
                    .flatMap(t -> ops.getMapValues(t)
                        .resultOrPartial(e -> errors.add(ops.createString("Implicit value must be a map"))))
                    .ifPresent(values -> values
                        .forEach(pair -> map.put(pair.getFirst(), pair.getSecond())));
            } else {
                type.encodeStart(ops, value)
                    .resultOrPartial(e -> errors.add(ops.createString(e)))
                    .ifPresent(t -> map.put(ops.createString(field.key), t));
            }
        }
        if (!errors.isEmpty()) {
            return DataResult.error("Error encoding builder", ops.createList(errors.stream()));
        }
        return ops.mergeToMap(prefix, map);
    }

    @Override
    @SuppressWarnings("unchecked")
    public  DataResult> decode(final DynamicOps ops, final T input) {
        return ops.getMap(input).flatMap(map -> {
            final B builder = this.builder.get();
            final Stream.Builder failed = Stream.builder();
            final MutableObject> result = new MutableObject<>(DataResult.success(Unit.INSTANCE));
            final Map> required = new HashMap<>(this.requiredFields);

            for (final DynamicField field : this.implicitFields.values()) {
                if (field.codec == null) throw new UnreachableException();
                final DataResult> element = ((Codec) field.codec).decode(ops, input);
                element.error().ifPresent(e -> failed.add(ops.createString(field.key)));
                result.setValue(result.getValue().apply2stable((r, v) -> {
                    ((BiConsumer) field.setter).accept(builder, v.getFirst());
                    return r;
                }, element));
            }
            for (final Map.Entry> entry : this.fields.entrySet()) {
                final DynamicField field = (DynamicField) entry.getValue();
                final T value = map.get(entry.getKey());
                if (field != null && !field.isImplicit()) {
                    if (value == null) {
                        if (field.isNullable()) {
                            field.setter.accept(builder, null);
                        }
                        continue;
                    }
                    required.remove(field.key);
                    Codec codec = field.codec;
                    if (codec == null) codec = (Codec) this;
                    final DataResult> element = codec.decode(ops,  value);

                    element.error().ifPresent(e -> failed.add(value));
                    result.setValue(result.getValue().apply2stable((r, v) -> {
                        field.setter.accept(builder, v.getFirst());
                        return r;
                    }, element));
                }
            }
            if (!required.isEmpty()) {
                for (final String missing : required.keySet()) {
                    failed.add(ops.createString(missing));
                }
                result.setValue(DataResult.error("Required values are missing"));
            }
            final Pair pair = Pair.of(this.out.apply(builder), ops.createList(failed.build()));
            return result.getValue().map(unit -> pair).setPartial(pair);
        });
    }

    public static class Builder {
        private final Supplier builder;
        private final Function in;
        private final Function out;

        public Builder(final Supplier builder, final Function in, final Function out) {
            this.builder = builder;
            this.in = in;
            this.out = out;
        }

        @SafeVarargs
        public final DynamicCodec create(final DynamicField... fields) {
            return new DynamicCodec<>(this.builder, this.in, this.out, fields);
        }
    }
}