From b40131b31f9fc07e4e71ec2a9ae9338d3aeed0cf Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Thu, 16 Jul 2026 21:54:31 +0200 Subject: [PATCH] Add PluginConflict --- .../plugin/metadata/PluginMetadata.java | 19 +++--- .../metadata/builtin/InheritableMetadata.java | 33 +++++++++- .../metadata/builtin/MetadataParser.java | 1 + .../builtin/StandardPluginMetadata.java | 9 +++ .../adapter/InheritableMetadataAdapter.java | 5 ++ .../adapter/model/PluginConflictAdapter.java | 56 +++++++++++++++++ .../plugin/metadata/model/PluginConflict.java | 62 +++++++++++++++++++ .../metadata/model/PluginDependency.java | 10 +-- .../metadata/builtin/MetadataParserTest.java | 2 + src/test/resources/legacy/entrypoint.json | 8 +++ src/test/resources/valid/full_global.json | 8 +++ src/test/resources/valid/full_override.json | 8 +++ .../resources/valid/main_entrypoint_only.json | 8 +++ src/test/resources/valid/mix.json | 8 +++ src/test/resources/valid/mix_in_root.json | 8 +++ 15 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginConflictAdapter.java create mode 100644 src/main/java/org/spongepowered/plugin/metadata/model/PluginConflict.java diff --git a/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java index ea6d4e0..b6082ba 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java +++ b/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java @@ -26,6 +26,7 @@ import org.apache.maven.artifact.versioning.ArtifactVersion; import org.spongepowered.plugin.metadata.model.PluginBranding; +import org.spongepowered.plugin.metadata.model.PluginConflict; import org.spongepowered.plugin.metadata.model.PluginContributor; import org.spongepowered.plugin.metadata.model.PluginDependency; import org.spongepowered.plugin.metadata.model.PluginEntrypoints; @@ -47,14 +48,7 @@ public interface PluginMetadata { /** * Gets the {@link String id}. * - *

Ids must conform to the following requirements:

- * - * + * @see Constants#VALID_ID_PATTERN * @return The id */ String id(); @@ -108,6 +102,15 @@ public interface PluginMetadata { */ List contributors(); + /** + * Gets the known and declared {@link PluginConflict conflicts}. + * This list is not exhaustive. Unknown conflicts may not be in this list. + * This list may contain multiple entries with the same id but with different version ranges and reasons. + * + * @return The {@link PluginConflict conflicts} as an unmodifiable {@link List}. + */ + List conflicts(); + /** * Gets the {@link PluginDependency plugin dependency} by {@link String id}. *

diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java index 385f0bd..c70ef47 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java +++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java @@ -27,6 +27,7 @@ import org.apache.maven.artifact.versioning.ArtifactVersion; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.plugin.metadata.model.PluginBranding; +import org.spongepowered.plugin.metadata.model.PluginConflict; import org.spongepowered.plugin.metadata.model.PluginContributor; import org.spongepowered.plugin.metadata.model.PluginDependency; import org.spongepowered.plugin.metadata.model.PluginLinks; @@ -53,6 +54,7 @@ public final class InheritableMetadata { private final PluginBranding branding; private final PluginLinks links; private final List contributors; + private final List conflicts; private final Map dependencies; private final Map properties; @@ -65,6 +67,7 @@ private InheritableMetadata() { this.branding = PluginBranding.none(); this.links = PluginLinks.none(); this.contributors = List.of(); + this.conflicts = List.of(); this.dependencies = Map.of(); this.properties = Map.of(); } @@ -78,6 +81,7 @@ private InheritableMetadata(final Builder builder) { this.branding = builder.branding; this.links = builder.links; this.contributors = List.copyOf(builder.contributors); + this.conflicts = List.copyOf(builder.conflicts); this.dependencies = Collections.unmodifiableMap(new LinkedHashMap<>(builder.dependencies)); this.properties = Collections.unmodifiableMap(new LinkedHashMap<>(builder.properties)); } @@ -114,6 +118,10 @@ public List contributors() { return this.contributors; } + public List conflicts() { + return this.conflicts; + } + public Map dependencies() { return this.dependencies; } @@ -144,6 +152,7 @@ public boolean equals(Object o) { && this.branding.equals(other.branding) && this.links.equals(other.links) && this.contributors.equals(other.contributors) + && this.conflicts.equals(other.conflicts) && this.dependencies.equals(other.dependencies) && this.properties.equals(other.properties); } @@ -151,7 +160,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(this.version, this.loader, this.name, this.description, this.license, - this.branding, this.links, this.contributors, this.dependencies, this.properties); + this.branding, this.links, this.contributors, this.conflicts, this.dependencies, this.properties); } @Override @@ -163,6 +172,7 @@ public String toString() { .add("branding=" + this.branding) .add("links=" + this.links) .add("contributors=" + this.contributors) + .add("conflicts=" + this.conflicts) .add("dependencies=" + this.dependencies) .add("properties=" + this.properties) .toString(); @@ -189,6 +199,7 @@ public static final class Builder { private PluginBranding branding = PluginBranding.none(); private PluginLinks links = PluginLinks.none(); private final List contributors = new LinkedList<>(); + private final List conflicts = new LinkedList<>(); private final Map dependencies = new LinkedHashMap<>(); private final Map properties = new LinkedHashMap<>(); @@ -247,6 +258,23 @@ public Builder addContributor(final PluginContributor contributor) { return this; } + public Builder conflicts(final Collection conflicts) { + Objects.requireNonNull(conflicts, "conflicts"); + this.conflicts.clear(); + this.conflicts.addAll(conflicts); + return this; + } + + public Builder addConflicts(final Collection conflicts) { + this.conflicts.addAll(Objects.requireNonNull(conflicts, "conflicts")); + return this; + } + + public Builder addConflict(final PluginConflict conflict) { + this.conflicts.add(Objects.requireNonNull(conflict, "conflict")); + return this; + } + public Builder dependencies(final Collection dependencies) { Objects.requireNonNull(dependencies, "dependencies"); this.dependencies.clear(); @@ -293,6 +321,8 @@ public Builder from(final InheritableMetadata value) { this.links = value.links; this.contributors.clear(); this.contributors.addAll(value.contributors); + this.conflicts.clear(); + this.conflicts.addAll(value.conflicts); this.dependencies.clear(); this.dependencies.putAll(value.dependencies); this.properties.clear(); @@ -323,6 +353,7 @@ public Builder with(final InheritableMetadata override) { this.links = override.links; } this.contributors.addAll(override.contributors); + this.conflicts.addAll(override.conflicts); this.dependencies.putAll(override.dependencies); this.properties.putAll(override.properties); return this; diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java index e420740..97943e2 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java +++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java @@ -54,6 +54,7 @@ public final class MetadataParser { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(PluginLoaderSpecification.class, new PluginLoaderSpecificationAdapter()) .registerTypeAdapter(PluginBranding.class, new PluginBrandingAdapter()) + .registerTypeAdapter(PluginConflict.class, new PluginConflictAdapter()) .registerTypeAdapter(PluginContributor.class, new PluginContributorAdapter()) .registerTypeAdapter(PluginDependency.class, new PluginDependencyAdapter()) .registerTypeAdapter(PluginEntrypoints.class, new PluginEntrypointsAdapter()) diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java index 679f3dd..e7c7314 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java +++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java @@ -30,6 +30,7 @@ import org.spongepowered.plugin.metadata.Constants; import org.spongepowered.plugin.metadata.PluginMetadata; import org.spongepowered.plugin.metadata.model.PluginBranding; +import org.spongepowered.plugin.metadata.model.PluginConflict; import org.spongepowered.plugin.metadata.model.PluginContributor; import org.spongepowered.plugin.metadata.model.PluginDependency; import org.spongepowered.plugin.metadata.model.PluginEntrypoints; @@ -57,6 +58,7 @@ public final class StandardPluginMetadata implements PluginMetadata { private final PluginBranding branding; private final PluginLinks links; private final List contributors; + private final List conflicts; private final Map dependencies; private final Map properties; @@ -74,6 +76,7 @@ private StandardPluginMetadata(final Builder builder) { this.branding = metadata.branding(); this.links = metadata.links(); this.contributors = metadata.contributors(); + this.conflicts = metadata.conflicts(); this.dependencies = metadata.dependencies(); this.properties = metadata.properties(); } @@ -136,6 +139,11 @@ public List contributors() { return this.contributors; } + @Override + public List conflicts() { + return this.conflicts; + } + @Override public Optional dependency(String id) { return Optional.ofNullable(this.dependencies.get(Objects.requireNonNull(id, "id"))); @@ -187,6 +195,7 @@ public String toString() { .add("branding=" + this.branding) .add("links=" + this.links) .add("contributors=" + this.contributors) + .add("conflicts=" + this.conflicts) .add("dependencies=" + this.dependencies) .add("properties=" + this.properties) .toString(); diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java index ddd4c9e..1c5a325 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java +++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java @@ -29,6 +29,7 @@ import org.spongepowered.plugin.metadata.builtin.InheritableMetadata; import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils; import org.spongepowered.plugin.metadata.model.PluginBranding; +import org.spongepowered.plugin.metadata.model.PluginConflict; import org.spongepowered.plugin.metadata.model.PluginContributor; import org.spongepowered.plugin.metadata.model.PluginDependency; import org.spongepowered.plugin.metadata.model.PluginLinks; @@ -51,6 +52,7 @@ public InheritableMetadata deserialize(final JsonElement element, final Type typ .branding(GsonUtils.optional(obj, "branding").map(v -> context.deserialize(v, PluginBranding.class)).orElseGet(PluginBranding::none)) .links(GsonUtils.optional(obj, "links").map(v -> context.deserialize(v, PluginLinks.class)).orElseGet(PluginLinks::none)) .contributors(GsonUtils.stream(obj, "contributors").map(v -> context.deserialize(v, PluginContributor.class)).toList()) + .conflicts(GsonUtils.stream(obj, "conflicts").map(v -> context.deserialize(v, PluginConflict.class)).toList()) .dependencies(GsonUtils.stream(obj, "dependencies").map(v -> context.deserialize(v, PluginDependency.class)).toList()) .properties(GsonUtils.deserializeMap(obj.get("properties"), JsonElement::getAsString, LinkedHashMap::new)) .build(); @@ -73,6 +75,9 @@ public JsonElement serialize(final InheritableMetadata value, final Type type, f if (!value.contributors().isEmpty()) { obj.add("contributors", GsonUtils.toArray(value.contributors().stream().map(v -> context.serialize(v, PluginContributor.class)))); } + if (!value.conflicts().isEmpty()) { + obj.add("conflicts", GsonUtils.toArray(value.conflicts().stream().map(v -> context.serialize(v, PluginConflict.class)))); + } if (!value.dependencies().isEmpty()) { obj.add("dependencies", GsonUtils.toArray(value.dependencies().values().stream().map(v -> context.serialize(v, PluginDependency.class)))); } diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginConflictAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginConflictAdapter.java new file mode 100644 index 0000000..27069df --- /dev/null +++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginConflictAdapter.java @@ -0,0 +1,56 @@ +/* + * This file is part of plugin-meta, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.plugin.metadata.builtin.adapter.model; + +import com.google.gson.*; +import org.apache.maven.artifact.versioning.VersionRange; +import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils; +import org.spongepowered.plugin.metadata.model.PluginConflict; + +import java.lang.reflect.Type; + +public final class PluginConflictAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public PluginConflict deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException { + final JsonObject obj = element.getAsJsonObject(); + return new PluginConflict( + GsonUtils.require(obj, "id").getAsString(), + context.deserialize(GsonUtils.require(obj, "version"), VersionRange.class), + GsonUtils.optional(obj, "fatal").map(JsonElement::getAsBoolean).orElse(false), + GsonUtils.optional(obj, "reason").map(JsonElement::getAsString) + ); + } + + @Override + public JsonElement serialize(final PluginConflict value, final Type type, final JsonSerializationContext context) { + final JsonObject obj = new JsonObject(); + obj.addProperty("id", value.id()); + obj.add("version", context.serialize(value.version(), VersionRange.class)); + obj.addProperty("fatal", value.fatal()); + value.reason().ifPresent(v -> obj.addProperty("reason", v)); + return obj; + } +} diff --git a/src/main/java/org/spongepowered/plugin/metadata/model/PluginConflict.java b/src/main/java/org/spongepowered/plugin/metadata/model/PluginConflict.java new file mode 100644 index 0000000..587f455 --- /dev/null +++ b/src/main/java/org/spongepowered/plugin/metadata/model/PluginConflict.java @@ -0,0 +1,62 @@ +/* + * This file is part of plugin-meta, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.plugin.metadata.model; + +import org.apache.maven.artifact.versioning.VersionRange; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.plugin.metadata.Constants; +import org.spongepowered.plugin.metadata.PluginMetadata; + +import java.util.Objects; +import java.util.Optional; + +/** + * Specification for an entity considered to be conflicting with a {@link PluginMetadata plugin metadata}. + *

+ * The vendor will either log a warning or refuse to load when this entity is present. + * + * @see Constants#VALID_ID_PATTERN + * @param id The {@link String id} + * @param version The {@link VersionRange version}, as a maven range. + * @param fatal Whether this conflict should prevent loading + * @param reason The {@link String reason} + */ +public record PluginConflict(String id, VersionRange version, boolean fatal, Optional reason) { + + public PluginConflict { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(reason, "reason"); + + if (!Constants.VALID_ID_PATTERN.matcher(id).matches()) { + throw new IllegalStateException(String.format("Conflict with supplied ID '{%s}' is invalid. %s", id, + Constants.INVALID_ID_REQUIREMENTS_MESSAGE)); + } + } + + public PluginConflict(String id, VersionRange version, boolean fatal, @Nullable String reason) { + this(id, version, fatal, Optional.ofNullable(reason)); + } +} diff --git a/src/main/java/org/spongepowered/plugin/metadata/model/PluginDependency.java b/src/main/java/org/spongepowered/plugin/metadata/model/PluginDependency.java index 2fbcb4b..0146b07 100644 --- a/src/main/java/org/spongepowered/plugin/metadata/model/PluginDependency.java +++ b/src/main/java/org/spongepowered/plugin/metadata/model/PluginDependency.java @@ -35,15 +35,7 @@ *

* Consult the vendor for further information on how this is used. * - *

Ids must conform to the following requirements:

- * - *
    - *
  • Must be between 2 and 64 characters in length
  • - *
  • Must start with a lower case letter (a-z)
  • - *
  • May only contain a mix of lower case letters (a-z), - * numbers (0-9) and underscores (_)
  • - *
- * + * @see Constants#VALID_ID_PATTERN * @param id The {@link String id} * @param version The {@link VersionRange version}, as a maven range. * @param loadOrder The {@link LoadOrder load order} diff --git a/src/test/java/org/spongepowered/plugin/metadata/builtin/MetadataParserTest.java b/src/test/java/org/spongepowered/plugin/metadata/builtin/MetadataParserTest.java index ca8107c..be12bee 100644 --- a/src/test/java/org/spongepowered/plugin/metadata/builtin/MetadataParserTest.java +++ b/src/test/java/org/spongepowered/plugin/metadata/builtin/MetadataParserTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.spongepowered.plugin.metadata.PluginMetadata; +import org.spongepowered.plugin.metadata.model.PluginConflict; import org.spongepowered.plugin.metadata.model.PluginContributor; import org.spongepowered.plugin.metadata.model.PluginDependency; import org.spongepowered.plugin.metadata.model.PluginEntrypoints; @@ -79,6 +80,7 @@ private static List writeContainer(final MetadataContainer container) th URI.create("https://github.com/SpongePowered/Sponge/issues") )) .addContributor(new PluginContributor("Spongie", "Mascot")) + .addConflict(new PluginConflict("bad_plugin", VersionRange.createFromVersion("3.2.1"), true, "Incompatible for some reasons")) .addDependency(new PluginDependency("spongeapi", VersionRange.createFromVersion("17.0.0"), PluginDependency.LoadOrder.AFTER, false)) .build(); diff --git a/src/test/resources/legacy/entrypoint.json b/src/test/resources/legacy/entrypoint.json index 4e6f8c8..b073aaa 100644 --- a/src/test/resources/legacy/entrypoint.json +++ b/src/test/resources/legacy/entrypoint.json @@ -24,6 +24,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi", diff --git a/src/test/resources/valid/full_global.json b/src/test/resources/valid/full_global.json index 3284035..117adc8 100644 --- a/src/test/resources/valid/full_global.json +++ b/src/test/resources/valid/full_global.json @@ -19,6 +19,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi", diff --git a/src/test/resources/valid/full_override.json b/src/test/resources/valid/full_override.json index 57475db..28adb82 100644 --- a/src/test/resources/valid/full_override.json +++ b/src/test/resources/valid/full_override.json @@ -32,6 +32,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi", diff --git a/src/test/resources/valid/main_entrypoint_only.json b/src/test/resources/valid/main_entrypoint_only.json index c9efc5a..5de12b2 100644 --- a/src/test/resources/valid/main_entrypoint_only.json +++ b/src/test/resources/valid/main_entrypoint_only.json @@ -26,6 +26,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi", diff --git a/src/test/resources/valid/mix.json b/src/test/resources/valid/mix.json index 1401eb0..e66fe4b 100644 --- a/src/test/resources/valid/mix.json +++ b/src/test/resources/valid/mix.json @@ -34,6 +34,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi", diff --git a/src/test/resources/valid/mix_in_root.json b/src/test/resources/valid/mix_in_root.json index b1efd2a..b1d03c0 100644 --- a/src/test/resources/valid/mix_in_root.json +++ b/src/test/resources/valid/mix_in_root.json @@ -32,6 +32,14 @@ "description": "Mascot" } ], + "conflicts": [ + { + "id": "bad_plugin", + "version": "3.2.1", + "fatal": true, + "reason": "Incompatible for some reasons" + } + ], "dependencies": [ { "id": "spongeapi",