Skip to content

Commit

Permalink
Add "defaulting" ConfigNode and ConfigLists
Browse files Browse the repository at this point in the history
 * Instances can be obtained by calling ConfigNode#defaulting and ConfigList#defaulting, respectively

 * "defaulting" nodes/lists will delegate to a secondary "defaults" node or list if a value is not present

 * Unit tests

 * Bump version
  • Loading branch information
Steanky committed Mar 10, 2024
1 parent ad668ad commit 36c984e
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group 'com.github.steanky'
version '0.23.2'
version '0.24.0'

publishing {
publications {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ public interface ConfigList extends ConfigElement, List<ConfigElement>, ConfigCo
*/
ConfigList EMPTY = ConfigContainers.EmptyImmutableConfigList.INSTANCE;

/**
* Returns a new "defaulting" ConfigList. If a value at a certain index is not present in {@code base},
* {@code defaults} will be used instead.
* <p>
* The returned node is immutable, but read-through to both {@code base} and {@code defaults}.
*
* @param base the base list
* @param defaults the default list
* @return a new ConfigList
*/
static @NotNull ConfigList defaulting(@NotNull ConfigList base, @NotNull ConfigList defaults) {
Objects.requireNonNull(base);
Objects.requireNonNull(defaults);

return new DefaultingConfigList(base, defaults);
}

/**
* Similarly to {@link ConfigNode#of(Object...)}, builds a new {@link ArrayConfigList} from the given object array.
* Objects that are instances of {@link ConfigElement} will be added to the resulting list directly, whereas objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ public interface ConfigNode extends ConfigElement, Map<String, ConfigElement>, C
*/
ConfigNode EMPTY = ConfigContainers.EmptyImmutableConfigNode.INSTANCE;

/**
* Returns a new "defaulting" ConfigNode. If a value is not found in {@code base}, {@code defaults} will be queried
* instead.
* <p>
* The returned node is immutable, but read-through to both {@code base} and {@code defaults}.
*
* @param base the base node
* @param defaults the default node
* @return a new ConfigNode
*/
static @NotNull ConfigNode defaulting(@NotNull ConfigNode base, @NotNull ConfigNode defaults) {
Objects.requireNonNull(base);
Objects.requireNonNull(defaults);

return new DefaultingConfigNode(base, defaults);
}

/**
* Overload of {@link ConfigNode#of(Object...)}. Returns a new, empty {@link LinkedConfigNode}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.github.steanky.ethylene.core.collection;

import com.github.steanky.ethylene.core.ConfigElement;
import com.github.steanky.toolkit.collection.Containers;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.RandomAccess;

/**
* An implementation of a "defaulting" ConfigList.
*
* @see ConfigList#defaulting(ConfigList, ConfigList)
*/
class DefaultingConfigList extends AbstractConfigList implements Immutable, RandomAccess {
private final ConfigList base;
private final ConfigList defaults;

/**
* Creates a new defaulting ConfigList.
*
* @param base the base list
* @param defaults the default list, only queried if there is no value at a given index in {@code base}
*/
DefaultingConfigList(@NotNull ConfigList base, @NotNull ConfigList defaults) {
this.base = base;
this.defaults = defaults;
}

@Override
public @UnmodifiableView @NotNull Collection<ConfigEntry> entryCollection() {
return Containers.mappedView(ConfigEntry::of, elementCollection());
}

@Override
public @UnmodifiableView @NotNull Collection<ConfigElement> elementCollection() {
return new AbstractCollection<>() {
@Override
public @NotNull Iterator<ConfigElement> iterator() {
int baseSize = base.size();
int max = DefaultingConfigList.this.size();

return new Iterator<>() {
private int i = 0;

@Override
public boolean hasNext() {
return i < max;
}

@Override
public ConfigElement next() {
int i = this.i++;
if (i < baseSize) {
return base.get(i);
}

return defaults.get(i);
}
};
}

@Override
public int size() {
return DefaultingConfigList.this.size();
}
};
}

@Override
public ConfigElement get(int index) {
if (index < base.size()) {
return base.get(index);
}

return defaults.get(index);
}

@Override
public int size() {
return Math.max(base.size(), defaults.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.github.steanky.ethylene.core.collection;

import com.github.steanky.ethylene.core.ConfigElement;
import com.github.steanky.toolkit.collection.Containers;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;

import java.util.*;

/**
* An implementation of a "defaulting" ConfigNode.
*
* @see ConfigNode#defaulting(ConfigNode, ConfigNode)
*/
class DefaultingConfigNode extends AbstractConfigNode implements Immutable {
private final ConfigNode base;
private final ConfigNode defaults;

/**
* Constructs a new defaulting ConfigNode.
*
* @param base the base node, which is queried first
* @param defaults the default node, which is only queried if a requested value is not present in {@code base}
*/
DefaultingConfigNode(@NotNull ConfigNode base, @NotNull ConfigNode defaults) {
this.base = base;
this.defaults = defaults;
}

@Override
public @UnmodifiableView @NotNull Collection<ConfigEntry> entryCollection() {
return Containers.mappedView(entry -> (ConfigEntry)entry, entrySet());
}

@Override
public @UnmodifiableView @NotNull Collection<ConfigElement> elementCollection() {
return Containers.mappedView(Entry::getValue, entrySet());
}

@NotNull
@Override
public Set<Entry<String, ConfigElement>> entrySet() {
return new AbstractSet<>() {
@Override
public @NotNull Iterator<Map.Entry<String, ConfigElement>> iterator() {
return new Iterator<>() {
private final Iterator<ConfigEntry> baseIterator = base.entryCollection().iterator();
private final Iterator<ConfigEntry> defaultsIterator = defaults.entryCollection().iterator();

private ConfigEntry nextDefault;

@Override
public boolean hasNext() {
return baseIterator.hasNext() || tryAdvanceDefaults();
}

@Override
public ConfigEntry next() {
if (baseIterator.hasNext()) {
return baseIterator.next();
}

ConfigEntry nextDefault = this.nextDefault;
if (nextDefault != null) {
this.nextDefault = null;
return nextDefault;
}

if (tryAdvanceDefaults()) {
nextDefault = this.nextDefault;
this.nextDefault = null;
return nextDefault;
}

throw new NoSuchElementException();
}

private boolean tryAdvanceDefaults() {
while (defaultsIterator.hasNext()) {
ConfigEntry nextDefault = defaultsIterator.next();

if (!base.containsKey(nextDefault.getKey())) {
this.nextDefault = nextDefault;
return true;
}
}

return false;
}
};
}

@Override
public int size() {
return DefaultingConfigNode.this.size();
}

@Override
public boolean contains(Object o) {
return base.entrySet().contains(o) || defaults.entrySet().contains(o);
}
};
}

@Override
public ConfigElement get(Object key) {
ConfigElement element = base.get(key);
if (element != null) {
return element;
}

return defaults.get(key);
}

@Override
public boolean containsKey(Object key) {
return base.containsKey(key) || defaults.containsKey(key);
}

@Override
public int size() {
int size = base.size();
for (String key : defaults.keySet()) {
if (!base.containsKey(key)) {
size++;
}
}

return size;
}

@Override
public boolean isEmpty() {
return base.isEmpty() && defaults.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.steanky.ethylene.core.collection;

import com.github.steanky.ethylene.core.ConfigPrimitive;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class DefaultingConfigListTest {
@Test
void emptyBase() {
ConfigList base = ConfigList.of();
ConfigList defaults = ConfigList.of("test");

DefaultingConfigList defaultingConfigList = new DefaultingConfigList(base, defaults);

assertEquals(ConfigPrimitive.of("test"), defaultingConfigList.get(0));
assertEquals(1, defaultingConfigList.size());

assertThrows(IndexOutOfBoundsException.class, () -> defaultingConfigList.get(1));

base.addString("test2");

assertEquals(ConfigPrimitive.of("test2"), defaultingConfigList.get(0));
}

@Test
void itr() {
ConfigList base = ConfigList.of("a", "b", "c");
ConfigList defaults = ConfigList.of("0", "1", "2", "d", "e", "f");

DefaultingConfigList defaultingConfigList = new DefaultingConfigList(base, defaults);
List<String> values = new ArrayList<>(6);
for (ConfigEntry entry : defaultingConfigList.entryCollection()) {
values.add(entry.getValue().asString());
}

assertEquals(List.of("a", "b", "c", "d", "e", "f"), values);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.github.steanky.ethylene.core.collection;

import com.github.steanky.ethylene.core.ConfigElement;
import com.github.steanky.ethylene.core.ConfigPrimitive;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

class DefaultingConfigNodeTest {
@Test
void simple() {
ConfigNode base = ConfigNode.of("key", "value");
ConfigNode defaults = ConfigNode.of("default", 69);

DefaultingConfigNode defaultingConfigNode = new DefaultingConfigNode(base, defaults);

assertEquals(ConfigPrimitive.of(69), defaultingConfigNode.get("default"));
assertEquals(2, defaultingConfigNode.size());

base.putNumber("default", 420);

assertEquals(ConfigPrimitive.of(420), defaultingConfigNode.get("default"));

Set<Map.Entry<String, ConfigElement>> entrySet = new HashSet<>();
entrySet.addAll(defaultingConfigNode.entrySet());

assertEquals(Set.of(Map.entry("key", ConfigPrimitive.of("value")),
Map.entry("default", ConfigPrimitive.of(420))), entrySet);
}
}

0 comments on commit 36c984e

Please sign in to comment.