Skip to content

Commit 4829d5b

Browse files
committed
Add NBTSchemaTest for validation API
1 parent baf1e5d commit 4829d5b

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2026 Glavo
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 org.glavo.nbt.validation;
17+
18+
import org.glavo.nbt.internal.schema.CompoundTagSchema;
19+
import org.glavo.nbt.tag.CompoundTag;
20+
import org.glavo.nbt.tag.IntTag;
21+
import org.glavo.nbt.tag.StringTag;
22+
import org.glavo.nbt.tag.Tag;
23+
import org.glavo.nbt.tag.TagType;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.lang.reflect.InvocationTargetException;
27+
28+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertFalse;
31+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
32+
import static org.junit.jupiter.api.Assertions.assertSame;
33+
import static org.junit.jupiter.api.Assertions.assertThrows;
34+
import static org.junit.jupiter.api.Assertions.assertTrue;
35+
36+
@SuppressWarnings("ThrowableNotThrown")
37+
final class NBTSchemaTest {
38+
39+
private static <T extends Tag> void assertAccepts(NBTSchema<T> schema, T tag) {
40+
assertTrue(schema.test(tag));
41+
assertDoesNotThrow(() -> schema.validate(tag));
42+
}
43+
44+
private static <T extends Tag> NBTValidationException assertRejects(NBTSchema<T> schema, T tag) {
45+
assertFalse(schema.test(tag));
46+
return assertThrows(NBTValidationException.class, () -> schema.validate(tag));
47+
}
48+
49+
private static <T extends Tag> NBTSchema<T> invokeOr(NBTSchema<T> left, NBTSchema<? extends T> right) {
50+
try {
51+
@SuppressWarnings("unchecked")
52+
NBTSchema<T> schema = (NBTSchema<T>) NBTSchema.class.getMethod("or", NBTSchema.class).invoke(left, right);
53+
return schema;
54+
} catch (ReflectiveOperationException e) {
55+
throw new AssertionError(e);
56+
}
57+
}
58+
59+
private static <T extends Tag> NBTSchema<T> invokeAnd(NBTSchema<T> left, NBTSchema<? extends T> right) {
60+
try {
61+
@SuppressWarnings("unchecked")
62+
NBTSchema<T> schema = (NBTSchema<T>) NBTSchema.class.getMethod("and", NBTSchema.class).invoke(left, right);
63+
return schema;
64+
} catch (ReflectiveOperationException e) {
65+
throw new AssertionError(e);
66+
}
67+
}
68+
69+
private static <T extends Tag> void assertOrRejectsNull(NBTSchema<T> schema) {
70+
InvocationTargetException exception = assertThrows(InvocationTargetException.class,
71+
() -> NBTSchema.class.getMethod("or", NBTSchema.class).invoke(schema, new Object[]{null}));
72+
assertInstanceOf(NullPointerException.class, exception.getCause());
73+
}
74+
75+
private static <T extends Tag> void assertAndRejectsNull(NBTSchema<T> schema) {
76+
InvocationTargetException exception = assertThrows(InvocationTargetException.class,
77+
() -> NBTSchema.class.getMethod("and", NBTSchema.class).invoke(schema, new Object[]{null}));
78+
assertInstanceOf(NullPointerException.class, exception.getCause());
79+
}
80+
81+
@Test
82+
void testTypeIs() {
83+
NBTSchema<StringTag> schema = NBTSchema.typeIs(TagType.STRING);
84+
85+
assertAccepts(schema, new StringTag("Hello"));
86+
assertAccepts(schema, new StringTag("Hello").setName("value"));
87+
}
88+
89+
@Test
90+
void testTypeIsRejectsWrongType() {
91+
NBTSchema<StringTag> schema = NBTSchema.typeIs(TagType.STRING);
92+
93+
@SuppressWarnings("unchecked")
94+
NBTSchema<Tag> rawSchema = (NBTSchema<Tag>) (NBTSchema<?>) schema;
95+
96+
NBTValidationException exception = assertRejects(rawSchema, new IntTag(42));
97+
assertEquals("Expected TAG_String, but got TAG_Int", exception.getMessage());
98+
}
99+
100+
@Test
101+
void testMatchScalarUsesEqualsAndClonesExpectedTag() {
102+
IntTag expected = new IntTag(42).setName("score");
103+
NBTSchema<IntTag> schema = NBTSchema.match(expected);
104+
105+
expected.set(99).setName("changed");
106+
107+
assertAccepts(schema, new IntTag(42).setName("score"));
108+
assertRejects(schema, new IntTag(42).setName("other"));
109+
assertRejects(schema, new IntTag(99).setName("score"));
110+
}
111+
112+
@Test
113+
void testMatchCompoundUsesSubsetMatchingAndClonesExpectedTag() {
114+
CompoundTag expected = new CompoundTag()
115+
.setString("name", "Alex")
116+
.addTag("stats", new CompoundTag().setInt("score", 42));
117+
118+
NBTSchema<CompoundTag> schema = NBTSchema.match(expected);
119+
120+
expected.setString("name", "Steve");
121+
CompoundTag stats = assertInstanceOf(CompoundTag.class, expected.get("stats"));
122+
stats.setInt("score", 0);
123+
124+
CompoundTag actual = new CompoundTag()
125+
.setString("name", "Alex")
126+
.setString("title", "Hero")
127+
.addTag("stats", new CompoundTag()
128+
.setInt("score", 42)
129+
.setInt("level", 7));
130+
131+
assertAccepts(schema, actual);
132+
assertRejects(schema, new CompoundTag()
133+
.setString("name", "Alex")
134+
.addTag("stats", new CompoundTag().setInt("score", 1)));
135+
assertRejects(schema, new CompoundTag().setString("name", "Alex"));
136+
}
137+
138+
@Test
139+
void testUnionAndOr() {
140+
NBTSchema<StringTag> stringSchema = NBTSchema.typeIs(TagType.STRING);
141+
NBTSchema<IntTag> intSchema = NBTSchema.typeIs(TagType.INT);
142+
NBTSchema<StringTag> helloSchema = NBTSchema.match(new StringTag("hello"));
143+
NBTSchema<StringTag> worldSchema = NBTSchema.match(new StringTag("world"));
144+
145+
assertSame(stringSchema, NBTSchema.union(stringSchema));
146+
assertThrows(IllegalArgumentException.class, NBTSchema::union);
147+
assertOrRejectsNull(helloSchema);
148+
149+
NBTSchema<Tag> union = NBTSchema.union(stringSchema, intSchema);
150+
NBTSchema<StringTag> orSchema = invokeOr(helloSchema, worldSchema);
151+
152+
assertAccepts(union, new StringTag("hello"));
153+
assertAccepts(union, new IntTag(42));
154+
assertAccepts(orSchema, new StringTag("hello"));
155+
assertAccepts(orSchema, new StringTag("world"));
156+
assertRejects(orSchema, new StringTag("other"));
157+
158+
CompoundTag actual = new CompoundTag();
159+
NBTValidationException exception = assertRejects(union, actual);
160+
assertEquals("No schema matched", exception.getMessage());
161+
assertEquals(2, exception.getSuppressed().length);
162+
assertEquals("Expected TAG_String, but got TAG_Compound", exception.getSuppressed()[0].getMessage());
163+
assertEquals("Expected TAG_Int, but got TAG_Compound", exception.getSuppressed()[1].getMessage());
164+
}
165+
166+
@Test
167+
void testIntersectionAndAnd() {
168+
NBTSchema<CompoundTag> hasName = NBTSchema.match(new CompoundTag().setString("name", "Alex"));
169+
NBTSchema<CompoundTag> hasScore = NBTSchema.match(new CompoundTag().setInt("score", 42));
170+
171+
assertSame(hasName, NBTSchema.intersection(hasName));
172+
assertThrows(IllegalArgumentException.class, NBTSchema::intersection);
173+
assertAndRejectsNull(hasName);
174+
175+
NBTSchema<CompoundTag> intersection = NBTSchema.intersection(hasName, hasScore);
176+
NBTSchema<CompoundTag> andSchema = invokeAnd(hasName, hasScore);
177+
178+
CompoundTag valid = new CompoundTag().setString("name", "Alex").setInt("score", 42).setInt("level", 7);
179+
assertAccepts(intersection, valid);
180+
assertAccepts(andSchema, valid);
181+
182+
NBTValidationException exception = assertRejects(intersection, new CompoundTag().setString("name", "Alex"));
183+
assertTrue(exception.getMessage().startsWith("Failed to validate tag against schema: "));
184+
NBTValidationException cause = assertInstanceOf(NBTValidationException.class, exception.getCause());
185+
assertEquals("Tag does not match the expected schema: " + new CompoundTag().setInt("score", 42), cause.getMessage());
186+
}
187+
188+
@Test
189+
void testCompoundTagBuilderWithRequiredOptionalAndNestedSchemas() {
190+
CompoundTag profilePattern = new CompoundTag().setString("role", "admin");
191+
192+
NBTSchema<CompoundTag> profileSchema = new CompoundTagSchema.Builder()
193+
.addRequired("id", TagType.INT)
194+
.addOptional("nickname", TagType.STRING)
195+
.addRequired("meta", profilePattern)
196+
.build();
197+
198+
profilePattern.setString("role", "guest");
199+
200+
NBTSchema<CompoundTag> rootSchema = new CompoundTagSchema.Builder()
201+
.addRequired("name", TagType.STRING)
202+
.addOptional("age", TagType.INT)
203+
.addRequired("profile", profileSchema)
204+
.build();
205+
206+
CompoundTag valid = new CompoundTag()
207+
.setString("name", "Alex")
208+
.setInt("age", 20)
209+
.addTag("profile", new CompoundTag()
210+
.setInt("id", 7)
211+
.setString("nickname", "builder")
212+
.addTag("meta", new CompoundTag()
213+
.setString("role", "admin")
214+
.setString("extra", "ok")));
215+
216+
assertAccepts(rootSchema, valid);
217+
218+
NBTValidationException missingRequired = assertRejects(rootSchema, new CompoundTag()
219+
.addTag("profile", new CompoundTag()
220+
.setInt("id", 7)
221+
.addTag("meta", new CompoundTag().setString("role", "admin"))));
222+
assertEquals("Missing required subtag: name", missingRequired.getMessage());
223+
224+
NBTValidationException invalidNested = assertRejects(rootSchema, new CompoundTag()
225+
.setString("name", "Alex")
226+
.addTag("profile", new CompoundTag()
227+
.setInt("id", 7)
228+
.addTag("meta", new CompoundTag().setString("role", "guest"))));
229+
assertEquals("Invalid subtag profile", invalidNested.getMessage());
230+
NBTValidationException nestedCause = assertInstanceOf(NBTValidationException.class, invalidNested.getCause());
231+
assertEquals("Invalid subtag meta", nestedCause.getMessage());
232+
NBTValidationException leafCause = assertInstanceOf(NBTValidationException.class, nestedCause.getCause());
233+
assertEquals("Tag does not match the expected schema: " + new CompoundTag().setString("role", "admin"), leafCause.getMessage());
234+
}
235+
}

0 commit comments

Comments
 (0)