Skip to content

Commit cc71a10

Browse files
committed
Fixed up tag immutability, values, made timestamp comparable and consistency issues with owner.
1 parent a8d85f4 commit cc71a10

File tree

6 files changed

+100
-31
lines changed

6 files changed

+100
-31
lines changed

common/src/main/java/com/codeheadsystems/motif/model/Event.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ public record Event(Owner owner,
2020
}
2121
identifier = Objects.requireNonNullElseGet(identifier, Identifier::new);
2222
timestamp = Objects.requireNonNullElse(timestamp, new Timestamp());
23-
tags = Objects.requireNonNullElseGet(tags, List::of);
23+
tags = tags == null ? List.of() : List.copyOf(tags);
24+
if (!owner.equals(subject.owner())) {
25+
throw new IllegalArgumentException("owner must match subject's owner");
26+
}
2427
}
2528

2629
public static Builder from(Event event) {

common/src/main/java/com/codeheadsystems/motif/model/Note.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
public record Note(Owner owner,
88
Subject subject,
9-
@Nullable String value,
9+
String value,
1010
@Nullable List<Tag> tags,
1111
@Nullable Identifier identifier,
1212
@Nullable Event event,
@@ -15,11 +15,17 @@ public record Note(Owner owner,
1515
public Note {
1616
Objects.requireNonNull(owner, "owner cannot be null");
1717
Objects.requireNonNull(subject, "subject cannot be null");
18-
value = Objects.requireNonNullElse(value, "").strip();
18+
value = Objects.requireNonNull(value, "value cannot be null").strip();
19+
if (value.isEmpty()) {
20+
throw new IllegalArgumentException("value cannot be empty");
21+
}
1922
if (value.length() > 4096) {
2023
throw new IllegalArgumentException("value cannot be longer than 4096 characters");
2124
}
22-
tags = Objects.requireNonNullElseGet(tags, List::of);
25+
tags = tags == null ? List.of() : List.copyOf(tags);
26+
if (!owner.equals(subject.owner())) {
27+
throw new IllegalArgumentException("owner must match subject's owner");
28+
}
2329
identifier = Objects.requireNonNullElseGet(identifier, Identifier::new);
2430
timestamp = Objects.requireNonNullElse(timestamp, new Timestamp());
2531
}
@@ -28,40 +34,35 @@ public static Builder from(Note note) {
2834
return Builder.from(note);
2935
}
3036

31-
public static Builder builder(Owner owner, Subject subject) {
32-
return new Builder(owner, subject);
37+
public static Builder builder(Owner owner, Subject subject, String value) {
38+
return new Builder(owner, subject, value);
3339
}
3440

3541
public static class Builder {
3642
private final Owner owner;
3743
private final Subject subject;
38-
private String value;
44+
private final String value;
3945
private List<Tag> tags;
4046
private Identifier identifier;
4147
private Event event;
4248
private Timestamp timestamp;
4349

44-
private Builder(Owner owner, Subject subject) {
50+
private Builder(Owner owner, Subject subject, String value) {
4551
this.owner = Objects.requireNonNull(owner, "owner cannot be null");
4652
this.subject = Objects.requireNonNull(subject, "subject cannot be null");
53+
this.value = Objects.requireNonNull(value, "value cannot be null");
4754
}
4855

4956
private static Builder from(Note note) {
5057
Objects.requireNonNull(note, "note cannot be null");
51-
Builder builder = new Builder(note.owner(), note.subject());
52-
builder.value = note.value();
58+
Builder builder = new Builder(note.owner(), note.subject(), note.value());
5359
builder.tags = note.tags();
5460
builder.identifier = note.identifier();
5561
builder.event = note.event();
5662
builder.timestamp = note.timestamp();
5763
return builder;
5864
}
5965

60-
public Builder value(String value) {
61-
this.value = value;
62-
return this;
63-
}
64-
6566
public Builder tags(List<Tag> tags) {
6667
this.tags = tags;
6768
return this;

common/src/main/java/com/codeheadsystems/motif/model/Timestamp.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
* @param timestamp The instant.
1414
*/
15-
public record Timestamp(Instant timestamp) {
15+
public record Timestamp(Instant timestamp) implements Comparable<Timestamp> {
1616

1717
public Timestamp {
1818
if (timestamp == null) {
@@ -53,4 +53,9 @@ public OffsetDateTime toOffsetDateTime() {
5353
return timestamp.atOffset(ZoneOffset.UTC);
5454
}
5555

56+
@Override
57+
public int compareTo(Timestamp other) {
58+
return this.timestamp.compareTo(other.timestamp);
59+
}
60+
5661
}

common/src/test/java/com/codeheadsystems/motif/model/EventTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55

66
import java.time.Instant;
7+
import java.util.ArrayList;
78
import java.util.List;
89
import org.junit.jupiter.api.Test;
910

@@ -112,6 +113,24 @@ void constructorPreservesProvidedTags() {
112113
assertThat(event.tags()).containsExactly(new Tag("A"), new Tag("B"));
113114
}
114115

116+
@Test
117+
void constructorMakesDefensiveCopyOfTags() {
118+
ArrayList<Tag> mutableTags = new ArrayList<>(List.of(new Tag("A")));
119+
120+
Event event = new Event(OWNER, SUBJECT, "value", null, null, mutableTags);
121+
mutableTags.add(new Tag("B"));
122+
123+
assertThat(event.tags()).containsExactly(new Tag("A"));
124+
}
125+
126+
@Test
127+
void constructorRejectsOwnerMismatchWithSubject() {
128+
Owner differentOwner = new Owner("DIFFERENT");
129+
assertThatThrownBy(() -> new Event(differentOwner, SUBJECT, "value", null, null, null))
130+
.isInstanceOf(IllegalArgumentException.class)
131+
.hasMessage("owner must match subject's owner");
132+
}
133+
115134
// --- Builder.from tests ---
116135

117136
@Test

common/src/test/java/com/codeheadsystems/motif/model/NoteTest.java

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55

66
import java.time.Instant;
7+
import java.util.ArrayList;
78
import java.util.List;
89
import org.junit.jupiter.api.Test;
910

@@ -30,10 +31,24 @@ void constructorRejectsNullSubject() {
3031
}
3132

3233
@Test
33-
void constructorDefaultsNullValueToEmpty() {
34-
Note note = new Note(OWNER, SUBJECT, null, null, null, null, null);
34+
void constructorRejectsNullValue() {
35+
assertThatThrownBy(() -> new Note(OWNER, SUBJECT, null, null, null, null, null))
36+
.isInstanceOf(NullPointerException.class)
37+
.hasMessage("value cannot be null");
38+
}
39+
40+
@Test
41+
void constructorRejectsEmptyValue() {
42+
assertThatThrownBy(() -> new Note(OWNER, SUBJECT, "", null, null, null, null))
43+
.isInstanceOf(IllegalArgumentException.class)
44+
.hasMessage("value cannot be empty");
45+
}
3546

36-
assertThat(note.value()).isEmpty();
47+
@Test
48+
void constructorRejectsBlankValue() {
49+
assertThatThrownBy(() -> new Note(OWNER, SUBJECT, " ", null, null, null, null))
50+
.isInstanceOf(IllegalArgumentException.class)
51+
.hasMessage("value cannot be empty");
3752
}
3853

3954
@Test
@@ -77,6 +92,16 @@ void constructorPreservesProvidedTags() {
7792
assertThat(note.tags()).containsExactly(new Tag("A"), new Tag("B"));
7893
}
7994

95+
@Test
96+
void constructorMakesDefensiveCopyOfTags() {
97+
ArrayList<Tag> mutableTags = new ArrayList<>(List.of(new Tag("A")));
98+
99+
Note note = new Note(OWNER, SUBJECT, "value", mutableTags, null, null, null);
100+
mutableTags.add(new Tag("B"));
101+
102+
assertThat(note.tags()).containsExactly(new Tag("A"));
103+
}
104+
80105
@Test
81106
void constructorDefaultsIdentifierWhenNull() {
82107
Note note = new Note(OWNER, SUBJECT, "value", null, null, null, null);
@@ -129,6 +154,14 @@ void constructorPreservesProvidedEvent() {
129154
assertThat(note.event()).isSameAs(event);
130155
}
131156

157+
@Test
158+
void constructorRejectsOwnerMismatchWithSubject() {
159+
Owner differentOwner = new Owner("DIFFERENT");
160+
assertThatThrownBy(() -> new Note(differentOwner, SUBJECT, "value", null, null, null, null))
161+
.isInstanceOf(IllegalArgumentException.class)
162+
.hasMessage("owner must match subject's owner");
163+
}
164+
132165
// --- Builder.from tests ---
133166

134167
@Test
@@ -153,48 +186,47 @@ void fromCopiesAllFields() {
153186

154187
@Test
155188
void fromAllowsOverridingFields() {
156-
Note original = Note.builder(OWNER, SUBJECT).value("original").build();
189+
Note original = Note.builder(OWNER, SUBJECT, "original").build();
157190

158191
Event event = Event.builder(OWNER, SUBJECT, "event-value").build();
159192
Note modified = Note.from(original)
160193
.event(event)
161-
.value("modified")
162194
.build();
163195

164196
assertThat(modified.owner()).isEqualTo(original.owner());
165197
assertThat(modified.subject()).isEqualTo(original.subject());
166198
assertThat(modified.identifier()).isEqualTo(original.identifier());
167199
assertThat(modified.timestamp()).isEqualTo(original.timestamp());
168-
assertThat(modified.value()).isEqualTo("modified");
200+
assertThat(modified.value()).isEqualTo("original");
169201
assertThat(modified.event()).isSameAs(event);
170202
}
171203

172204
// --- Builder tests ---
173205

174206
@Test
175207
void builderRejectsNullOwner() {
176-
assertThatThrownBy(() -> Note.builder(null, SUBJECT))
208+
assertThatThrownBy(() -> Note.builder(null, SUBJECT, "value"))
177209
.isInstanceOf(NullPointerException.class)
178210
.hasMessage("owner cannot be null");
179211
}
180212

181213
@Test
182214
void builderRejectsNullSubject() {
183-
assertThatThrownBy(() -> Note.builder(OWNER, null))
215+
assertThatThrownBy(() -> Note.builder(OWNER, null, "value"))
184216
.isInstanceOf(NullPointerException.class)
185217
.hasMessage("subject cannot be null");
186218
}
187219

188220
@Test
189-
void builderDefaultsNullValueToEmpty() {
190-
Note note = Note.builder(OWNER, SUBJECT).build();
191-
192-
assertThat(note.value()).isEmpty();
221+
void builderRejectsNullValue() {
222+
assertThatThrownBy(() -> Note.builder(OWNER, SUBJECT, null))
223+
.isInstanceOf(NullPointerException.class)
224+
.hasMessage("value cannot be null");
193225
}
194226

195227
@Test
196228
void builderWithDefaults() {
197-
Note note = Note.builder(OWNER, SUBJECT).value("note-value").build();
229+
Note note = Note.builder(OWNER, SUBJECT, "note-value").build();
198230

199231
assertThat(note.owner()).isEqualTo(OWNER);
200232
assertThat(note.subject()).isEqualTo(SUBJECT);
@@ -212,8 +244,7 @@ void builderWithAllFields() {
212244
List<Tag> tags = List.of(new Tag("X"));
213245
Event event = Event.builder(OWNER, SUBJECT, "event-value").build();
214246

215-
Note note = Note.builder(OWNER, SUBJECT)
216-
.value("note-value")
247+
Note note = Note.builder(OWNER, SUBJECT, "note-value")
217248
.tags(tags)
218249
.identifier(id)
219250
.event(event)

common/src/test/java/com/codeheadsystems/motif/model/TimestampTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,14 @@ void fromIsoHandlesOffsetFormat() {
6262

6363
assertThat(timestamp.timestamp()).isEqualTo(Instant.parse("2026-03-28T12:30:00Z"));
6464
}
65+
66+
@Test
67+
void compareToOrdersByInstant() {
68+
Timestamp earlier = new Timestamp(Instant.parse("2026-01-01T00:00:00Z"));
69+
Timestamp later = new Timestamp(Instant.parse("2026-06-01T00:00:00Z"));
70+
71+
assertThat(earlier).isLessThan(later);
72+
assertThat(later).isGreaterThan(earlier);
73+
assertThat(earlier.compareTo(new Timestamp(earlier.timestamp()))).isZero();
74+
}
6575
}

0 commit comments

Comments
 (0)