Skip to content

Commit 50369b7

Browse files
committed
Add Bukkit-free shared libraries and safe Configurate configuration I/O
Build core, Configurate and SQL artifacts from the existing maintained sources without changing the legacy SimpleAPI POM or existing public APIs. Add revisioned local YAML editing, native SQL configuration, packaged classpath checks and pinned AdvancedCore/VotingPlugin compatibility builds.
1 parent fdec696 commit 50369b7

21 files changed

Lines changed: 1519 additions & 0 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Shared libraries and downstream compatibility
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main, codex/simpleapi-shared-libraries]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: shared-libraries-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
verify:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 25
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
persist-credentials: false
25+
- uses: actions/setup-java@v4
26+
with:
27+
java-version: '21'
28+
distribution: temurin
29+
cache: maven
30+
# Pinned compatibility fixtures, not moving branches or a release/deployment job.
31+
- uses: actions/checkout@v4
32+
with:
33+
repository: BenCodez/AdvancedCore
34+
ref: 3f66545590644d87ec45d7864d831691a07830ac
35+
path: .compat/AdvancedCore
36+
persist-credentials: false
37+
- uses: actions/checkout@v4
38+
with:
39+
repository: BenCodez/VotingPlugin
40+
ref: 350caccb12053a12bb47bbe22012b875580a77d2
41+
path: .compat/VotingPlugin
42+
persist-credentials: false
43+
- name: Isolate Maven cache
44+
shell: bash
45+
run: |
46+
set -euo pipefail
47+
repo="$RUNNER_TEMP/simpleapi-candidate-m2"
48+
mkdir -p "$repo"
49+
if [ -d "$HOME/.m2/repository" ]; then cp -a "$HOME/.m2/repository/." "$repo/"; fi
50+
echo "CANDIDATE_M2=$repo" >> "$GITHUB_ENV"
51+
- name: Build shared libraries and unchanged legacy artifact
52+
run: mvn -B -ntp -Dmaven.repo.local="$CANDIDATE_M2" clean install
53+
- name: Collect runtime classpaths
54+
shell: bash
55+
run: |
56+
set -euo pipefail
57+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -pl simpleapi-core,simpleapi-configurate,simpleapi-sql -am org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/runtime-deps
58+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -f SimpleAPI/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath -Dmdep.outputFile=target/compatibility-classpath.txt
59+
- name: Verify actual packaged artifacts and Bukkit parity
60+
run: python3 tools/verify-shared-artifacts.py
61+
- name: Pin candidate dependency bytes
62+
shell: bash
63+
run: |
64+
set -euo pipefail
65+
candidate="$CANDIDATE_M2/com/bencodez/simpleapi/1.0.2-SNAPSHOT/simpleapi-1.0.2-SNAPSHOT.jar"
66+
cmp SimpleAPI/target/SimpleAPI.jar "$candidate"
67+
sha256sum "$candidate" > "$RUNNER_TEMP/simpleapi-candidate.sha256"
68+
- name: Build AdvancedCore against candidate
69+
shell: bash
70+
run: |
71+
set -euo pipefail
72+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -f .compat/AdvancedCore/AdvancedCore/pom.xml clean install
73+
sha256sum -c "$RUNNER_TEMP/simpleapi-candidate.sha256"
74+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -f .compat/AdvancedCore/AdvancedCore/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath -Dmdep.outputFile=target/candidate-classpath.txt
75+
grep -F "$CANDIDATE_M2/com/bencodez/simpleapi/1.0.2-SNAPSHOT/simpleapi-1.0.2-SNAPSHOT.jar" .compat/AdvancedCore/AdvancedCore/target/candidate-classpath.txt
76+
sha256sum "$CANDIDATE_M2/com/bencodez/advancedcore/3.8.2-SNAPSHOT/advancedcore-3.8.2-SNAPSHOT.jar" > "$RUNNER_TEMP/advancedcore-candidate.sha256"
77+
- name: Build VotingPlugin against candidate chain
78+
shell: bash
79+
run: |
80+
set -euo pipefail
81+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -f .compat/VotingPlugin/VotingPlugin/pom.xml clean verify
82+
sha256sum -c "$RUNNER_TEMP/simpleapi-candidate.sha256"
83+
sha256sum -c "$RUNNER_TEMP/advancedcore-candidate.sha256"
84+
mvn -B -ntp -nsu -Dmaven.repo.local="$CANDIDATE_M2" -f .compat/VotingPlugin/VotingPlugin/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath -Dmdep.outputFile=target/candidate-classpath.txt
85+
grep -F "$CANDIDATE_M2/com/bencodez/advancedcore/3.8.2-SNAPSHOT/advancedcore-3.8.2-SNAPSHOT.jar" .compat/VotingPlugin/VotingPlugin/target/candidate-classpath.txt
86+
- name: Preserve test reports and candidate artifacts
87+
if: always()
88+
uses: actions/upload-artifact@v4
89+
with:
90+
name: shared-library-validation
91+
retention-days: 7
92+
if-no-files-found: warn
93+
include-hidden-files: true
94+
path: |
95+
simpleapi-*/target/*.jar
96+
simpleapi-*/target/surefire-reports/*.xml
97+
SimpleAPI/target/surefire-reports/*.xml
98+
.compat/AdvancedCore/AdvancedCore/target/surefire-reports/*.xml
99+
.compat/VotingPlugin/VotingPlugin/target/surefire-reports/*.xml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/target/
2+
.compat/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.bencodez.simpleapi.file.config;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Path;
5+
import java.util.function.Consumer;
6+
7+
/**
8+
* Synchronous local-file configuration operations. Call from an I/O worker, not
9+
* a server/entity thread. Reads and failed operations never create empty files
10+
* or replace a last-good snapshot. The caller owns exclusive writes to the file;
11+
* content revisions detect observed external edits, not cross-process file CAS.
12+
*/
13+
public interface ConfigDocument {
14+
Path path();
15+
ConfigSnapshot snapshot();
16+
ConfigSnapshot reload() throws IOException;
17+
/**
18+
* Edits a private copy, persists it, then publishes the new snapshot. Any
19+
* validation, stale-revision or pre-publication I/O failure leaves the current
20+
* in-memory snapshot unchanged. The callback must not reenter document writes.
21+
*/
22+
ConfigSnapshot update(String expectedRevision, Consumer<ConfigEditor> edit) throws IOException;
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.bencodez.simpleapi.file.config;
2+
3+
/**
4+
* A short-lived editor supplied by ConfigDocument.update. Retaining an editor
5+
* does not grant access to future document state. Implementations reject writes
6+
* after the callback returns. Values are plain scalar/list/string-keyed map trees;
7+
* native player/item objects must be converted by the owning platform first.
8+
*/
9+
public interface ConfigEditor extends ConfigView {
10+
/** Sets a value using the document's separator. Null removes the value. */
11+
void set(String path, Object value);
12+
/** Sets a value using literal key segments, including keys containing dots. */
13+
void setAt(Object value, String... keys);
14+
default void remove(String path) { set(path, null); }
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.bencodez.simpleapi.file.config;
2+
3+
import java.util.Objects;
4+
5+
/** Detached read view and opaque content revision used to reject stale edits. */
6+
public record ConfigSnapshot(String revision, ConfigView view) {
7+
public ConfigSnapshot {
8+
Objects.requireNonNull(revision, "revision");
9+
Objects.requireNonNull(view, "view");
10+
}
11+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.bencodez.simpleapi.file.config.configurate;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashSet;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
import java.util.Set;
9+
import java.util.regex.Pattern;
10+
11+
import org.spongepowered.configurate.ConfigurationNode;
12+
13+
import com.bencodez.simpleapi.file.config.ConfigView;
14+
15+
/**
16+
* Read-only view of a Configurate section. Scalar numbers/booleans are read
17+
* strictly, rather than using Configurate's more permissive string coercions.
18+
* This matches the Bukkit scalar/list rules used by the existing binder.
19+
*
20+
* <p>Paths use '.' by default; at(String...) uses literal segments for identifiers
21+
* containing dots. An externally supplied node is a live view and the caller must
22+
* coordinate its mutation. YamlConfigDocument supplies detached snapshots.</p>
23+
*
24+
* <p>Getter/annotation defaults are supported. Bukkit's mutable default-section
25+
* overlay and native serialized Bukkit objects are deliberately not emulated.</p>
26+
*/
27+
public class ConfigurateConfigView implements ConfigView {
28+
protected final ConfigurationNode node;
29+
private final char separator;
30+
31+
public ConfigurateConfigView(ConfigurationNode node) { this(node, '.'); }
32+
33+
public ConfigurateConfigView(ConfigurationNode node, char separator) {
34+
this.node = Objects.requireNonNull(node, "node");
35+
this.separator = separator;
36+
}
37+
38+
protected final String[] segments(String path) {
39+
Objects.requireNonNull(path, "path");
40+
return path.isEmpty() ? new String[0] : path.split(Pattern.quote(String.valueOf(separator)), -1);
41+
}
42+
43+
protected final ConfigurationNode resolve(String... keys) {
44+
Objects.requireNonNull(keys, "keys");
45+
ConfigurationNode current = node;
46+
for (String key : keys) {
47+
Objects.requireNonNull(key, "key");
48+
ConfigurationNode match = null;
49+
// YAML numeric keys remain addressable as strings, without replacing
50+
// them with a second, differently typed key when an editor writes.
51+
for (Map.Entry<Object, ? extends ConfigurationNode> entry : current.childrenMap().entrySet()) {
52+
if (String.valueOf(entry.getKey()).equals(key)) {
53+
if (match != null) throw new IllegalArgumentException("Ambiguous configuration key");
54+
match = entry.getValue();
55+
}
56+
}
57+
current = match == null ? current.node(key) : match;
58+
}
59+
return current;
60+
}
61+
62+
/** Returns a section using literal key segments, or null for an absent/non-map node. */
63+
public ConfigurateConfigView at(String... keys) {
64+
ConfigurationNode child = resolve(keys);
65+
return child.isMap() || child == node && child.isNull()
66+
? new ConfigurateConfigView(child, separator) : null;
67+
}
68+
69+
@Override public boolean contains(String path) {
70+
return path.isEmpty() || !resolve(segments(path)).isNull();
71+
}
72+
@Override public String getString(String path, String fallback) {
73+
ConfigurationNode child = resolve(segments(path));
74+
if (child.isMap()) return fallback; // no Bukkit section toString emulation
75+
Object value = child.raw();
76+
return value == null ? fallback : value.toString();
77+
}
78+
@Override public boolean getBoolean(String path, boolean fallback) {
79+
Object value = resolve(segments(path)).rawScalar();
80+
return value instanceof Boolean bool ? bool : fallback;
81+
}
82+
@Override public int getInt(String path, int fallback) {
83+
Object value = resolve(segments(path)).rawScalar();
84+
return value instanceof Number number ? number.intValue() : fallback;
85+
}
86+
@Override public long getLong(String path, long fallback) {
87+
Object value = resolve(segments(path)).rawScalar();
88+
return value instanceof Number number ? number.longValue() : fallback;
89+
}
90+
@Override public double getDouble(String path, double fallback) {
91+
Object value = resolve(segments(path)).rawScalar();
92+
return value instanceof Number number ? number.doubleValue() : fallback;
93+
}
94+
@Override public List<String> getStringList(String path) {
95+
List<String> result = new ArrayList<>();
96+
for (ConfigurationNode child : resolve(segments(path)).childrenList()) {
97+
Object value = child.rawScalar();
98+
if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Character)
99+
result.add(value.toString());
100+
}
101+
return result;
102+
}
103+
@Override public List<Integer> getIntegerList(String path) {
104+
List<Integer> result = new ArrayList<>();
105+
for (ConfigurationNode child : resolve(segments(path)).childrenList()) {
106+
Object value = child.rawScalar();
107+
if (value instanceof Number number) result.add(number.intValue());
108+
else if (value instanceof Character character) result.add((int) character);
109+
else if (value instanceof String string) {
110+
try { result.add(Integer.parseInt(string)); } catch (NumberFormatException ignored) { }
111+
}
112+
}
113+
return result;
114+
}
115+
@Override public boolean isConfigurationSection(String path) { return at(segments(path)) != null; }
116+
@Override public ConfigurateConfigView getConfigurationSection(String path) { return at(segments(path)); }
117+
@Override public Set<String> getKeys(boolean deep) {
118+
LinkedHashSet<String> result = new LinkedHashSet<>();
119+
collectKeys(node, "", deep, result, 0);
120+
return result;
121+
}
122+
private void collectKeys(ConfigurationNode parent, String prefix, boolean deep, Set<String> keys, int depth) {
123+
if (depth > 64) throw new IllegalArgumentException("Configuration nesting is too deep");
124+
Set<String> siblings = new LinkedHashSet<>();
125+
for (Map.Entry<Object, ? extends ConfigurationNode> entry : parent.childrenMap().entrySet()) {
126+
String key = String.valueOf(entry.getKey());
127+
if (!siblings.add(key)) throw new IllegalArgumentException("Ambiguous configuration key");
128+
String path = prefix + key;
129+
keys.add(path);
130+
if (deep && entry.getValue().isMap()) collectKeys(entry.getValue(), path + separator, true, keys, depth + 1);
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)