Establish buildable modules and shared host-service boundaries

This commit is contained in:
2026-11-12 07:49:06 +01:00
parent 1326b18746
commit 306266ae40
41 changed files with 1980 additions and 23 deletions
@@ -0,0 +1,14 @@
package de.neitzel.fxdesigner.contracts;
import java.net.URI;
import java.util.Objects;
/** Immutable host-owned draft; revision ordering is independent of native undo stamps. */
public record DocumentSnapshot(
URI document, long revision, String text, boolean writable, boolean dirty) {
public DocumentSnapshot {
Objects.requireNonNull(document);
Objects.requireNonNull(text);
if (revision < 0) throw new IllegalArgumentException("Negative revision");
}
}
@@ -0,0 +1,40 @@
package de.neitzel.fxdesigner.contracts;
import java.util.*;
/** One named undo transaction. All UTF-16 ranges address the same expected revision. */
public record EditBatch(long expectedRevision, String name, List<Replacement> replacements) {
public record Replacement(int start, int end, String text) {
public Replacement {
if (start < 0 || end < start) throw new IllegalArgumentException("Invalid range");
Objects.requireNonNull(text);
}
}
public EditBatch {
if (expectedRevision < 0) throw new IllegalArgumentException("Negative revision");
if (name == null || name.isBlank()) throw new IllegalArgumentException("Missing command name");
replacements = List.copyOf(replacements);
}
/** Validates every range before computing a result; never partially edits a host draft. */
public String applyTo(DocumentSnapshot draft) {
if (!draft.writable()) throw new IllegalStateException("Document is read-only");
if (draft.revision() != expectedRevision)
throw new ConcurrentModificationException("Stale document revision");
var ordered = new ArrayList<>(replacements);
ordered.sort(Comparator.comparingInt(Replacement::start));
int previousEnd = -1;
for (var r : ordered) {
if (r.end() > draft.text().length() || r.start() < previousEnd)
throw new IllegalArgumentException("Overlapping/out-of-bounds edits");
previousEnd = r.end();
}
var result = new StringBuilder(draft.text());
for (int i = ordered.size() - 1; i >= 0; i--) {
var r = ordered.get(i);
result.replace(r.start(), r.end(), r.text());
}
return result.toString();
}
}
@@ -0,0 +1,37 @@
package de.neitzel.fxdesigner.contracts;
import static org.junit.jupiter.api.Assertions.*;
import java.net.URI;
import java.util.*;
import org.junit.jupiter.api.Test;
class EditBatchTest {
private DocumentSnapshot draft(long r, boolean writable) {
return new DocumentSnapshot(URI.create("file:/sample.fxml"), r, "abcdef", writable, false);
}
@Test
void multipleRangesUseOriginalOffsets() {
var b =
new EditBatch(
4,
"Rename",
List.of(new EditBatch.Replacement(1, 2, "LONG"), new EditBatch.Replacement(4, 6, "!")));
assertEquals("aLONGcd!", b.applyTo(draft(4, true)));
}
@Test
void rejectsStaleReadOnlyAndOverlappingTransactions() {
var b = new EditBatch(3, "Change", List.of(new EditBatch.Replacement(0, 2, "x")));
assertThrows(ConcurrentModificationException.class, () -> b.applyTo(draft(4, true)));
assertThrows(IllegalStateException.class, () -> b.applyTo(draft(3, false)));
var overlap =
new EditBatch(
3,
"Bad",
List.of(new EditBatch.Replacement(1, 4, ""), new EditBatch.Replacement(3, 5, "")));
assertThrows(IllegalArgumentException.class, () -> overlap.applyTo(draft(3, true)));
assertEquals("abcdef", draft(3, true).text());
}
}