Skip to content

Commit 3b7e150

Browse files
committed
Improve testability and error checking
1 parent 8877072 commit 3b7e150

20 files changed

Lines changed: 582 additions & 29 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ bus.ifPresent(redisBus -> {
4848

4949
- Caller identity is resolved at runtime from the platform plugin context.
5050
- `registerDatabase(...)` is reference-counted internally; repeated registrations reuse the same connection.
51+
-

docs/ARCHITECTURE_REVIEW.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Architecture Review
2+
3+
Reviewed on: 2026-03-24
4+
5+
## Scope
6+
7+
- Public API ergonomics (`DataProviderAPI`, `DatabaseProvider` helpers)
8+
- Runtime safety in connection/config resolution internals
9+
- Documentation and test coverage quality
10+
11+
## Strengths
12+
13+
- Clear platform boundary (`BukkitDataProvider`, `VelocityDataProvider`) with a shared internal core.
14+
- Plugin-scoped identity model protects privileged operations from external callers.
15+
- Unified abstraction over relational, document, key-value, and messaging backends.
16+
- Reference-counted connection registry prevents duplicate connection churn.
17+
18+
## Key Findings (Before Improvements)
19+
20+
- Test coverage was effectively absent (`src/test/java/Test.java` placeholder only).
21+
- Bundled database templates used `default_credentials`, while docs/examples/integration commonly used `default`.
22+
- API default helper methods had null/readiness edge-cases that could produce unclear failures.
23+
- Missing docs files referenced by README reduced maintainability and onboarding quality.
24+
25+
## Improvements Applied
26+
27+
- Added real JUnit 5 unit tests for:
28+
- `DatabaseProvider` helper semantics (`Optional` + `require*` behaviors)
29+
- `DatabaseConfigMap` identifier fallback and diagnostics
30+
- Document query/update builder input validation
31+
- Improved API helper behavior:
32+
- `getDataAccessOptional(...)` now safely handles not-ready providers.
33+
- `requireDataAccess(...)` now reports null provider data access explicitly.
34+
- `getDataSourceOptional()` now tolerates unsupported and not-ready provider states.
35+
- Improved config usability:
36+
- Added compatibility alias resolution between `default` and `default_credentials`.
37+
- Added clearer missing-section warnings with available section names.
38+
- Updated bundled config templates to include `default` as the primary identifier.
39+
- Added `player_data_rw` MySQL template for common ORM write usage.
40+
- Hardened registry lookups:
41+
- Stale/disconnected providers are now evicted during lookup, not just during registration.
42+
43+
## Residual Risks / Next Steps
44+
45+
- Most tests are unit-level; no automated integration tests currently validate real MySQL/Mongo/Redis instances.
46+
- Memcached implementations exist but are not reachable through `DatabaseType`; either expose or remove to reduce dead surface area.
47+
- Consider adding CI quality gates (`checkstyle`, test coverage thresholds, and smoke integration matrix).

docs/BEST_PRACTICES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727

2828
## Configuration
2929

30-
- Keep one connection identifier per concrete config section (for example: `default`, `player_data_rw`).
30+
- Keep one connection identifier per concrete config section (for example: `default`).
31+
- Keep identifier names consistent across code and YAML sections.
3132
- Use separate identifiers for different operational requirements (read-only, read-write, analytics).
3233

3334
## Concurrency

docs/USAGE_GUIDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ Optional<MessagingDataAccess> redisBus = api.registerDataAccess(
5353
);
5454
```
5555

56+
Identifier guidance:
57+
58+
- Prefer `default` for single-connection setups.
59+
5660
## 3. Use the provider safely
5761

5862
`DatabaseProvider` has helper methods to avoid raw casts:

pom.xml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1616
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
1717
<velocity.version>3.5.0-SNAPSHOT</velocity.version>
18+
<junit.jupiter.version>5.10.2</junit.jupiter.version>
19+
<maven.surefire.version>3.2.5</maven.surefire.version>
1820
</properties>
1921

2022
<distributionManagement>
@@ -30,11 +32,16 @@
3032
<plugin>
3133
<groupId>org.apache.maven.plugins</groupId>
3234
<artifactId>maven-compiler-plugin</artifactId>
35+
<version>3.13.0</version>
3336
<configuration>
34-
<source>21</source>
35-
<target>21</target>
37+
<release>21</release>
3638
</configuration>
3739
</plugin>
40+
<plugin>
41+
<groupId>org.apache.maven.plugins</groupId>
42+
<artifactId>maven-surefire-plugin</artifactId>
43+
<version>${maven.surefire.version}</version>
44+
</plugin>
3845
<plugin>
3946
<groupId>org.apache.maven.plugins</groupId>
4047
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -121,9 +128,9 @@
121128
<scope>provided</scope>
122129
</dependency>
123130
<dependency>
124-
<groupId>junit</groupId>
125-
<artifactId>junit</artifactId>
126-
<version>3.8.1</version>
131+
<groupId>org.junit.jupiter</groupId>
132+
<artifactId>junit-jupiter</artifactId>
133+
<version>${junit.jupiter.version}</version>
127134
<scope>test</scope>
128135
</dependency>
129136
<!-- Hibernate Core -->

src/main/java/nl/hauntedmc/dataprovider/api/DataProviderAPI.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public <T extends DataAccess> Optional<T> registerDataAccess(
6363
String connectionIdentifier,
6464
Class<T> expectedDataAccessType
6565
) {
66+
Objects.requireNonNull(expectedDataAccessType, "Expected data access type cannot be null.");
6667
return registerDatabaseOptional(databaseType, connectionIdentifier)
6768
.flatMap(provider -> provider.getDataAccessOptional(expectedDataAccessType));
6869
}
@@ -121,6 +122,7 @@ public <T extends DataAccess> Optional<T> getRegisteredDataAccess(
121122
String connectionIdentifier,
122123
Class<T> expectedDataAccessType
123124
) {
125+
Objects.requireNonNull(expectedDataAccessType, "Expected data access type cannot be null.");
124126
return getRegisteredDatabaseOptional(databaseType, connectionIdentifier)
125127
.flatMap(provider -> provider.getDataAccessOptional(expectedDataAccessType));
126128
}

src/main/java/nl/hauntedmc/dataprovider/database/DatabaseProvider.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ public interface DatabaseProvider {
5050
*/
5151
default <T extends DataAccess> Optional<T> getDataAccessOptional(Class<T> expectedType) {
5252
Objects.requireNonNull(expectedType, "Expected data access type cannot be null.");
53-
DataAccess dataAccess = getDataAccess();
53+
DataAccess dataAccess;
54+
try {
55+
dataAccess = getDataAccess();
56+
} catch (IllegalStateException | UnsupportedOperationException ignored) {
57+
return Optional.empty();
58+
}
59+
if (dataAccess == null) {
60+
return Optional.empty();
61+
}
5462
if (expectedType.isInstance(dataAccess)) {
5563
return Optional.of(expectedType.cast(dataAccess));
5664
}
@@ -67,6 +75,12 @@ default <T extends DataAccess> Optional<T> getDataAccessOptional(Class<T> expect
6775
default <T extends DataAccess> T requireDataAccess(Class<T> expectedType) {
6876
Objects.requireNonNull(expectedType, "Expected data access type cannot be null.");
6977
DataAccess dataAccess = getDataAccess();
78+
if (dataAccess == null) {
79+
throw new IllegalStateException(
80+
"Expected data access type " + expectedType.getName()
81+
+ " but provider returned null."
82+
);
83+
}
7084
if (expectedType.isInstance(dataAccess)) {
7185
return expectedType.cast(dataAccess);
7286
}
@@ -84,7 +98,7 @@ default <T extends DataAccess> T requireDataAccess(Class<T> expectedType) {
8498
default Optional<DataSource> getDataSourceOptional() {
8599
try {
86100
return Optional.ofNullable(getDataSource());
87-
} catch (UnsupportedOperationException ignored) {
101+
} catch (UnsupportedOperationException | IllegalStateException ignored) {
88102
return Optional.empty();
89103
}
90104
}

src/main/java/nl/hauntedmc/dataprovider/database/document/model/DocumentQuery.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55
import java.util.Map;
6+
import java.util.Objects;
67

78
/**
89
* A minimal DSL for building a "query" filter.
@@ -21,7 +22,8 @@ public class DocumentQuery {
2122
* @return this query instance for chaining
2223
*/
2324
public DocumentQuery eq(String field, Object value) {
24-
criteria.put(field, value);
25+
String validatedField = requireFieldName(field);
26+
criteria.put(validatedField, value);
2527
return this;
2628
}
2729

@@ -33,9 +35,11 @@ public DocumentQuery eq(String field, Object value) {
3335
* @return this query instance for chaining
3436
*/
3537
public DocumentQuery gte(String field, Object value) {
38+
String validatedField = requireFieldName(field);
39+
Objects.requireNonNull(value, "Greater-than-or-equal value cannot be null.");
3640
Map<String, Object> op = new HashMap<>();
3741
op.put("$gte", value);
38-
criteria.put(field, op);
42+
criteria.put(validatedField, op);
3943
return this;
4044
}
4145

@@ -47,7 +51,9 @@ public DocumentQuery gte(String field, Object value) {
4751
* @return this query instance for chaining
4852
*/
4953
public DocumentQuery raw(String field, Object expression) {
50-
criteria.put(field, expression);
54+
String validatedField = requireFieldName(field);
55+
Objects.requireNonNull(expression, "Raw expression cannot be null.");
56+
criteria.put(validatedField, expression);
5157
return this;
5258
}
5359

@@ -59,4 +65,11 @@ public DocumentQuery raw(String field, Object expression) {
5965
public Map<String, Object> toMap() {
6066
return Collections.unmodifiableMap(criteria);
6167
}
68+
69+
private static String requireFieldName(String field) {
70+
if (field == null || field.isBlank()) {
71+
throw new IllegalArgumentException("Query field name cannot be null or blank.");
72+
}
73+
return field;
74+
}
6275
}

src/main/java/nl/hauntedmc/dataprovider/database/document/model/DocumentUpdate.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55
import java.util.Map;
6+
import java.util.Objects;
67

78
/**
89
* Represents an update operation.
@@ -20,10 +21,11 @@ public class DocumentUpdate {
2021
* @return this update instance for chaining
2122
*/
2223
public DocumentUpdate set(String field, Object value) {
24+
String validatedField = requireFieldName(field);
2325
operations.computeIfAbsent("$set", k -> new HashMap<String, Object>());
2426
@SuppressWarnings("unchecked")
2527
Map<String, Object> setMap = (Map<String, Object>) operations.get("$set");
26-
setMap.put(field, value);
28+
setMap.put(validatedField, value);
2729
return this;
2830
}
2931

@@ -35,10 +37,12 @@ public DocumentUpdate set(String field, Object value) {
3537
* @return this update instance for chaining
3638
*/
3739
public DocumentUpdate inc(String field, Number amount) {
40+
String validatedField = requireFieldName(field);
41+
Objects.requireNonNull(amount, "Increment amount cannot be null.");
3842
operations.computeIfAbsent("$inc", k -> new HashMap<String, Object>());
3943
@SuppressWarnings("unchecked")
4044
Map<String, Object> incMap = (Map<String, Object>) operations.get("$inc");
41-
incMap.put(field, amount);
45+
incMap.put(validatedField, amount);
4246
return this;
4347
}
4448

@@ -50,4 +54,11 @@ public DocumentUpdate inc(String field, Number amount) {
5054
public Map<String, Object> toMap() {
5155
return Collections.unmodifiableMap(operations);
5256
}
57+
58+
private static String requireFieldName(String field) {
59+
if (field == null || field.isBlank()) {
60+
throw new IllegalArgumentException("Update field name cannot be null or blank.");
61+
}
62+
return field;
63+
}
5364
}

src/main/java/nl/hauntedmc/dataprovider/internal/DataProviderRegistry.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,18 @@ protected DatabaseProvider getDatabase(String pluginName, DatabaseType databaseT
131131
if (registration == null) {
132132
return null;
133133
}
134-
return registration.provider();
134+
135+
DatabaseProvider provider = registration.provider();
136+
if (isProviderHealthy(provider, key)) {
137+
return provider;
138+
}
139+
140+
if (activeDatabases.remove(key, registration)) {
141+
disconnectQuietly(provider, key, "stale connection during lookup");
142+
logger.warn("Removed stale " + databaseType.name() + " connection for " + pluginName
143+
+ " (" + connectionIdentifier + ") while retrieving the provider.");
144+
}
145+
return null;
135146
}
136147

137148
protected void unregisterDatabase(String pluginName, DatabaseType databaseType, String connectionIdentifier) {

0 commit comments

Comments
 (0)