Skip to content

Commit 37e23fc

Browse files
Merge pull request #50 from OrnitheMC/namespaced-identifier
add `NamespacedIdentifier` to Core API
2 parents 53cb35a + 4b93892 commit 37e23fc

File tree

8 files changed

+359
-0
lines changed

8 files changed

+359
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package net.ornithemc.osl.core.api.util;
2+
3+
/**
4+
* Namespaced identifiers are two-part strings that uniquely point to content in Minecraft.
5+
* The two parts are the namespace and the identifier. They can be combined into a single
6+
* string representation as namespace:identifier (the namespace, followed by the identifier,
7+
* separated by a colon).
8+
* <p>
9+
* A namespace is a domain for content. It is used not to point to specific content, but to
10+
* differentiate between different content sources or publishers. The use of namespaces can
11+
* prevent conflicts between mods, resource packs, or data packs, in cases where the same
12+
* identifier is used.
13+
* <p>
14+
* The identifier is a unique name for content within a namespace. It should be descriptive
15+
* to avoid naming conflicts with other content. The preferred format is snake_case.
16+
* <p>
17+
* Namespaces may only contain alphanumeric characters [a-zA-Z0-9] and special characters
18+
* [-._]. Identifiers may also contain the special character [/].
19+
*/
20+
public interface NamespacedIdentifier {
21+
22+
/**
23+
* The separator between the namespace and identifier in the {@code String}
24+
* representation of a {@code NamespacedIdentifier}.
25+
*/
26+
static char SEPARATOR = ':';
27+
28+
/**
29+
* @return the namespace of this {@code NamespacedIdentifier}.
30+
*/
31+
String namespace();
32+
33+
/**
34+
* @return the identifier of this {@code NamespacedIdentifier}.
35+
*/
36+
String identifier();
37+
38+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package net.ornithemc.osl.core.api.util;
2+
3+
import java.util.Comparator;
4+
5+
import net.ornithemc.osl.core.impl.util.NamespacedIdentifierException;
6+
import net.ornithemc.osl.core.impl.util.NamespacedIdentifierParseException;
7+
import net.ornithemc.osl.core.impl.util.NamespacedIdentifierImpl;
8+
9+
/**
10+
* Utility methods for creating and validating {@link NamespacedIdentifier}s.
11+
*/
12+
public final class NamespacedIdentifiers {
13+
14+
/**
15+
* The {@code minecraft} namespace is used for Vanilla resources and ids.
16+
*/
17+
public static final String MINECRAFT_NAMESPACE = "minecraft";
18+
/**
19+
* The default namespace of {@code NamespacedIdentifier}s.
20+
* It is recommended to use a custom namespace for your own identifiers.
21+
*/
22+
public static final String DEFAULT_NAMESPACE = MINECRAFT_NAMESPACE;
23+
24+
/**
25+
* The maximum length of a {@code NamespacedIdentifier}'s namespace string.
26+
*/
27+
public static final int MAX_LENGTH_NAMESPACE = Integer.MAX_VALUE;
28+
/**
29+
* The maximum length of a {@code NamespacedIdentifier} identifier string.
30+
*/
31+
public static final int MAX_LENGTH_IDENTIFIER = Integer.MAX_VALUE;
32+
33+
/**
34+
* A comparator for {@code NamespacedIdentifier}s, comparing first by identifier, then by namespace.
35+
*/
36+
public static final Comparator<NamespacedIdentifier> COMPARATOR = (a, b) -> {
37+
int c = a.identifier().compareTo(b.identifier());
38+
if (c == 0) {
39+
c = a.namespace().compareTo(b.namespace());
40+
}
41+
42+
return c;
43+
};
44+
45+
/**
46+
* Construct and validate a {@code NamespacedIdentifier} with the default namespace and the given identifier.
47+
*
48+
* @return a {@code NamespacedIdentifier} with the default namespace and the given identifier.
49+
* @throws NamespacedIdentifierException
50+
* if the given identifier is invalid.
51+
*/
52+
public static NamespacedIdentifier from(String identifier) {
53+
return from(DEFAULT_NAMESPACE, identifier);
54+
}
55+
56+
/**
57+
* Construct and validate a {@code NamespacedIdentifier} from the given namespace and identifier.
58+
*
59+
* @return a {@code NamespacedIdentifier} with the given namespace and identifier.
60+
* @throws NamespacedIdentifierException
61+
* if the given namespace or identifier is invalid.
62+
*/
63+
public static NamespacedIdentifier from(String namespace, String identifier) {
64+
return new NamespacedIdentifierImpl(
65+
validateNamespace(namespace),
66+
validateIdentifier(identifier)
67+
);
68+
}
69+
70+
/**
71+
* Parse a {@code NamespacedIdentifier} from the given {@code String}.
72+
* The returned identifier is always valid. If no valid identifier can
73+
* be parsed from the given string, an exception is thrown.
74+
*
75+
* @return the {@code NamespacedIdentifier}} represented by the {@code String}.
76+
* @throws NamespacedIdentifierParseException
77+
* if no valid {@code NamespacedIdentifier} can be parsed from the given {@code String}.
78+
*/
79+
public static NamespacedIdentifier parse(String s) {
80+
int i = s.indexOf(NamespacedIdentifier.SEPARATOR);
81+
82+
try {
83+
if (i < 0) {
84+
return from(s.substring(i + 1));
85+
} else if (i > 0) {
86+
return from(s.substring(0, i), s.substring(i + 1));
87+
} else {
88+
throw NamespacedIdentifierParseException.invalid(s, "badly formatted");
89+
}
90+
} catch (NamespacedIdentifierException e) {
91+
throw NamespacedIdentifierParseException.invalid(s, e);
92+
}
93+
}
94+
95+
/**
96+
* Check whether the given {@code NamespacedIdentifier} is valid, or throw an exception.
97+
*/
98+
public static NamespacedIdentifier validate(NamespacedIdentifier id) {
99+
try {
100+
validateNamespace(id.namespace());
101+
validateIdentifier(id.identifier());
102+
103+
return id;
104+
} catch (NamespacedIdentifierException e) {
105+
throw NamespacedIdentifierException.invalid(id, e);
106+
}
107+
}
108+
109+
/**
110+
* Check that the given namespace is valid for a {@code NamespacedIdentifier}.
111+
*/
112+
public static String validateNamespace(String namespace) {
113+
if (namespace == null || namespace.isEmpty()) {
114+
throw NamespacedIdentifierException.invalidNamespace(namespace, "null or empty");
115+
}
116+
if (namespace.length() > MAX_LENGTH_NAMESPACE) {
117+
throw NamespacedIdentifierException.invalidNamespace(namespace, "length " + namespace.length() + " is greater than maximum allowed " + MAX_LENGTH_NAMESPACE);
118+
}
119+
if (!namespace.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) {
120+
throw NamespacedIdentifierException.invalidNamespace(namespace, "contains illegal characters - only [a-zA-Z0-9-._] are allowed");
121+
}
122+
123+
return namespace;
124+
}
125+
126+
/**
127+
* Check that the given identifier is valid for a {@code NamespacedIdentifier}.
128+
*/
129+
public static String validateIdentifier(String identifier) {
130+
if (identifier == null || identifier.isEmpty()) {
131+
throw NamespacedIdentifierException.invalidIdentifier(identifier, "null or empty");
132+
}
133+
if (identifier.length() > MAX_LENGTH_IDENTIFIER) {
134+
throw NamespacedIdentifierException.invalidIdentifier(identifier, "length " + identifier.length() + " is greater than maximum allowed " + MAX_LENGTH_IDENTIFIER);
135+
}
136+
if (!identifier.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || chr == '/' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) {
137+
throw NamespacedIdentifierException.invalidIdentifier(identifier, "contains illegal characters - only [a-zA-Z0-9-._/] are allowed");
138+
}
139+
140+
return identifier;
141+
}
142+
143+
public static boolean equals(NamespacedIdentifier a, NamespacedIdentifier b) {
144+
return a.namespace().equals(b.namespace()) && a.identifier().equals(b.identifier());
145+
}
146+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package net.ornithemc.osl.core.impl.mixin;
2+
3+
import org.spongepowered.asm.mixin.Mixin;
4+
import org.spongepowered.asm.mixin.Pseudo;
5+
import org.spongepowered.asm.mixin.Shadow;
6+
import org.spongepowered.asm.mixin.injection.At;
7+
import org.spongepowered.asm.mixin.injection.Inject;
8+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
9+
10+
import net.minecraft.client.resource.Identifier;
11+
12+
import net.ornithemc.osl.core.api.util.NamespacedIdentifier;
13+
import net.ornithemc.osl.core.api.util.NamespacedIdentifiers;
14+
15+
@Pseudo // needed because Identifier does not exist in all versions
16+
@Mixin(Identifier.class)
17+
public class IdentifierMixin implements NamespacedIdentifier { // TODO: interface injection
18+
19+
@Shadow
20+
private String namespace;
21+
@Shadow
22+
private String path;
23+
24+
@Inject(
25+
method = "equals",
26+
remap = false,
27+
cancellable = true,
28+
at = @At(
29+
value = "HEAD"
30+
)
31+
)
32+
private void osl$core$equalsNamespacedIdentifier(Object o, CallbackInfoReturnable<Boolean> cir) {
33+
if (o instanceof NamespacedIdentifier) {
34+
cir.setReturnValue(NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o));
35+
}
36+
}
37+
38+
@Override
39+
public String namespace() {
40+
return namespace;
41+
}
42+
43+
@Override
44+
public String identifier() {
45+
return path;
46+
}
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.ornithemc.osl.core.impl.util;
2+
3+
import net.ornithemc.osl.core.api.util.NamespacedIdentifier;
4+
5+
@SuppressWarnings("serial")
6+
public class NamespacedIdentifierException extends RuntimeException {
7+
8+
private NamespacedIdentifierException(String message) {
9+
super(message);
10+
}
11+
12+
private NamespacedIdentifierException(String message, Throwable cause) {
13+
super(message, cause);
14+
}
15+
16+
public static NamespacedIdentifierException invalid(NamespacedIdentifier id, Throwable cause) {
17+
return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier", cause);
18+
}
19+
20+
public static NamespacedIdentifierException invalid(NamespacedIdentifier id, String reason) {
21+
return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier: " + reason);
22+
}
23+
24+
public static NamespacedIdentifierException invalidNamespace(String namespace, String reason) {
25+
return new NamespacedIdentifierException("\'" + namespace + "\' is not a valid namespace: " + reason);
26+
}
27+
28+
public static NamespacedIdentifierException invalidIdentifier(String identifier, String reason) {
29+
return new NamespacedIdentifierException("\'" + identifier + "\' is not a valid identifier: " + reason);
30+
}
31+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package net.ornithemc.osl.core.impl.util;
2+
3+
import net.ornithemc.osl.core.api.util.NamespacedIdentifier;
4+
import net.ornithemc.osl.core.api.util.NamespacedIdentifiers;
5+
6+
/**
7+
* This class is a version-agnostic implementation of {@link NamespacedIdentifier}.
8+
* <p>
9+
* This class is essentially equivalent to Vanilla's {@code Identifier}. It was added for a
10+
* few reasons. For one, Vanilla's {@code Identifier} was only added in 13w21a, and then was
11+
* client-only until 14w27b. Implementation details of this class also changed a few times,
12+
* and only since 17w43a were {@code Identifiers} validated in any way.
13+
* <br> This class is available for all Minecraft versions, without any version-specific
14+
* implementation details.
15+
*/
16+
public final class NamespacedIdentifierImpl implements NamespacedIdentifier {
17+
18+
private final String namespace;
19+
private final String identifier;
20+
21+
public NamespacedIdentifierImpl(String namespace, String identifier) {
22+
this.namespace = namespace;
23+
this.identifier = identifier;
24+
}
25+
26+
@Override
27+
public boolean equals(Object o) {
28+
if (this == o) {
29+
return true;
30+
}
31+
if (!(o instanceof NamespacedIdentifier)) {
32+
return false;
33+
}
34+
return NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o);
35+
}
36+
37+
@Override
38+
public int hashCode() {
39+
// this impl matches Vanilla Identifier's impl
40+
return 31 * namespace.hashCode() + identifier.hashCode();
41+
}
42+
43+
@Override
44+
public String toString() {
45+
return namespace + SEPARATOR + identifier;
46+
}
47+
48+
@Override
49+
public String namespace() {
50+
return namespace;
51+
}
52+
53+
@Override
54+
public String identifier() {
55+
return identifier;
56+
}
57+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.ornithemc.osl.core.impl.util;
2+
3+
@SuppressWarnings("serial")
4+
public class NamespacedIdentifierParseException extends RuntimeException {
5+
6+
private NamespacedIdentifierParseException(String message) {
7+
super(message);
8+
}
9+
10+
private NamespacedIdentifierParseException(String message, Throwable cause) {
11+
super(message, cause);
12+
}
13+
14+
public static NamespacedIdentifierParseException invalid(String s, Throwable cause) {
15+
return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\'", cause);
16+
}
17+
18+
public static NamespacedIdentifierParseException invalid(String s, String reason) {
19+
return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\': " + reason);
20+
}
21+
}

libraries/core/src/main/resources/fabric.mod.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"license": "Apache-2.0",
1717
"icon": "assets/ornithe-standard-libraries/core/icon.png",
1818
"environment": "*",
19+
"mixins": [
20+
"osl.core.mixins.json"
21+
],
1922
"depends": {
2023
"fabricloader": ">=0.16.0"
2124
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"required": true,
3+
"minVersion": "0.8",
4+
"package": "net.ornithemc.osl.core.impl.mixin",
5+
"compatibilityLevel": "JAVA_8",
6+
"mixins": [
7+
"IdentifierMixin"
8+
],
9+
"client": [
10+
],
11+
"server": [
12+
],
13+
"injectors": {
14+
"defaultRequire": 1
15+
}
16+
}

0 commit comments

Comments
 (0)