Skip to content

Commit

Permalink
⚡ some changes to data structures to support entries with the same key
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsTheSky committed Jan 3, 2025
1 parent 3afcd26 commit a3623f8
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package info.itsthesky.disky.api.datastruct;

import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.lang.parser.ParserInstance;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.skriptlang.skript.lang.entry.EntryContainer;
import org.skriptlang.skript.lang.entry.EntryData;
import org.skriptlang.skript.lang.entry.EntryValidator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* A "better" version of the {@link EntryContainer} which allows multiple node per entry key (basically, allows entry with the same key).
* This is useful for data structure where you can have multiple value for the same key.
* @author Sky & SkriptLang Team
* @since 1.0
* @see EntryContainer
* @see DataStructureFactory#validate(EntryValidator, SectionNode)
*/
public class BetterEntryContainer {

private final SectionNode source;
@Nullable
private final EntryValidator entryValidator;
@Nullable
private final Map<String, List<Node>> handledNodes;
private final List<Node> unhandledNodes;

public BetterEntryContainer(
SectionNode source,
@Nullable EntryValidator entryValidator,
@Nullable Map<String, List<Node>> handledNodes,
List<Node> unhandledNodes
) {
this.source = source;
this.entryValidator = entryValidator;
this.handledNodes = handledNodes;
this.unhandledNodes = unhandledNodes;
}

public SectionNode getSource() {
return source;
}

public List<Node> getUnhandledNodes() {
return unhandledNodes;
}

public List<Node> getNodesForEntry(String key) {
if (handledNodes == null)
return Collections.emptyList();
return handledNodes.getOrDefault(key, Collections.emptyList());
}

public <E, R extends E> List<R> getAllValues(String key, Class<E> expectedType, boolean useDefaultValue) {
List<R> values = new ArrayList<>();
for (Node node : getNodesForEntry(key)) {
R value = getValueFromNode(node, key, expectedType, useDefaultValue);
if (value != null) {
values.add(value);
}
}
return values;
}

public <E, R extends E> R get(String key, Class<E> expectedType, boolean useDefaultValue) {
R value = getOptional(key, expectedType, useDefaultValue);
if (value == null)
throw new RuntimeException("Null value for asserted non-null value");
return value;
}

public Object get(String key, boolean useDefaultValue) {
Object parsed = getOptional(key, useDefaultValue);
if (parsed == null)
throw new RuntimeException("Null value for asserted non-null value");
return parsed;
}

@Nullable
@SuppressWarnings("unchecked")
public <E, R extends E> R getOptional(String key, Class<E> expectedType, boolean useDefaultValue) {
Object parsed = getOptional(key, useDefaultValue);
if (parsed == null)
return null;
if (!expectedType.isInstance(parsed))
throw new RuntimeException("Expected entry with key '" + key + "' to be '" + expectedType + "', but got '" + parsed.getClass() + "'");
return (R) parsed;
}

@Nullable
public Object getOptional(String key, int index, boolean useDefaultValue) {
if (entryValidator == null || handledNodes == null)
return null;

EntryData<?> entryData = findEntryData(key);
if (entryData == null)
return null;

List<Node> nodes = handledNodes.get(key);
if (nodes == null || nodes.isEmpty())
return entryData.getDefaultValue();

if (index < 0 || index >= nodes.size())
return null;

Node node = nodes.get(index);
return getValueFromNode(node, key, Object.class, useDefaultValue);
}

@Nullable
private <E, R extends E> R getValueFromNode(Node node, String key, Class<E> expectedType, boolean useDefaultValue) {
if (entryValidator == null)
return null;

EntryData<?> entryData = findEntryData(key);
if (entryData == null)
return null;

ParserInstance parser = ParserInstance.get();
Node oldNode = parser.getNode();
parser.setNode(node);
Object value = entryData.getValue(node);
if (value == null && useDefaultValue)
value = entryData.getDefaultValue();
parser.setNode(oldNode);

if (value == null || !expectedType.isInstance(value))
return null;

@SuppressWarnings("unchecked")
R result = (R) value;
return result;
}

@Nullable
private EntryData<?> findEntryData(String key) {
if (entryValidator == null)
return null;

for (EntryData<?> data : entryValidator.getEntryData()) {
if (data.getKey().equals(key)) {
return data;
}
}
return null;
}

public boolean hasEntry(@NotNull String key) {
return handledNodes != null && handledNodes.containsKey(key) && !handledNodes.get(key).isEmpty();
}

public int getEntryCount(@NotNull String key) {
if (handledNodes == null)
return 0;
List<Node> nodes = handledNodes.get(key);
return nodes == null ? 0 : nodes.size();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@
*/
Class<?> clazz();

/**
* Whether this structure can be made from a {@link info.itsthesky.disky.elements.sections.CreateStructSection create structure section}.
* A data structure may only serve as "sub-data structure" for other structures (like {@link info.itsthesky.disky.api.datastruct.structures.EmbedFieldStructure} for {@link info.itsthesky.disky.api.datastruct.structures.EmbedStructure}).
*/
boolean canBeCreated() default true;

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@
boolean optional() default false;

/**
* In case of an array/list of object,
* the key (usually singular) used to represent a new instance of that object.
* @return The key used to represent a new instance of that object
* The minimum amount of this entry, in case it's
* an array or a list.
* @return The minimum amount of this entry
*/
String singleKey() default "";
int minimum() default 0;

/**
* The maximum amount of this entry, in case it's
* an array or a list.
* @return The maximum amount of this entry
*/
int maximum() default 0;

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package info.itsthesky.disky.api.datastruct;

import ch.njol.skript.ScriptLoader;
import ch.njol.skript.Skript;
import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.lang.util.SimpleLiteral;
import info.itsthesky.disky.DiSky;
import info.itsthesky.disky.api.ReflectionUtils;
import info.itsthesky.disky.api.datastruct.base.DataStruct;
import info.itsthesky.disky.api.skript.BetterExpressionEntryData;
import org.jetbrains.annotations.Nullable;
import org.skriptlang.skript.lang.entry.EntryContainer;
import org.skriptlang.skript.lang.entry.EntryData;
import org.skriptlang.skript.lang.entry.EntryValidator;
import org.skriptlang.skript.lang.entry.util.ExpressionEntryData;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

public final class DataStructureFactory {

Expand All @@ -32,10 +42,11 @@ public record DataStructureInfo(Class<?> structureClazz, Class<?> returnedClazz,
String name,
List<DataStructureEntryInfo> entries,
EntryValidator validator) {};
public record DataStructureEntryInfo(String name, String fieldName,
Class<?> returnType,
boolean optional,
@Nullable Object defaultValue) {};
public record DataStructureEntryInfo(DataStructureEntry entry,
String fieldName,
Class<?> returnType,
boolean array,
@Nullable Object defaultValue) {};

private List<DataStructureInfo> registeredStructures;

Expand Down Expand Up @@ -63,15 +74,29 @@ public void registerDataStructure(Class<?> clazz) {
}

DataStructureEntry entry = field.getAnnotation(DataStructureEntry.class);
entries.add(new DataStructureEntryInfo(entry.value(), field.getName(), field.getType(),
entry.optional(), defaultValue));
entries.add(new DataStructureEntryInfo(entry, field.getName(),
field.getType(), field.getType().isArray() || List.class.isAssignableFrom(field.getType()), defaultValue));
}

var entryValidatorBuilder = EntryValidator.builder();
for (var entry : entries) {
// TODO: Handle lists/arrays
entryValidatorBuilder.addEntryData(new ExpressionEntryData<Object>(entry.name(),
null, entry.optional(), (Class) entry.returnType));

// FIRST CASE: it's not an array, and requires a data structure
if (!entry.array() && DataStruct.class.isAssignableFrom(entry.returnType())) { {

entryValidatorBuilder.addEntryData(new ExpressionEntryData<Object>(entry.entry().value(),
null, entry.entry().optional(), (Class) entry.returnType));

}}

// SECOND CASE: it's an array, and requires a data structure
else if (entry.array() && DataStruct.class.isAssignableFrom(entry.returnType())) {

entryValidatorBuilder.addEntryData(new BetterExpressionEntryData<Object>(entry.entry().value(),
null, entry.entry().optional(), (Class) entry.returnType));

}

}

var structInfo = new DataStructureInfo(clazz, annotation.clazz(), annotation.value(), entries, entryValidatorBuilder.build());
Expand All @@ -80,4 +105,57 @@ public void registerDataStructure(Class<?> clazz) {
DiSky.debug("Registered data structure " + annotation.value() + " with " + entries.size() + " entries.");
}

@Nullable
public static BetterEntryContainer validate(EntryValidator entryValidator, SectionNode sectionNode) {
List<EntryData<?>> entries = new ArrayList<>(entryValidator.getEntryData());
Map<String, List<Node>> handledNodes = new HashMap<>();
List<Node> unhandledNodes = new ArrayList<>();

var unexpectedNodeTester = (Predicate<Node>) ReflectionUtils.getFieldValueViaInstance(entryValidator, "unexpectedNodeTester");
var unexpectedEntryMessage = (Function<String, String>) ReflectionUtils.getFieldValueViaInstance(entryValidator, "unexpectedEntryMessage");
var missingRequiredEntryMessage = (Function<String, String>) ReflectionUtils.getFieldValueViaInstance(entryValidator, "missingRequiredEntryMessage");

boolean ok = true;
nodeLoop: for (Node node : sectionNode) {
if (node.getKey() == null)
continue;

// Le premier pas est de déterminer si le node est présent dans la liste entryData
boolean foundMatch = false;
for (EntryData<?> data : entries) {
if (data.canCreateWith(node)) {
// C'est un node connu, on l'ajoute à la liste correspondante
handledNodes.computeIfAbsent(data.getKey(), k -> new ArrayList<>()).add(node);
foundMatch = true;
// Ne pas retirer l'EntryData car on peut avoir plusieurs nodes correspondants
continue nodeLoop;
}
}

// Aucun EntryData correspondant trouvé
if (!foundMatch) {
if (unexpectedNodeTester == null || unexpectedNodeTester.test(node)) {
ok = false;
Skript.error(unexpectedEntryMessage.apply(ScriptLoader.replaceOptions(node.getKey())));
} else {
unhandledNodes.add(node);
}
}
}

// Vérification des entrées requises
for (EntryData<?> entryData : entries) {
List<Node> matchingNodes = handledNodes.getOrDefault(entryData.getKey(), Collections.emptyList());
if (!entryData.isOptional() && matchingNodes.isEmpty()) {
Skript.error(missingRequiredEntryMessage.apply(entryData.getKey()));
ok = false;
}
}

if (!ok)
return null;

return new BetterEntryContainer(sectionNode, entryValidator, handledNodes, unhandledNodes);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import info.itsthesky.disky.api.datastruct.base.DataStruct;
import info.itsthesky.disky.core.SkriptUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;

import java.time.Instant;
import java.util.List;

@DataStructure(value = "embed", clazz = EmbedBuilder.class)
public class EmbedStructure implements DataStruct<EmbedBuilder> {
Expand Down Expand Up @@ -45,6 +47,10 @@ public class EmbedStructure implements DataStruct<EmbedBuilder> {
@DataStructureEntry(value = "color", optional = true)
public Color color = SkriptColor.YELLOW;

@DataStructureEntry(value = "field", optional = true,
maximum = MessageEmbed.MAX_FIELD_AMOUNT)
public List<EmbedFieldStructure> fields;

@Override
public EmbedBuilder build() {
EmbedBuilder builder = new EmbedBuilder();
Expand Down Expand Up @@ -81,6 +87,12 @@ public EmbedBuilder build() {

if (timestamp != null) builder.setTimestamp(Instant.ofEpochMilli(timestamp.getTimestamp()));

if (fields != null) {
for (EmbedFieldStructure field : fields) {
builder.addField(field.build());
}
}

return builder;
}

Expand Down
Loading

0 comments on commit a3623f8

Please sign in to comment.