001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.spongepowered.configurate.extra.dfu.v3;
018
019import static java.util.Objects.requireNonNull;
020
021import com.google.common.collect.ImmutableList;
022import com.google.common.collect.ImmutableMap;
023import com.mojang.datafixers.util.Pair;
024import com.mojang.serialization.DataResult;
025import com.mojang.serialization.Dynamic;
026import com.mojang.serialization.DynamicOps;
027import com.mojang.serialization.MapLike;
028import org.checkerframework.checker.nullness.qual.Nullable;
029import org.spongepowered.configurate.BasicConfigurationNode;
030import org.spongepowered.configurate.CommentedConfigurationNode;
031import org.spongepowered.configurate.ConfigurationNode;
032import org.spongepowered.configurate.ConfigurationNodeFactory;
033import org.spongepowered.configurate.serialize.TypeSerializerCollection;
034
035import java.nio.ByteBuffer;
036import java.util.List;
037import java.util.Map;
038import java.util.function.Consumer;
039import java.util.function.Function;
040import java.util.stream.IntStream;
041import java.util.stream.LongStream;
042import java.util.stream.Stream;
043
044/**
045 * Implementation of DataFixerUpper's DynamicOps.
046 *
047 * <p>The {@link DynamicOps} interface should be thought of essentially as a way
048 * to perform operations on a type without having to directly implement that
049 * interface on the type. Rather than taking an object that implements an
050 * interface, DFU methods take the implementation of the DynamicOps interface
051 * plus the type implemented onto.
052 *
053 * <p>When possible, the first node's {@link ConfigurationNode#copy()} method
054 * will be used to create a new node to contain results. Otherwise, the provided
055 * factory will be used. The default factory creates a
056 * {@link CommentedConfigurationNode} with the default serializer collection
057 * but a custom factory may be provided.
058 *
059 * <p>DynamicOps has the following primitive types (as determined by those
060 * codecs that implement {@link com.mojang.serialization.codecs.PrimitiveCodec}):
061 * <dl>
062 *     <dt>boolean</dt>
063 *     <dd>literal boolean, or numeric 1 for true, 0 for false</dd>
064 *     <dt>byte</dt>
065 *     <dd>numeric value, {@link Number#byteValue() coerced} to a byte.
066 *     If {@link #compressMaps()}, a string may be parsed as a byte as well.</dd>
067 *     <dt>short</dt>
068 *     <dd>numeric value, {@link Number#shortValue() coerced} to a short.
069 *     If {@link #compressMaps()}, a string may be parsed as a short as well.</dd>
070 *     <dt>int</dt>
071 *     <dd>numeric value, {@link Number#intValue() coerced} to an integer.
072 *     If {@link #compressMaps()}, a string may be parsed as an integer as well.</dd>
073 *     <dt>long</dt>
074 *     <dd>numeric value, {@link Number#longValue() coerced} to a long.
075 *     If {@link #compressMaps()}, a string may be parsed as a long as well.</dd>
076 *     <dt>float</dt>
077 *     <dd>numeric value, {@link Number#floatValue() coerced} to a float.
078 *     If {@link #compressMaps()}, a string may be parsed as a float as well.</dd>
079 *     <dt>double</dt>
080 *     <dd>numeric value, {@link Number#doubleValue() coerced} to a double.
081 *     If {@link #compressMaps()}, a string may be parsed as a double as well.</dd>
082 *     <dt>{@link String}</dt>
083 *     <dd>Any scalar value, as {@link Object#toString() a string}</dd>
084 *     <dt>{@link java.nio.ByteBuffer}</dt>
085 *     <dd>An array of bytes. Either a native byte array in the node,
086 *         or (by default impl) a list of bytes</dd>
087 *     <dt>{@link java.util.stream.IntStream}</dt>
088 *     <dd>A sequence of integers. Either a native int array in the node,
089 *         or (by default impl) a list of integers</dd>
090 *     <dt>{@link java.util.stream.LongStream}</dt>
091 *     <dd>A sequence of longs. Either a native long array in the node,
092 *         or (by default impl) a list of longs</dd>
093 * </dl>
094 *
095 * @since 4.0.0
096 */
097public final class ConfigurateOps implements DynamicOps<ConfigurationNode> {
098
099    private static final ConfigurateOps UNCOMPRESSED = ConfigurateOps.builder().build();
100    private static final ConfigurateOps COMPRESSED = ConfigurateOps.builder().compressed(true).build();
101
102    private final ConfigurationNodeFactory<? extends ConfigurationNode> factory;
103    private final boolean compressed;
104    private final Protection readProtection;
105    private final Protection writeProtection;
106
107    /**
108     * Get the shared instance of this class, which creates new nodes using
109     * the default factory. The returned instance will not be compressed
110     *
111     * @return the shared instance
112     * @since 4.0.0
113     */
114    public static DynamicOps<ConfigurationNode> instance() {
115        return instance(false);
116    }
117
118    /**
119     * Get the shared instance of this class, which creates new nodes using
120     * the default factory.
121     *
122     * <p>See {@link #compressMaps()} for a description of what the
123     * <pre>compressed</pre> parameter does.
124     *
125     * @param compressed whether keys should be compressed in the output of
126     *     this serializer
127     * @return the shared instance
128     * @since 4.0.0
129     */
130    public static DynamicOps<ConfigurationNode> instance(final boolean compressed) {
131        return compressed ? COMPRESSED : UNCOMPRESSED;
132    }
133
134    /**
135     * Get an ops instance that will create nodes using the provided collection.
136     *
137     * @param collection collection to provide through created nodes' options
138     * @return ops instance
139     * @since 4.0.0
140     */
141    public static DynamicOps<ConfigurationNode> forSerializers(final TypeSerializerCollection collection) {
142        if (requireNonNull(collection, "collection").equals(TypeSerializerCollection.defaults())) {
143            return UNCOMPRESSED;
144        } else {
145            return builder().factoryFromSerializers(collection).build();
146        }
147    }
148
149    /**
150     * Wrap a ConfigurationNode in a {@link Dynamic} instance. The returned
151     * Dynamic will use the same type serializer collection as the original node
152     * for its operations.
153     *
154     * @param node the node to wrap
155     * @return a wrapped node
156     * @since 4.0.0
157     */
158    public static Dynamic<ConfigurationNode> wrap(final ConfigurationNode node) {
159        if (node.options().serializers().equals(TypeSerializerCollection.defaults())) {
160            return new Dynamic<>(instance(), node);
161        } else {
162            return builder().factoryFromNode(node).buildWrapping(node);
163        }
164    }
165
166    /**
167     * Configure an ops instance using the options of an existing node.
168     *
169     * @param value the value type
170     * @return values
171     * @since 4.0.0
172     */
173    public static DynamicOps<ConfigurationNode> fromNode(final ConfigurationNode value) {
174        return builder().factoryFromNode(value).build();
175    }
176
177    /**
178     * Create a new builder for an ops instance.
179     *
180     * @return builder
181     * @since 4.0.0
182     */
183    public static ConfigurateOpsBuilder builder() {
184        return new ConfigurateOpsBuilder();
185    }
186
187    ConfigurateOps(final ConfigurationNodeFactory<? extends ConfigurationNode> factory, final boolean compressed,
188            final Protection readProtection, final Protection writeProtection) {
189        this.factory = factory;
190        this.compressed = compressed;
191        this.readProtection = readProtection;
192        this.writeProtection = writeProtection;
193    }
194
195    /**
196     * Whether data passed through this ops will be compressed or not.
197     *
198     * <p>In the context of DFU, <pre>compressed</pre> means that in situations
199     * where values are of a {@link com.mojang.serialization.Keyable} type
200     * (as is with types like Minecraft Registries)
201     * rather than fully encoding each value, its index into the container
202     * is encoded.
203     *
204     * <p>While data encoded this way may take less space to store, the
205     * compressed data will also require an explicit mapping of indices to
206     * values. If this is not stored with the node, the indices of values must
207     * be preserved to correctly deserialize compressed values.
208     *
209     * <p>For example, for an enum new values could only be appended, not added
210     * in the middle of the constants.
211     *
212     * @return whether maps are compressed
213     * @since 4.0.0
214     */
215    @Override
216    public boolean compressMaps() {
217        return this.compressed;
218    }
219
220    /**
221     * Extract a value from a node used as a {@code key} in DFU methods.
222     *
223     * <p>This currently only attempts to interpret the key as a single level
224     * down. However, we may want to try to extract an array or iterable of path
225     * elements to be able to traverse multiple levels.
226     *
227     * @param node data source
228     * @return a key, asserted non-null
229     */
230    static Object keyFrom(final ConfigurationNode node) {
231        if (node.isList() || node.isMap()) {
232            throw new IllegalArgumentException("Key nodes must have scalar values");
233        }
234        return requireNonNull(node.raw(), "The provided key node must have a value");
235    }
236
237    /**
238     * Guard source node according to ops instance's copying policy.
239     *
240     * @implNote currently, this will make a deep copy of the node.
241     *
242     * @param untrusted original node
243     * @return a node with equivalent data
244     */
245    ConfigurationNode guardOutputRead(final ConfigurationNode untrusted) {
246        switch (this.readProtection) {
247            case COPY_DEEP: return untrusted.copy();
248            case NONE: return untrusted;
249            default: throw new IllegalArgumentException("Unexpected state");
250        }
251    }
252
253    ConfigurationNode guardInputWrite(final ConfigurationNode untrusted) {
254        switch (this.writeProtection) {
255            case COPY_DEEP: return untrusted.copy();
256            case NONE: return untrusted;
257            default: throw new IllegalArgumentException("Unexpected state");
258        }
259    }
260
261    /**
262     * Create a new empty node using this ops instance's factory.
263     *
264     * @return the new node
265     */
266    @Override
267    public ConfigurationNode empty() {
268        return this.factory.createNode();
269    }
270
271    @Override
272    public ConfigurationNode emptyMap() {
273        return empty().raw(ImmutableMap.of());
274    }
275
276    @Override
277    public ConfigurationNode emptyList() {
278        return empty().raw(ImmutableList.of());
279    }
280
281    // If the destination ops is another Configurate ops instance, just directly pass the node through
282    @SuppressWarnings("unchecked")
283    private <U> @Nullable U convertSelf(final DynamicOps<U> outOps, final ConfigurationNode input) {
284        if (outOps instanceof ConfigurateOps) {
285            return (U) input;
286        } else {
287            return null;
288        }
289    }
290
291    /**
292     * Create a copy of the source node converted to a different data structure.
293     *
294     * <p>Value types will be preserved as much as possible, but a reverse
295     * conversion will most likely be lossy
296     *
297     * @param targetOps output type
298     * @param source source value
299     * @param <U> output type
300     * @return output value
301     */
302    @Override
303    public <U> U convertTo(final DynamicOps<U> targetOps, final ConfigurationNode source) {
304        final @Nullable U self = convertSelf(requireNonNull(targetOps, "targetOps"), requireNonNull(source, "source"));
305        if (self != null) {
306            return self;
307        }
308
309        if (source.isMap()) {
310            return convertMap(targetOps, source);
311        } else if (source.isList()) {
312            return convertList(targetOps, source);
313        } else {
314            final @Nullable Object value = source.rawScalar();
315            if (value == null) {
316                return targetOps.empty();
317            } else if (value instanceof String) {
318                return targetOps.createString((String) value);
319            } else if (value instanceof Boolean) {
320                return targetOps.createBoolean((Boolean) value);
321            } else if (value instanceof Short) {
322                return targetOps.createShort((Short) value);
323            } else if (value instanceof Integer) {
324                return targetOps.createInt((Integer) value);
325            } else if (value instanceof Long) {
326                return targetOps.createLong((Long) value);
327            } else if (value instanceof Float) {
328                return targetOps.createFloat((Float) value);
329            } else if (value instanceof Double) {
330                return targetOps.createDouble((Double) value);
331            } else if (value instanceof Byte) {
332                return targetOps.createByte((Byte) value);
333            } else if (value instanceof byte[]) {
334                return targetOps.createByteList(ByteBuffer.wrap((byte[]) value));
335            } else if (value instanceof int[]) {
336                return targetOps.createIntList(IntStream.of((int[]) value));
337            } else if (value instanceof long[]) {
338                return targetOps.createLongList(LongStream.of((long[]) value));
339            } else {
340                throw new IllegalArgumentException("Scalar value '" + source + "' has an unknown type: " + value.getClass().getName());
341            }
342        }
343    }
344
345    /**
346     * Get the value of the provided node if it is a number or boolean.
347     *
348     * <p>If {@link #compressMaps()} is true, values may be coerced from
349     * another type.
350     *
351     * @param input data source
352     * @return extracted number
353     */
354    @Override
355    public DataResult<Number> getNumberValue(final ConfigurationNode input) {
356        if (!(input.isMap() || input.isList())) {
357            final @Nullable Object value = input.raw();
358            if (value instanceof Number) {
359                return DataResult.success((Number) value);
360            } else if (value instanceof Boolean) {
361                return DataResult.success((boolean) value ? 1 : 0);
362            }
363
364            if (compressMaps()) {
365                final int result = input.getInt(Integer.MIN_VALUE);
366                if (result == Integer.MIN_VALUE) {
367                    return DataResult.error("Value is not a number");
368                }
369                return DataResult.success(result);
370            }
371        }
372
373        return DataResult.error("Not a number: " + input);
374    }
375
376    /**
377     * Get the value of the provided node if it is a scalar, converted to
378     * a {@link String}.
379     *
380     * @param input data source
381     * @return string | error
382     */
383    @Override
384    public DataResult<String> getStringValue(final ConfigurationNode input) {
385        final @Nullable String value = input.getString();
386        if (value != null) {
387            return DataResult.success(value);
388        }
389
390        return DataResult.error("Not a string: " + input);
391    }
392
393    /**
394     * Create a new node using this ops instance's node factory,
395     * and set its value to the provided number.
396     *
397     * @param value value
398     * @return new node with value
399     */
400    @Override
401    public ConfigurationNode createNumeric(final Number value) {
402        return empty().raw(requireNonNull(value, "value"));
403    }
404
405    /**
406     * Create a new node using this ops instance's node factory,
407     * and set its value to the provided boolean.
408     *
409     * @param value value
410     * @return new node with value
411     */
412    @Override
413    public ConfigurationNode createBoolean(final boolean value) {
414        return empty().raw(value);
415    }
416
417    /**
418     * Create a new node using this ops instance's node factory,
419     * and set its value to the provided string.
420     *
421     * @param value value
422     * @return new node with value
423     */
424    @Override
425    public ConfigurationNode createString(final String value) {
426        return empty().raw(requireNonNull(value, "value"));
427    }
428
429    /**
430     * Return a result where if {@code prefix} is empty, the node is
431     * {@code value}, but otherwise returns an error.
432     *
433     * @param prefix starting value
434     * @param value to update base with
435     * @return result of updated node or error
436     */
437    @Override
438    public DataResult<ConfigurationNode> mergeToPrimitive(final ConfigurationNode prefix, final ConfigurationNode value) {
439        if (!prefix.empty()) {
440            return DataResult.error("Cannot merge " + value + " into non-empty node " + prefix);
441        }
442        return DataResult.success(guardOutputRead(value));
443    }
444
445    /**
446     * Appends element {@code value} to list node {@code input}.
447     *
448     * @param input base node. Must be empty or of list type
449     * @param value value to add as element to the list
450     * @return success with modified node, or error if {@code input} contains a
451     *          non-{@link ConfigurationNode#isList() list} value
452     */
453    @Override
454    public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final ConfigurationNode value) {
455        if (input.isList() || input.empty()) {
456            final ConfigurationNode ret = guardOutputRead(input);
457            ret.appendListNode().from(value);
458            return DataResult.success(ret);
459        }
460
461        return DataResult.error("mergeToList called on a node which is not a list: " + input, input);
462    }
463
464    /**
465     * Appends nodes in {@code values} to copy of list node {@code input}.
466     *
467     * @param input base node. Must be empty or of list type
468     * @param values list of values to append to base node
469     * @return success with modified node, or error if {@code input} contains a
470     *          non-{@link ConfigurationNode#isList() list} value
471     */
472    @Override
473    public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final List<ConfigurationNode> values) {
474        if (input.isList() || input.empty()) {
475            final ConfigurationNode ret = guardInputWrite(input);
476            for (ConfigurationNode node : values) {
477                ret.appendListNode().from(node);
478            }
479            return DataResult.success(ret);
480        }
481
482        return DataResult.error("mergeToList called on a node which is not a list: " + input, input);
483    }
484
485    /**
486     * Update the child of {@code input} at {@code key} with {@code value}.
487     *
488     * <p>This operation will only affect the returned copy of the input node
489     *
490     * @param input base node. Must be empty or of map type
491     * @param key key relative to base node
492     * @param value value to set at empty node
493     * @return success with modified node, or error if {@code input} contains a
494     *          non-{@link ConfigurationNode#isList() list} value
495     */
496    @Override
497    public DataResult<ConfigurationNode> mergeToMap(final ConfigurationNode input, final ConfigurationNode key, final ConfigurationNode value) {
498        if (input.isMap() || input.empty()) {
499            final ConfigurationNode copied = guardInputWrite(input);
500            copied.node(keyFrom(key)).from(value);
501            return DataResult.success(copied);
502        }
503
504        return DataResult.error("mergeToMap called on a node which is not a map: " + input, input);
505    }
506
507    /**
508     * Return a stream of pairs of (key, value) for map data in the input node.
509     *
510     * <p>If the input node is non-empty and not a map, the result will
511     * be a failure.
512     *
513     * @param input input node
514     * @return result, if successful, of a stream of pairs (key, value) of
515     *          entries in the input node.
516     */
517    @Override
518    public DataResult<Stream<Pair<ConfigurationNode, ConfigurationNode>>> getMapValues(final ConfigurationNode input) {
519        if (input.empty() || input.isMap()) {
520            return DataResult.success(input.childrenMap().entrySet().stream()
521                    .map(entry -> Pair.of(BasicConfigurationNode.root(input.options()).raw(entry.getKey()),
522                        guardOutputRead(entry.getValue()))));
523        }
524
525        return DataResult.error("Not a map: " + input);
526    }
527
528    /**
529     * Get a map-like view of a copy of the contents of {@code input}.
530     *
531     * <p>If the input node is non-empty and not a map, the result will
532     * be a failure.
533     *
534     * @param input input node
535     * @return result, if successful, of map-like view of a copy of the input
536     */
537    @Override
538    public DataResult<MapLike<ConfigurationNode>> getMap(final ConfigurationNode input) {
539        if (input.empty() || input.isMap()) {
540            return DataResult.success(new NodeMaplike(this, input.options(), input.childrenMap()));
541        } else {
542            return DataResult.error("Input node is not a map");
543        }
544    }
545
546    /**
547     * Get a consumer that takes an action to perform on every element of
548     * list node {@code input}.
549     *
550     * <p>As an example, to print out every node in a list
551     *      (minus error checking):
552     *     <pre>
553     *         getList(listNode).result().get()
554     *             .accept(element -&gt; System.out.println(element);
555     *     </pre>
556     *
557     * @param input data source
558     * @return result, that if successful will take an action to perform on
559     *          every element
560     */
561    @Override
562    public DataResult<Consumer<Consumer<ConfigurationNode>>> getList(final ConfigurationNode input) {
563        if (input.isList()) {
564            return DataResult.success(action -> {
565                for (ConfigurationNode child : input.childrenList()) {
566                    action.accept(guardOutputRead(child));
567                }
568            });
569        } else {
570            return DataResult.error("Input node is not a list");
571        }
572    }
573
574    /**
575     * Get the contents of list node {@code input} as a {@link Stream} of nodes.
576     *
577     * @param input data source
578     * @return if node is empty or a list, stream of nodes
579     */
580    @Override
581    public DataResult<Stream<ConfigurationNode>> getStream(final ConfigurationNode input) {
582        if (input.empty() || input.isList()) {
583            final Stream<ConfigurationNode> stream = input.childrenList().stream().map(this::guardOutputRead);
584            return DataResult.success(stream);
585        }
586
587        return DataResult.error("Not a list: " + input);
588    }
589
590    /**
591     * Create a new node containing the map entries from the
592     * stream {@code values}.
593     *
594     * <p>Keys will be interpreted as a single Object, and can only
595     * currently access direct children.
596     *
597     * @param values entries in the map
598     * @return newly created node
599     */
600    @Override
601    public ConfigurationNode createMap(final Stream<Pair<ConfigurationNode, ConfigurationNode>> values) {
602        final ConfigurationNode ret = empty();
603
604        values.forEach(p -> ret.node(keyFrom(p.getFirst())).from(p.getSecond()));
605
606        return ret;
607    }
608
609    /**
610     * Create a new node containing the map entries from the
611     * map {@code values}.
612     *
613     * <p>Keys will be interpreted as a single Object, and can only
614     * currently access direct children.
615     *
616     * @param values unwrapped node map
617     * @return newly created node
618     */
619    @Override
620    public ConfigurationNode createMap(final Map<ConfigurationNode, ConfigurationNode> values) {
621        final ConfigurationNode ret = empty();
622
623        for (Map.Entry<ConfigurationNode, ConfigurationNode> entry : values.entrySet()) {
624            ret.node(keyFrom(entry.getKey())).from(entry.getValue());
625        }
626
627        return ret;
628    }
629
630    /**
631     * Create a new node containing values emitted by {@code input} as
632     * list elements.
633     *
634     * @param input data source
635     * @return newly created node
636     */
637    @Override
638    public ConfigurationNode createList(final Stream<ConfigurationNode> input) {
639        final ConfigurationNode ret = empty();
640        input.forEach(it -> ret.appendListNode().from(it));
641        return ret;
642    }
643
644    /**
645     * Get a copy of {@code input} without the value at node {@code key}.
646     *
647     * <p>If the input node is not a map, the input node will be returned.
648     *
649     * @param input data source
650     * @param key key to the node to be removed
651     * @return if node removed, a copy of the input without node,
652     *          otherwise input
653     */
654    @Override
655    public ConfigurationNode remove(final ConfigurationNode input, final String key) {
656        if (input.isMap()) {
657            final ConfigurationNode ret = guardInputWrite(input);
658            ret.node(key).raw(null);
659            return ret;
660        }
661
662        return input;
663    }
664
665    /**
666     * Attempt to get the child of {@code input} at {@code key}.
667     *
668     * @param input data source
669     * @param key child key
670     * @return success containing child if child is non-virtual,
671     *          otherwise failure
672     */
673    @Override
674    public DataResult<ConfigurationNode> get(final ConfigurationNode input, final String key) {
675        final ConfigurationNode ret = input.node(key);
676        return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret));
677    }
678
679    /**
680     * Get a child of the provided node at {@code key}.
681     *
682     * <p>Keys will be interpreted as a single Object, and can only
683     * currently access direct children.
684     *
685     * @param input parent node
686     * @param key wrapped key of child
687     * @return success containing child if child is non-virtual,
688     *          otherwise failure
689     */
690    @Override
691    public DataResult<ConfigurationNode> getGeneric(final ConfigurationNode input, final ConfigurationNode key) {
692        final ConfigurationNode ret = input.node(keyFrom(key));
693        return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret));
694    }
695
696    /**
697     * Update a copy of {@code input} with {@code value} at path {@code key}.
698     *
699     * @param input data source
700     * @param key key of child node
701     * @param value value for child node
702     * @return updated parent node
703     */
704    @Override
705    public ConfigurationNode set(final ConfigurationNode input, final String key, final ConfigurationNode value) {
706        final ConfigurationNode ret = guardInputWrite(input);
707        ret.node(key).from(value);
708        return ret;
709    }
710
711    /**
712     * Copies the input node and transform its child at {@code key}.
713     *
714     * <p>Return a copy of the input node with the child at {@code key}
715     * transformed by the provided function
716     *
717     * <p>If there is no value at {@code key}, the input node will be
718     * returned unmodified.
719     *
720     * @param input base value
721     * @param key key to change
722     * @param function function to process the node at {@code wrappedKey}
723     * @return an updated copy of input node
724     */
725    @Override
726    public ConfigurationNode update(final ConfigurationNode input, final String key, final Function<ConfigurationNode, ConfigurationNode> function) {
727        if (input.node(key).virtual()) {
728            return input;
729        }
730
731        final ConfigurationNode ret = guardInputWrite(input);
732        final ConfigurationNode child = ret.node(key);
733        child.from(function.apply(child));
734        return ret;
735    }
736
737    /**
738     * Copies the input node and transform the node at {@code wrappedKey}.
739     *
740     * <p>Return a copy of the input node with the child at {@code wrappedKey}
741     * transformed by the provided function
742     *
743     * <p>If there is no value at {@code wrappedKey}, the input node will be
744     * returned unmodified.
745     *
746     * <p>Keys will be interpreted as a single Object, and can only
747     * currently access direct children.
748     *
749     * @param input base value
750     * @param wrappedKey key to change
751     * @param function function to process the node at {@code wrappedKey}
752     * @return an updated copy of input node
753     */
754    @Override
755    public ConfigurationNode updateGeneric(final ConfigurationNode input, final ConfigurationNode wrappedKey,
756            final Function<ConfigurationNode, ConfigurationNode> function) {
757        final Object key = keyFrom(wrappedKey);
758        if (input.node(key).virtual()) {
759            return input;
760        }
761
762        final ConfigurationNode ret = guardInputWrite(input);
763
764        final ConfigurationNode child = ret.node(key);
765        child.from(function.apply(child));
766        return ret;
767    }
768
769    @Override
770    public String toString() {
771        return "Configurate";
772    }
773
774    /**
775     * Protection level for configuration node accesses through ops instance.
776     *
777     * @since 4.0.0
778     */
779    public enum Protection {
780        /**
781         * When an operation is executed on the node, make a deep copy of the
782         * result.
783         */
784        COPY_DEEP,
785
786        /**
787         * Directly pass on nodes, still attached to their original structure.
788         */
789        NONE
790
791    }
792
793}