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.v4;
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     */
214    @Override
215    public boolean compressMaps() {
216        return this.compressed;
217    }
218
219    /**
220     * Extract a value from a node used as a {@code key} in DFU methods.
221     *
222     * <p>This currently only attempts to interpret the key as a single level
223     * down. However, we may want to try to extract an array or iterable of path
224     * elements to be able to traverse multiple levels.
225     *
226     * @param node data source
227     * @return a key, asserted non-null
228     */
229    static Object keyFrom(final ConfigurationNode node) {
230        if (node.isList() || node.isMap()) {
231            throw new IllegalArgumentException("Key nodes must have scalar values");
232        }
233        return requireNonNull(node.raw(), "The provided key node must have a value");
234    }
235
236    /**
237     * Guard source node according to ops instance's copying policy.
238     *
239     * @implNote currently, this will make a deep copy of the node.
240     *
241     * @param untrusted original node
242     * @return a node with equivalent data
243     */
244    ConfigurationNode guardOutputRead(final ConfigurationNode untrusted) {
245        switch (this.readProtection) {
246            case COPY_DEEP: return untrusted.copy();
247            case NONE: return untrusted;
248            default: throw new IllegalArgumentException("Unexpected state");
249        }
250    }
251
252    ConfigurationNode guardInputWrite(final ConfigurationNode untrusted) {
253        switch (this.writeProtection) {
254            case COPY_DEEP: return untrusted.copy();
255            case NONE: return untrusted;
256            default: throw new IllegalArgumentException("Unexpected state");
257        }
258    }
259
260    /**
261     * Create a new empty node using this ops instance's factory.
262     *
263     * @return the new node
264     */
265    @Override
266    public ConfigurationNode empty() {
267        return this.factory.createNode();
268    }
269
270    @Override
271    public ConfigurationNode emptyMap() {
272        return empty().raw(ImmutableMap.of());
273    }
274
275    @Override
276    public ConfigurationNode emptyList() {
277        return empty().raw(ImmutableList.of());
278    }
279
280    // If the destination ops is another Configurate ops instance, just directly pass the node through
281    @SuppressWarnings("unchecked")
282    private <U> @Nullable U convertSelf(final DynamicOps<U> outOps, final ConfigurationNode input) {
283        if (outOps instanceof ConfigurateOps) {
284            return (U) input;
285        } else {
286            return null;
287        }
288    }
289
290    /**
291     * Create a copy of the source node converted to a different data structure.
292     *
293     * <p>Value types will be preserved as much as possible, but a reverse
294     * conversion will most likely be lossy
295     *
296     * @param targetOps output type
297     * @param source source value
298     * @param <U> output type
299     * @return output value
300     */
301    @Override
302    public <U> U convertTo(final DynamicOps<U> targetOps, final ConfigurationNode source) {
303        final @Nullable U self = convertSelf(requireNonNull(targetOps, "targetOps"), requireNonNull(source, "source"));
304        if (self != null) {
305            return self;
306        }
307
308        if (source.isMap()) {
309            return convertMap(targetOps, source);
310        } else if (source.isList()) {
311            return convertList(targetOps, source);
312        } else {
313            final @Nullable Object value = source.rawScalar();
314            if (value == null) {
315                return targetOps.empty();
316            } else if (value instanceof String) {
317                return targetOps.createString((String) value);
318            } else if (value instanceof Boolean) {
319                return targetOps.createBoolean((Boolean) value);
320            } else if (value instanceof Short) {
321                return targetOps.createShort((Short) value);
322            } else if (value instanceof Integer) {
323                return targetOps.createInt((Integer) value);
324            } else if (value instanceof Long) {
325                return targetOps.createLong((Long) value);
326            } else if (value instanceof Float) {
327                return targetOps.createFloat((Float) value);
328            } else if (value instanceof Double) {
329                return targetOps.createDouble((Double) value);
330            } else if (value instanceof Byte) {
331                return targetOps.createByte((Byte) value);
332            } else if (value instanceof byte[]) {
333                return targetOps.createByteList(ByteBuffer.wrap((byte[]) value));
334            } else if (value instanceof int[]) {
335                return targetOps.createIntList(IntStream.of((int[]) value));
336            } else if (value instanceof long[]) {
337                return targetOps.createLongList(LongStream.of((long[]) value));
338            } else {
339                throw new IllegalArgumentException("Scalar value '" + source + "' has an unknown type: " + value.getClass().getName());
340            }
341        }
342    }
343
344    /**
345     * Get the value of the provided node if it is a number or boolean.
346     *
347     * <p>If {@link #compressMaps()} is true, values may be coerced from
348     * another type.
349     *
350     * @param input data source
351     * @return extracted number
352     */
353    @Override
354    public DataResult<Number> getNumberValue(final ConfigurationNode input) {
355        if (!(input.isMap() || input.isList())) {
356            final @Nullable Object value = input.rawScalar();
357            if (value instanceof Number) {
358                return DataResult.success((Number) value);
359            } else if (value instanceof Boolean) {
360                return DataResult.success((boolean) value ? 1 : 0);
361            }
362
363            if (compressMaps()) {
364                final int result = input.getInt(Integer.MIN_VALUE);
365                if (result == Integer.MIN_VALUE) {
366                    return DataResult.error("Value is not a number");
367                }
368                return DataResult.success(result);
369            }
370        }
371
372        return DataResult.error("Not a number: " + input);
373    }
374
375    /**
376     * Get the value of the provided node if it is a scalar, converted to
377     * a {@link String}.
378     *
379     * @param input data source
380     * @return string | error
381     */
382    @Override
383    public DataResult<String> getStringValue(final ConfigurationNode input) {
384        final @Nullable String value = input.getString();
385        if (value != null) {
386            return DataResult.success(value);
387        }
388
389        return DataResult.error("Not a string: " + input);
390    }
391
392    /**
393     * Create a new node using this ops instance's node factory,
394     * and set its value to the provided number.
395     *
396     * @param value value
397     * @return new node with value
398     */
399    @Override
400    public ConfigurationNode createNumeric(final Number value) {
401        return empty().raw(requireNonNull(value, "value"));
402    }
403
404    /**
405     * Create a new node using this ops instance's node factory,
406     * and set its value to the provided boolean.
407     *
408     * @param value value
409     * @return new node with value
410     */
411    @Override
412    public ConfigurationNode createBoolean(final boolean value) {
413        return empty().raw(value);
414    }
415
416    /**
417     * Create a new node using this ops instance's node factory,
418     * and set its value to the provided string.
419     *
420     * @param value value
421     * @return new node with value
422     */
423    @Override
424    public ConfigurationNode createString(final String value) {
425        return empty().raw(requireNonNull(value, "value"));
426    }
427
428    /**
429     * Return a result where if {@code prefix} is empty, the node is
430     * {@code value}, but otherwise returns an error.
431     *
432     * @param prefix starting value
433     * @param value to update base with
434     * @return result of updated node or error
435     */
436    @Override
437    public DataResult<ConfigurationNode> mergeToPrimitive(final ConfigurationNode prefix, final ConfigurationNode value) {
438        if (!prefix.empty()) {
439            return DataResult.error("Cannot merge " + value + " into non-empty node " + prefix);
440        }
441        return DataResult.success(guardOutputRead(value));
442    }
443
444    /**
445     * Appends element {@code value} to list node {@code input}.
446     *
447     * @param input base node. Must be empty or of list type
448     * @param value value to add as element to the list
449     * @return success with modified node, or error if {@code input} contains a
450     *          non-{@link ConfigurationNode#isList() list} value
451     */
452    @Override
453    public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final ConfigurationNode value) {
454        if (input.isList() || input.empty()) {
455            final ConfigurationNode ret = guardOutputRead(input);
456            ret.appendListNode().from(value);
457            return DataResult.success(ret);
458        }
459
460        return DataResult.error("mergeToList called on a node which is not a list: " + input, input);
461    }
462
463    /**
464     * Appends nodes in {@code values} to copy of list node {@code input}.
465     *
466     * @param input base node. Must be empty or of list type
467     * @param values list of values to append to base node
468     * @return success with modified node, or error if {@code input} contains a
469     *          non-{@link ConfigurationNode#isList() list} value
470     */
471    @Override
472    public DataResult<ConfigurationNode> mergeToList(final ConfigurationNode input, final List<ConfigurationNode> values) {
473        if (input.isList() || input.empty()) {
474            final ConfigurationNode ret = guardInputWrite(input);
475            for (ConfigurationNode node : values) {
476                ret.appendListNode().from(node);
477            }
478            return DataResult.success(ret);
479        }
480
481        return DataResult.error("mergeToList called on a node which is not a list: " + input, input);
482    }
483
484    /**
485     * Update the child of {@code input} at {@code key} with {@code value}.
486     *
487     * <p>This operation will only affect the returned copy of the input node
488     *
489     * @param input base node. Must be empty or of map type
490     * @param key key relative to base node
491     * @param value value to set at empty node
492     * @return success with modified node, or error if {@code input} contains a
493     *          non-{@link ConfigurationNode#isList() list} value
494     */
495    @Override
496    public DataResult<ConfigurationNode> mergeToMap(final ConfigurationNode input, final ConfigurationNode key, final ConfigurationNode value) {
497        if (input.isMap() || input.empty()) {
498            final ConfigurationNode copied = guardInputWrite(input);
499            copied.node(keyFrom(key)).from(value);
500            return DataResult.success(copied);
501        }
502
503        return DataResult.error("mergeToMap called on a node which is not a map: " + input, input);
504    }
505
506    /**
507     * Return a stream of pairs of (key, value) for map data in the input node.
508     *
509     * <p>If the input node is non-empty and not a map, the result will
510     * be a failure.
511     *
512     * @param input input node
513     * @return result, if successful, of a stream of pairs (key, value) of
514     *          entries in the input node.
515     */
516    @Override
517    public DataResult<Stream<Pair<ConfigurationNode, ConfigurationNode>>> getMapValues(final ConfigurationNode input) {
518        if (input.empty() || input.isMap()) {
519            return DataResult.success(input.childrenMap().entrySet().stream()
520                                              .map(entry -> Pair.of(BasicConfigurationNode.root(input.options()).raw(entry.getKey()),
521                                                                    guardOutputRead(entry.getValue()))));
522        }
523
524        return DataResult.error("Not a map: " + input);
525    }
526
527    /**
528     * Get a map-like view of a copy of the contents of {@code input}.
529     *
530     * <p>If the input node is non-empty and not a map, the result will
531     * be a failure.
532     *
533     * @param input input node
534     * @return result, if successful, of map-like view of a copy of the input
535     */
536    @Override
537    public DataResult<MapLike<ConfigurationNode>> getMap(final ConfigurationNode input) {
538        if (input.empty() || input.isMap()) {
539            return DataResult.success(new NodeMaplike(this, input.options(), input.childrenMap()));
540        } else {
541            return DataResult.error("Input node is not a map");
542        }
543    }
544
545    /**
546     * Get a consumer that takes an action to perform on every element of
547     * list node {@code input}.
548     *
549     * <p>As an example, to print out every node in a list
550     *      (minus error checking):
551     *     <pre>
552     *         getList(listNode).result().get()
553     *             .accept(element -&gt; System.out.println(element);
554     *     </pre>
555     *
556     * @param input data source
557     * @return result, that if successful will take an action to perform on
558     *          every element
559     */
560    @Override
561    public DataResult<Consumer<Consumer<ConfigurationNode>>> getList(final ConfigurationNode input) {
562        if (input.isList()) {
563            return DataResult.success(action -> {
564                for (ConfigurationNode child : input.childrenList()) {
565                    action.accept(guardOutputRead(child));
566                }
567            });
568        } else {
569            return DataResult.error("Input node is not a list");
570        }
571    }
572
573    /**
574     * Get the contents of list node {@code input} as a {@link Stream} of nodes.
575     *
576     * @param input data source
577     * @return if node is empty or a list, stream of nodes
578     */
579    @Override
580    public DataResult<Stream<ConfigurationNode>> getStream(final ConfigurationNode input) {
581        if (input.empty() || input.isList()) {
582            final Stream<ConfigurationNode> stream = input.childrenList().stream().map(this::guardOutputRead);
583            return DataResult.success(stream);
584        }
585
586        return DataResult.error("Not a list: " + input);
587    }
588
589    /**
590     * Create a new node containing the map entries from the
591     * stream {@code values}.
592     *
593     * <p>Keys will be interpreted as a single Object, and can only
594     * currently access direct children.
595     *
596     * @param values entries in the map
597     * @return newly created node
598     */
599    @Override
600    public ConfigurationNode createMap(final Stream<Pair<ConfigurationNode, ConfigurationNode>> values) {
601        final ConfigurationNode ret = empty();
602
603        values.forEach(p -> ret.node(keyFrom(p.getFirst())).from(p.getSecond()));
604
605        return ret;
606    }
607
608    /**
609     * Create a new node containing the map entries from the
610     * map {@code values}.
611     *
612     * <p>Keys will be interpreted as a single Object, and can only
613     * currently access direct children.
614     *
615     * @param values unwrapped node map
616     * @return newly created node
617     */
618    @Override
619    public ConfigurationNode createMap(final Map<ConfigurationNode, ConfigurationNode> values) {
620        final ConfigurationNode ret = empty();
621
622        for (Map.Entry<ConfigurationNode, ConfigurationNode> entry : values.entrySet()) {
623            ret.node(keyFrom(entry.getKey())).from(entry.getValue());
624        }
625
626        return ret;
627    }
628
629    /**
630     * Create a new node containing values emitted by {@code input} as
631     * list elements.
632     *
633     * @param input data source
634     * @return newly created node
635     */
636    @Override
637    public ConfigurationNode createList(final Stream<ConfigurationNode> input) {
638        final ConfigurationNode ret = empty();
639        input.forEach(it -> ret.appendListNode().from(it));
640        return ret;
641    }
642
643    /**
644     * Get a copy of {@code input} without the value at node {@code key}.
645     *
646     * <p>If the input node is not a map, the input node will be returned.
647     *
648     * @param input data source
649     * @param key key to the node to be removed
650     * @return if node removed, a copy of the input without node,
651     *          otherwise input
652     */
653    @Override
654    public ConfigurationNode remove(final ConfigurationNode input, final String key) {
655        if (input.isMap()) {
656            final ConfigurationNode ret = guardInputWrite(input);
657            ret.node(key).raw(null);
658            return ret;
659        }
660
661        return input;
662    }
663
664    /**
665     * Attempt to get the child of {@code input} at {@code key}.
666     *
667     * @param input data source
668     * @param key child key
669     * @return success containing child if child is non-virtual,
670     *          otherwise failure
671     */
672    @Override
673    public DataResult<ConfigurationNode> get(final ConfigurationNode input, final String key) {
674        final ConfigurationNode ret = input.node(key);
675        return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret));
676    }
677
678    /**
679     * Get a child of the provided node at {@code key}.
680     *
681     * <p>Keys will be interpreted as a single Object, and can only
682     * currently access direct children.
683     *
684     * @param input parent node
685     * @param key wrapped key of child
686     * @return success containing child if child is non-virtual,
687     *          otherwise failure
688     */
689    @Override
690    public DataResult<ConfigurationNode> getGeneric(final ConfigurationNode input, final ConfigurationNode key) {
691        final ConfigurationNode ret = input.node(keyFrom(key));
692        return ret.virtual() ? DataResult.error("No element " + key + " in the map " + input) : DataResult.success(guardOutputRead(ret));
693    }
694
695    /**
696     * Update a copy of {@code input} with {@code value} at path {@code key}.
697     *
698     * @param input data source
699     * @param key key of child node
700     * @param value value for child node
701     * @return updated parent node
702     */
703    @Override
704    public ConfigurationNode set(final ConfigurationNode input, final String key, final ConfigurationNode value) {
705        final ConfigurationNode ret = guardInputWrite(input);
706        ret.node(key).from(value);
707        return ret;
708    }
709
710    /**
711     * Copies the input node and transform its child at {@code key}.
712     *
713     * <p>Return a copy of the input node with the child at {@code key}
714     * transformed by the provided function
715     *
716     * <p>If there is no value at {@code key}, the input node will be
717     * returned unmodified.
718     *
719     * @param input base value
720     * @param key key to change
721     * @param function function to process the node at {@code wrappedKey}
722     * @return an updated copy of input node
723     */
724    @Override
725    public ConfigurationNode update(final ConfigurationNode input, final String key, final Function<ConfigurationNode, ConfigurationNode> function) {
726        if (input.node(key).virtual()) {
727            return input;
728        }
729
730        final ConfigurationNode ret = guardInputWrite(input);
731        final ConfigurationNode child = ret.node(key);
732        child.from(function.apply(child));
733        return ret;
734    }
735
736    /**
737     * Copies the input node and transform the node at {@code wrappedKey}.
738     *
739     * <p>Return a copy of the input node with the child at {@code wrappedKey}
740     * transformed by the provided function
741     *
742     * <p>If there is no value at {@code wrappedKey}, the input node will be
743     * returned unmodified.
744     *
745     * <p>Keys will be interpreted as a single Object, and can only
746     * currently access direct children.
747     *
748     * @param input base value
749     * @param wrappedKey key to change
750     * @param function function to process the node at {@code wrappedKey}
751     * @return an updated copy of input node
752     */
753    @Override
754    public ConfigurationNode updateGeneric(final ConfigurationNode input, final ConfigurationNode wrappedKey,
755            final Function<ConfigurationNode, ConfigurationNode> function) {
756        final Object key = keyFrom(wrappedKey);
757        if (input.node(key).virtual()) {
758            return input;
759        }
760
761        final ConfigurationNode ret = guardInputWrite(input);
762
763        final ConfigurationNode child = ret.node(key);
764        child.from(function.apply(child));
765        return ret;
766    }
767
768    @Override
769    public String toString() {
770        return "Configurate";
771    }
772
773    /**
774     * Protection level for configuration node accesses through ops instance.
775     *
776     * @since 4.0.0
777     */
778    public enum Protection {
779        /**
780         * When an operation is executed on the node, make a deep copy of the
781         * result.
782         */
783        COPY_DEEP,
784
785        /**
786         * Directly pass on nodes, still attached to their original structure.
787         */
788        NONE
789
790    }
791
792}