Skip to content

Commit 18b4cdd

Browse files
authored
refactor: general quality improvements (#13)
1 parent a4991d3 commit 18b4cdd

26 files changed

Lines changed: 621 additions & 703 deletions

ANALYSIS.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Structural validation runs at **load time** — before any change executes — v
2727
- `OperationValidator` — Interface for per-operation parameter validators + unrecognized key utility
2828
- 8 parameter validators (`InsertParametersValidator`, `UpdateParametersValidator`, `DeleteParametersValidator`, `CreateIndexParametersValidator`, `DropIndexParametersValidator`, `RenameCollectionParametersValidator`, `CreateViewParametersValidator`, `ModifyCollectionParametersValidator`)
2929
- 11 operator classes (`CreateCollectionOperator`, `InsertOperator`, etc.)
30-
- 6 mapper classes (`IndexOptionsMapper`, `InsertOptionsMapper`, `UpdateOptionsMapper`, `CreateViewOptionsMapper`, `RenameCollectionOptionsMapper`, `MapperUtil`)
30+
- 7 mapper classes (`IndexOptionsMapper`, `InsertOptionsMapper`, `UpdateOptionsMapper`, `CreateViewOptionsMapper`, `RenameCollectionOptionsMapper`, `MapperUtil`, `BsonConverter`)
3131

3232
---
3333

@@ -227,11 +227,11 @@ All 7 changes from the original analysis have been implemented across multiple P
227227
- Unrecognized parameter key detection catches typos at load time
228228

229229
### Negative
230-
- Heavy code duplication in `InsertOperator` and `UpdateOperator` (4 branches for session x options combinations)
231-
- `MongoOperation` is a god object — it has getters for every operation type's parameters, even though each getter is only relevant to 1–2 operation types
230+
- ~~Heavy code duplication in `InsertOperator` and `UpdateOperator`~~ RESOLVED — Flattened nested if/else to single chain; extracted options to local variable to avoid duplicate mapper calls
231+
- ~~`MongoOperation` is a god object~~ REDUCED — Moved 9 single-use getters to their respective operator classes as private methods. `MongoOperation` retains 4 shared getters (`getOptions`, `getFilter`, `isMulti`, `getKeys`) used by multiple operators
232232
- No builder pattern or factory for `MongoOperation` in tests — all tests manually construct via setters
233-
- `MapperUtil` mixes concerns: type extraction + BSON conversion + Collation building in one class
234-
- Test infrastructure duplication — every integration test class independently sets up MongoDBContainer with identical boilerplate
233+
- ~~`MapperUtil` mixes concerns: type extraction + BSON conversion + Collation building~~ RESOLVED — Split into `MapperUtil` (parameter extraction + collation) and `BsonConverter` (BSON serialization)
234+
- ~~Test infrastructure duplication~~ RESOLVED — Extracted `AbstractMongoOperatorTest` base class with shared MongoDBContainer setup; 12 test classes now extend it
235235

236236
---
237237

@@ -244,8 +244,8 @@ All 7 changes from the original analysis have been implemented across multiple P
244244
| Validation & Error Handling | 20% | 9 | 1.80 |
245245
| Test Coverage | 20% | 6 | 1.20 |
246246
| Security & Safety | 10% | 8 | 0.80 |
247-
| Code Quality & Maintainability | 5% | 7 | 0.35 |
248-
| **Total** | **100%** | | **8.2 / 10** |
247+
| Code Quality & Maintainability | 5% | 8 | 0.40 |
248+
| **Total** | **100%** | | **8.3 / 10** |
249249

250250
### Score Justification
251251

@@ -259,8 +259,8 @@ All 7 changes from the original analysis have been implemented across multiple P
259259

260260
**Security (8/10):** Developer-authored context makes injection-style concerns not applicable. Collection name `$`/`\0` checks serve as guardrails, now applied to both `collection` and `target` parameters. `modifyCollection` parameters are validated against known values. YAML deserialization is delegated to the framework.
261261

262-
**Code Quality (7/10):** Clean code style, good naming, proper license headers. Deductions remain for code duplication in operators (session x options branches), the `MongoOperation` god-object pattern, and test infrastructure boilerplate. These are maintainability concerns, not correctness issues.
262+
**Code Quality (8/10):** Clean code style, good naming, proper license headers. Four of five code quality negatives have been addressed: operator duplication flattened, `MongoOperation` god-object reduced (9 getters moved to operators), `MapperUtil` split into extraction + BSON conversion, and test boilerplate extracted to `AbstractMongoOperatorTest`. Remaining deduction: no builder/factory for `MongoOperation` in tests, and `MongoOperation` still has 4 shared getters.
263263

264264
### Bottom Line
265265

266-
The module has **matured significantly** from its initial state. All 10 identified issues have been resolved, the validation architecture was overhauled to leverage framework-level load-time validation, and the test suite grew from ~90 to ~199 tests. The remaining gaps are in integration-level testing (transactional paths, options end-to-end) and code-level cleanup (operator duplication, god-object pattern). The module is **ready for production use** with the understanding that transactional behavior is covered by the framework's integration test suite.
266+
The module has **matured significantly** from its initial state. All 10 identified issues have been resolved, the validation architecture was overhauled to leverage framework-level load-time validation, and the test suite grew from ~90 to ~199 tests. Code quality improvements addressed 4 of 5 negatives: operator duplication flattened, god-object reduced, `MapperUtil` concerns separated, and test boilerplate extracted. The remaining gaps are in integration-level testing (transactional paths, options end-to-end). The module is **ready for production use** with the understanding that transactional behavior is covered by the framework's integration test suite.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.template.mongodb.mapper;
17+
18+
import org.bson.BsonArray;
19+
import org.bson.BsonDocument;
20+
import org.bson.BsonValue;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
public final class BsonConverter {
26+
private BsonConverter() {}
27+
28+
public static BsonDocument toBsonDocument(Map<String, Object> map) {
29+
BsonDocument document = new BsonDocument();
30+
map.forEach((key, value) -> document.append(key, toBsonValue(value)));
31+
return document;
32+
}
33+
34+
@SuppressWarnings("unchecked")
35+
public static BsonValue toBsonValue(Object value) {
36+
if (value == null) {
37+
return org.bson.BsonNull.VALUE;
38+
} else if (value instanceof String) {
39+
return new org.bson.BsonString((String) value);
40+
} else if (value instanceof Integer) {
41+
return new org.bson.BsonInt32((Integer) value);
42+
} else if (value instanceof Long) {
43+
return new org.bson.BsonInt64((Long) value);
44+
} else if (value instanceof Double) {
45+
return new org.bson.BsonDouble((Double) value);
46+
} else if (value instanceof Boolean) {
47+
return new org.bson.BsonBoolean((Boolean) value);
48+
} else if (value instanceof List) {
49+
return toBsonArray((List<?>) value);
50+
} else if (value instanceof Map) {
51+
return toBsonDocument((Map<String, Object>) value);
52+
}
53+
throw new IllegalArgumentException("Unsupported BSON type: " + value.getClass().getSimpleName());
54+
}
55+
56+
public static BsonArray toBsonArray(List<?> list) {
57+
BsonArray array = new BsonArray();
58+
for (Object item : list) {
59+
array.add(toBsonValue(item));
60+
}
61+
return array;
62+
}
63+
}

src/main/java/io/flamingock/template/mongodb/mapper/MapperUtil.java

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@
2020
import com.mongodb.client.model.CollationCaseFirst;
2121
import com.mongodb.client.model.CollationMaxVariable;
2222
import com.mongodb.client.model.CollationStrength;
23-
import org.bson.BsonArray;
24-
import org.bson.BsonDocument;
25-
import org.bson.BsonValue;
2623
import org.bson.conversions.Bson;
2724

28-
import java.util.List;
2925
import java.util.Map;
3026

3127
public final class MapperUtil {
@@ -82,7 +78,7 @@ public static Bson getBson(Map<String, Object> options, String key) {
8278
if (value instanceof Bson) {
8379
return (Bson) value;
8480
} else if (value instanceof Map) {
85-
return toBsonDocument((Map<String, Object>) value);
81+
return BsonConverter.toBsonDocument((Map<String, Object>) value);
8682
} else {
8783
throw new IllegalArgumentException(String.format("field[%s] should be Bson", key));
8884
}
@@ -135,42 +131,4 @@ private static Collation buildCollationFromMap(Map<String, Object> map) {
135131
return builder.build();
136132
}
137133

138-
// Recursively converts a Map<String, Object> to BsonDocument
139-
public static BsonDocument toBsonDocument(Map<String, Object> map) {
140-
BsonDocument document = new BsonDocument();
141-
map.forEach((key, value) -> document.append(key, toBsonValue(value)));
142-
return document;
143-
}
144-
145-
// Converts Java types into BSON types
146-
@SuppressWarnings("unchecked")
147-
public static BsonValue toBsonValue(Object value) {
148-
if (value == null) {
149-
return org.bson.BsonNull.VALUE;
150-
} else if (value instanceof String) {
151-
return new org.bson.BsonString((String) value);
152-
} else if (value instanceof Integer) {
153-
return new org.bson.BsonInt32((Integer) value);
154-
} else if (value instanceof Long) {
155-
return new org.bson.BsonInt64((Long) value);
156-
} else if (value instanceof Double) {
157-
return new org.bson.BsonDouble((Double) value);
158-
} else if (value instanceof Boolean) {
159-
return new org.bson.BsonBoolean((Boolean) value);
160-
} else if (value instanceof List) {
161-
return toBsonArray((List<?>) value);
162-
} else if (value instanceof Map) {
163-
return toBsonDocument((Map<String, Object>) value);
164-
}
165-
throw new IllegalArgumentException("Unsupported BSON type: " + value.getClass().getSimpleName());
166-
}
167-
168-
// Converts a List to BsonArray
169-
public static BsonArray toBsonArray(List<?> list) {
170-
BsonArray array = new BsonArray();
171-
for (Object item : list) {
172-
array.add(toBsonValue(item));
173-
}
174-
return array;
175-
}
176134
}

src/main/java/io/flamingock/template/mongodb/mapper/UpdateOptionsMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static UpdateOptions mapToUpdateOptions(Map<String, Object> options) {
4949
if (filter instanceof Map) {
5050
@SuppressWarnings("unchecked")
5151
Map<String, Object> filterMap = (Map<String, Object>) filter;
52-
bsonFilters.add(MapperUtil.toBsonDocument(filterMap));
52+
bsonFilters.add(BsonConverter.toBsonDocument(filterMap));
5353
} else if (filter instanceof org.bson.conversions.Bson) {
5454
bsonFilters.add((org.bson.conversions.Bson) filter);
5555
}

src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.util.ArrayList;
2929
import java.util.List;
3030
import java.util.Map;
31-
import java.util.stream.Collectors;
3231

3332
@NonLockGuarded(NonLockGuardedType.NONE)
3433
public class MongoOperation implements TemplatePayload {
@@ -48,20 +47,11 @@ public class MongoOperation implements TemplatePayload {
4847

4948
public void setParameters(Map<String, Object> parameters) { this.parameters = parameters; }
5049

51-
@SuppressWarnings("unchecked")
52-
public List<Document> getDocuments() {
53-
return ((List<Map<String, Object>>) parameters.get("documents"))
54-
.stream().map(Document::new)
55-
.collect(Collectors.toList());
56-
}
57-
5850
@SuppressWarnings("unchecked")
5951
public Document getKeys() {
6052
return new Document((Map<String, Object>) parameters.get("keys"));
6153
}
6254

63-
64-
6555
@SuppressWarnings("unchecked")
6656
public Document getOptions() {
6757
return parameters.containsKey("options")
@@ -74,46 +64,6 @@ public Document getFilter() {
7464
return new Document((Map<String, Object>) parameters.get("filter"));
7565
}
7666

77-
public String getIndexName() {
78-
Object value = parameters.get("indexName");
79-
return value != null ? (String) value : null;
80-
}
81-
82-
public String getTarget() {
83-
return (String) parameters.get("target");
84-
}
85-
86-
@SuppressWarnings("unchecked")
87-
public Document getValidator() {
88-
Object value = parameters.get("validator");
89-
return value != null ? new Document((Map<String, Object>) value) : null;
90-
}
91-
92-
public String getValidationLevel() {
93-
return (String) parameters.get("validationLevel");
94-
}
95-
96-
public String getValidationAction() {
97-
return (String) parameters.get("validationAction");
98-
}
99-
100-
public String getViewOn() {
101-
return (String) parameters.get("viewOn");
102-
}
103-
104-
@SuppressWarnings("unchecked")
105-
public List<Document> getPipeline() {
106-
List<Map<String, Object>> rawPipeline = (List<Map<String, Object>>) parameters.get("pipeline");
107-
return rawPipeline != null
108-
? rawPipeline.stream().map(Document::new).collect(Collectors.toList())
109-
: null;
110-
}
111-
112-
@SuppressWarnings("unchecked")
113-
public Document getUpdate() {
114-
return new Document((Map<String, Object>) parameters.get("update"));
115-
}
116-
11767
public boolean isMulti() {
11868
Object multi = parameters.get("multi");
11969
return multi != null && (Boolean) multi;

src/main/java/io/flamingock/template/mongodb/model/operator/CreateViewOperator.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
import com.mongodb.client.model.CreateViewOptions;
2121
import io.flamingock.template.mongodb.mapper.CreateViewOptionsMapper;
2222
import io.flamingock.template.mongodb.model.MongoOperation;
23+
import org.bson.Document;
24+
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.stream.Collectors;
2328

2429
public class CreateViewOperator extends MongoOperator {
2530

@@ -30,6 +35,18 @@ public CreateViewOperator(MongoDatabase mongoDatabase, MongoOperation operation)
3035
@Override
3136
protected void applyInternal(ClientSession clientSession) {
3237
CreateViewOptions options = CreateViewOptionsMapper.map(op.getOptions());
33-
mongoDatabase.createView(op.getCollection(), op.getViewOn(), op.getPipeline(), options);
38+
mongoDatabase.createView(op.getCollection(), getViewOn(), getPipeline(), options);
39+
}
40+
41+
private String getViewOn() {
42+
return (String) op.getParameters().get("viewOn");
43+
}
44+
45+
@SuppressWarnings("unchecked")
46+
private List<Document> getPipeline() {
47+
List<Map<String, Object>> rawPipeline = (List<Map<String, Object>>) op.getParameters().get("pipeline");
48+
return rawPipeline != null
49+
? rawPipeline.stream().map(Document::new).collect(Collectors.toList())
50+
: null;
3451
}
3552
}

src/main/java/io/flamingock/template/mongodb/model/operator/DropIndexOperator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@ public DropIndexOperator(MongoDatabase mongoDatabase, MongoOperation operation)
2727

2828
@Override
2929
protected void applyInternal(ClientSession clientSession) {
30-
String indexName = op.getIndexName();
30+
String indexName = getIndexName();
3131
if (indexName != null) {
3232
mongoDatabase.getCollection(op.getCollection()).dropIndex(indexName);
3333
} else {
3434
mongoDatabase.getCollection(op.getCollection()).dropIndex(op.getKeys());
3535
}
3636
}
37+
38+
private String getIndexName() {
39+
Object value = op.getParameters().get("indexName");
40+
return value != null ? (String) value : null;
41+
}
3742
}

0 commit comments

Comments
 (0)