Skip to content

Commit

Permalink
port 1.19 changes
Browse files Browse the repository at this point in the history
- improve duplicate checks for implicit counts of 1 (#28)
- reorganize json compare tests
- fixes #27
  • Loading branch information
rlnt committed Dec 5, 2022
1 parent e8f8e02 commit eb1933c
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 151 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog],
and this project adheres to [Semantic Versioning].

## Unreleased

### Changed
- improved duplicate checks for recipes with implicit counts of 1

## [0.3.1] - 2022-12-01

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public static RecipeLink compare(RecipeLink first, RecipeLink second, JsonCompar

JsonObject compare = null;
if (first.getType().toString().equals("minecraft:crafting_shaped")) {
compare = JsonCompare.compareShaped(selfActual, toCompareActual, compareSettings.getIgnoredFields());
} else if (JsonCompare.matches(selfActual, toCompareActual, compareSettings.getIgnoredFields())) {
compare = JsonCompare.compareShaped(selfActual, toCompareActual, compareSettings);
} else if (JsonCompare.matches(selfActual, toCompareActual, compareSettings)) {
compare = JsonCompare.compare(compareSettings.getRules(), selfActual, toCompareActual);
}

Expand Down
136 changes: 130 additions & 6 deletions Common/src/main/java/com/almostreliable/unified/utils/JsonCompare.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

public final class JsonCompare {

private static final Set<String> SANITIZE_KEYS = Set.of("item", "tag", "id");

private JsonCompare() {}

public static int compare(JsonObject first, JsonObject second, Map<String, Rule> rules) {
Expand All @@ -36,8 +38,8 @@ public static JsonObject compare(Map<String, Rule> rules, JsonObject... jsonObje
}

@Nullable
public static JsonObject compareShaped(JsonObject first, JsonObject second, Collection<String> ignoredFields) {
if (!matches(first, second, ignoredFields)) return null;
public static JsonObject compareShaped(JsonObject first, JsonObject second, CompareSettings compareSettings) {
if (!matches(first, second, compareSettings)) return null;

JsonArray firstPattern = JsonUtils.arrayOrSelf(first.get("pattern"));
JsonArray secondPattern = JsonUtils.arrayOrSelf(second.get("pattern"));
Expand Down Expand Up @@ -86,29 +88,141 @@ private static Map<Character, JsonObject> createShapedKeyMap(JsonObject json) {
return keyMap;
}

public static boolean matches(JsonObject first, JsonObject second, Collection<String> ignoredProperties) {
public static boolean matches(JsonObject first, JsonObject second, CompareSettings compareSettings) {
Collection<String> ignoredFields = compareSettings.getIgnoredFields();
List<String> firstValidKeys = first
.keySet()
.stream()
.filter(key -> !ignoredProperties.contains(key))
.filter(key -> !ignoredFields.contains(key))
.toList();
List<String> secondValidKeys = second
.keySet()
.stream()
.filter(key -> !ignoredProperties.contains(key))
.filter(key -> !ignoredFields.contains(key))
.toList();

if (firstValidKeys.size() != secondValidKeys.size()) return false;

for (String firstKey : firstValidKeys) {
if (!first.get(firstKey).equals(second.get(firstKey))) {
JsonElement firstElem = first.get(firstKey);
JsonElement secondElem = second.get(firstKey);

// the second element can still be null although the valid keys have the same size
if (secondElem == null) return false;

// sanitize elements for implicit counts of 1
if (compareSettings.shouldSanitize && needsSanitizing(firstElem, secondElem)) {
firstElem = sanitize(firstElem);
secondElem = sanitize(secondElem);
}

if (!firstElem.equals(secondElem)) {
return false;
}
}

return true;
}

/**
* A check whether the given elements need to be sanitized. The purpose of this check is
* to save performance by skipping pairs that are not affected by sanitizing.
* <p>
* Conditions are both elements being a JSON array with the same size, both elements being
* a JSON object, one element being a JSON object and the other being a JSON primitive.
* @param firstElem the first element
* @param secondElem the second element
* @return true if the elements need to be sanitized, false otherwise
*/
private static boolean needsSanitizing(JsonElement firstElem, JsonElement secondElem) {
return (firstElem instanceof JsonArray firstArray && secondElem instanceof JsonArray secondArray &&
firstArray.size() == secondArray.size()) ||
(firstElem instanceof JsonObject && secondElem instanceof JsonObject) ||
(firstElem instanceof JsonPrimitive && secondElem instanceof JsonObject) ||
(firstElem instanceof JsonObject && secondElem instanceof JsonPrimitive);
}

/**
* Creates a sanitized object from the given element with a count of 1 and the
* value from the original object under a dummy key called "au_sanitized".
* <p>
* If the element is not a string primitive, the default object is returned.
* @param value The value to sanitize
* @param defaultValue The default value to return if the element is not a string primitive
* @return The sanitized object or the default value
*/
private static JsonElement createSanitizedObjectOrDefault(JsonElement value, JsonElement defaultValue) {
if (value instanceof JsonPrimitive primitive && primitive.isString()) {
var newObject = new JsonObject();
newObject.addProperty("au_sanitized", primitive.getAsString());
newObject.addProperty("count", 1);
return newObject;
}
return defaultValue;
}

/**
* Used to sanitize root level JSON elements to make them comparable when the count of 1 is implicit.
* <p>
* This transforms string primitives, JSON objects and JSON arrays to JSON objects where the
* count of 1 is explicitly set. The transformation is only applied to a dummy object and not to
* the original recipe, so it can be safely used for comparison.
* <p>
* If the object doesn't support this transformation, the original object is returned.
* @param element The element to sanitize
* @return The sanitized element or the original element if it can't be sanitized
*/
@SuppressWarnings("ChainOfInstanceofChecks")
private static JsonElement sanitize(JsonElement element) {
if (element instanceof JsonArray jsonArray) {
JsonArray newArray = new JsonArray();
for (JsonElement arrayElement : jsonArray) {
newArray.add(sanitize(arrayElement));
}
return newArray;
}

if (element instanceof JsonObject jsonObject) {
var keySet = jsonObject.keySet();

if (keySet.stream().filter(SANITIZE_KEYS::contains).count() != 1) {
return element;
}

// if it has a count property, it needs to be 1, otherwise it's implicit 1 as well and needs sanitizing
if (keySet.contains("count") && JsonQuery.of(jsonObject, "count").asInt().filter(i -> i == 1).isEmpty()) {
return element;
}

var key = keySet.stream().filter(SANITIZE_KEYS::contains).findFirst().orElseThrow();
var sanitized = createSanitizedObjectOrDefault(jsonObject.get(key), jsonObject);

// ensure the object changed (was sanitized) and that we got a JsonObject
//noinspection ObjectEquality
if (sanitized == jsonObject || !(sanitized instanceof JsonObject sanitizedObject)) {
return jsonObject;
}

mergeRemainingProperties(jsonObject, sanitizedObject);
return sanitizedObject;
}

return createSanitizedObjectOrDefault(element, element);
}

/**
* Merges remaining properties from the original object to the sanitized object.
* @param jsonObject The original object
* @param sanitizedObject The sanitized object
*/
private static void mergeRemainingProperties(JsonObject jsonObject, JsonObject sanitizedObject) {
for (var entry : jsonObject.entrySet()) {
if (!SANITIZE_KEYS.contains(entry.getKey()) && !entry.getKey().equals("count")) {
sanitizedObject.add(entry.getKey(), entry.getValue());
}
}
}

public interface Rule {
/**
* Compare two JsonElements. The caller must ensure that at least one element is not null.
Expand Down Expand Up @@ -154,9 +268,11 @@ public String getName() {
public static class CompareSettings {
public static final String IGNORED_FIELDS = "ignoredFields";
public static final String RULES = "rules";
public static final String SHOULD_SANITIZE = "shouldSanitize";

private final LinkedHashMap<String, Rule> rules = new LinkedHashMap<>();
private final Set<String> ignoredFields = new HashSet<>();
private boolean shouldSanitize;

public void ignoreField(String property) {
ignoredFields.add(property);
Expand All @@ -170,6 +286,10 @@ public void addRule(String key, Rule rule) {
}
}

public void setShouldSanitize(boolean shouldSanitize) {
this.shouldSanitize = shouldSanitize;
}

public Set<String> getIgnoredFields() {
return Collections.unmodifiableSet(ignoredFields);
}
Expand All @@ -187,6 +307,8 @@ public JsonObject serialize() {
});
result.add(RULES, rulesJson);

result.addProperty(SHOULD_SANITIZE, shouldSanitize);

return result;
}

Expand All @@ -203,6 +325,8 @@ public void deserialize(JsonObject json) {
};
addRule(e.getKey(), r);
});

shouldSanitize = json.getAsJsonPrimitive(SHOULD_SANITIZE).getAsBoolean();
}

public Map<String, Rule> getRules() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -79,4 +80,25 @@ public Optional<JsonArray> asArray() {
public Optional<String> asString() {
return asElement().filter(JsonElement::isJsonPrimitive).map(JsonElement::getAsString);
}

public Optional<Integer> asInt() {
return asElement().filter(JsonElement::isJsonPrimitive).map(JsonElement::getAsJsonPrimitive)
.filter(JsonPrimitive::isNumber).map(JsonElement::getAsInt);
}

public JsonQuery shallowCopy() {
if (element == null) {
return new JsonQuery();
}

if (element instanceof JsonObject jsonObject) {
var copyObject = new JsonObject();
for (var entry : jsonObject.entrySet()) {
copyObject.add(entry.getKey(), entry.getValue());
}
return new JsonQuery(copyObject);
}

throw new UnsupportedOperationException();
}
}
Loading

0 comments on commit eb1933c

Please sign in to comment.