Skip to content

Commit cf9c88b

Browse files
authored
custom converters (#23)
* custom converters * readme
1 parent 297d671 commit cf9c88b

File tree

3 files changed

+154
-5
lines changed

3 files changed

+154
-5
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ struct Message:
99
contents: string
1010
sender: offline player
1111
const timestamp: date = now
12+
converts to:
13+
string via this->contents
1214
```
1315
A template's name and field names are case-insensitive. The template name follows the same rules as a function name, while field names can only consist of letters, underscores, and spaces.
1416
Each field has a name and a type, as well as an optional default value. This default value may be an expression, as it is evaluated when a struct is created, not when the template is registered.
1517
Adding `const` or `constant` to the start will prevent the field from being changed after creation.
1618

19+
Structs also support conversion expressions, which allow you to define how a struct can be converted to other types. The syntax is
20+
`<type> via <expression>`, where `<type>` is the type to convert to and `<expression>` is an expression that evaluates to that type.
21+
The expression may use `this` to refer to the struct being converted. Multiple conversions can be defined by adding more lines under the `converts to:` section.
22+
1723
Creating a struct involves a simple expression:
1824
```
1925
set {_a} to a message struct
@@ -31,6 +37,13 @@ set {_a} to a message struct:
3137
contents: "hello world"
3238
```
3339

40+
### Custom Types
41+
42+
Struct templates automatically create a new Skript type using the template's name + `struct`.
43+
In the above example, `message struct` is now a valid Skript type that can be used in function parameters and other struct fields.
44+
**You should be very careful when reloading templates, as any existing code that used the type may break if the template was modified or removed.**
45+
ALWAYS reload all scripts after modifying templates to ensure all code is properly re-parsed.
46+
3447
### Type Safety
3548
oopsk attempts to ensure type safety at parse time via checking all fields with the given name. This means having unique field names across structs allows oopsk to give you more accurate parse errors, while sharing field names can result in invalid code not causing any errors during parsing.
3649
Any type violations not caught during parsing should be caught at runtime via runtime errors that cannot be suppressed. Note that code parsed in one script prior to updates to a struct in another script will not show parse errors until it is reloaded again, though it should properly emit runtime errors.

src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ch.njol.skript.classes.ClassInfo;
55
import ch.njol.skript.localization.Language;
66
import ch.njol.skript.registrations.Classes;
7+
import ch.njol.util.Pair;
78
import com.sovdee.oopsk.core.Struct;
89
import org.skriptlang.skript.lang.converter.Converter;
910
import org.skriptlang.skript.lang.converter.ConverterInfo;
@@ -16,6 +17,7 @@
1617
import java.util.HashMap;
1718
import java.util.List;
1819
import java.util.Locale;
20+
import java.util.Map;
1921

2022
public class ReflectionUtils {
2123

@@ -26,6 +28,7 @@ public class ReflectionUtils {
2628
private static final Field localizedLanguageField;
2729
private static final Field classInfosField;
2830
private static final Field convertersField;
31+
private static final Field quickAccessConvertersField;
2932
private static final Method sortClassInfosMethod;
3033

3134
static {
@@ -38,6 +41,7 @@ public class ReflectionUtils {
3841
localizedLanguageField = Language.class.getDeclaredField("localizedLanguage");
3942
classInfosField = Classes.class.getDeclaredField("classInfos");
4043
convertersField = Converters.class.getDeclaredField("CONVERTERS");
44+
quickAccessConvertersField = Converters.class.getDeclaredField("QUICK_ACCESS_CONVERTERS");
4145

4246
// Get the method
4347
sortClassInfosMethod = Classes.class.getDeclaredMethod("sortClassInfos");
@@ -51,6 +55,7 @@ public class ReflectionUtils {
5155
localizedLanguageField.setAccessible(true);
5256
classInfosField.setAccessible(true);
5357
convertersField.setAccessible(true);
58+
quickAccessConvertersField.setAccessible(true);
5459
sortClassInfosMethod.setAccessible(true);
5560

5661
} catch (Exception e) {
@@ -79,6 +84,11 @@ public static List<ConverterInfo<?,?>> getConverters() throws Exception {
7984
return (List<ConverterInfo<?, ?>>) convertersField.get(null);
8085
}
8186

87+
public static Map<Pair<Class<?>, Class<?>>, ConverterInfo<?, ?>> getQuickAccessConverters() throws Exception {
88+
//noinspection unchecked
89+
return (Map<Pair<Class<?>, Class<?>>, ConverterInfo<?, ?>>) quickAccessConvertersField.get(null);
90+
}
91+
8292
@SuppressWarnings("unchecked")
8393
public static List<ClassInfo<?>> getTempClassInfos() throws Exception {
8494
return (List<ClassInfo<?>>) tempClassInfosField.get(null);
@@ -170,6 +180,12 @@ public static ClassInfo<? extends Struct> addClassInfo(Class<? extends Struct> c
170180
}
171181

172182
// converters
183+
try {
184+
//noinspection SuspiciousMethodCalls,removal
185+
getQuickAccessConverters().remove(new Pair<>(Struct.class, customClass));
186+
} catch (Exception e) {
187+
throw new RuntimeException(e);
188+
}
173189
Converter<Struct, Struct> castingConverter = struct -> {
174190
if (customClass.isInstance(struct))
175191
return customClass.cast(struct);
@@ -206,10 +222,18 @@ public static void removeClassInfo(ClassInfo<?> classInfo) {
206222
List<ConverterInfo<?,?>> toRemove = new ArrayList<>();
207223
var converters = getConverters();
208224
for (var converterInfo : converters) {
209-
if (converterInfo.getTo().equals(customClass))
225+
if (converterInfo.getTo().equals(customClass) || converterInfo.getFrom().equals(customClass))
210226
toRemove.add(converterInfo);
211227
}
212228
converters.removeAll(toRemove);
229+
// remove all converters from or to customClass
230+
var quickAccessConverters = getQuickAccessConverters();
231+
// remove all converters starting from customClass
232+
for (var key : new ArrayList<>(quickAccessConverters.keySet())) {
233+
if (key.getKey().equals(customClass) || key.getValue().equals(customClass)) {
234+
quickAccessConverters.remove(key);
235+
}
236+
}
213237

214238
getExactClassInfos().remove(classInfo.getC());
215239
getClassInfosByCodeName().remove(classInfo.getCodeName());

src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
import ch.njol.skript.doc.Name;
1212
import ch.njol.skript.doc.Since;
1313
import ch.njol.skript.lang.Literal;
14+
import ch.njol.skript.lang.ParseContext;
1415
import ch.njol.skript.lang.SkriptParser;
1516
import ch.njol.skript.lang.function.Functions;
1617
import ch.njol.skript.registrations.Classes;
18+
import ch.njol.skript.util.LiteralUtils;
1719
import ch.njol.skript.util.Utils;
20+
import ch.njol.util.Pair;
1821
import com.sovdee.oopsk.Oopsk;
1922
import com.sovdee.oopsk.core.Field;
2023
import com.sovdee.oopsk.core.Field.Modifier;
@@ -24,22 +27,26 @@
2427
import org.bukkit.event.Event;
2528
import org.jetbrains.annotations.NotNull;
2629
import org.jetbrains.annotations.Nullable;
30+
import org.skriptlang.skript.lang.converter.Converter;
31+
import org.skriptlang.skript.lang.converter.Converters;
2732
import org.skriptlang.skript.lang.entry.EntryContainer;
2833
import org.skriptlang.skript.lang.structure.Structure;
2934

3035
import java.util.ArrayList;
3136
import java.util.EnumSet;
37+
import java.util.HashMap;
3238
import java.util.List;
3339
import java.util.Locale;
40+
import java.util.Map;
3441
import java.util.Set;
3542
import java.util.regex.MatchResult;
3643
import java.util.regex.Matcher;
3744
import java.util.regex.Pattern;
3845

3946
import static com.sovdee.oopsk.core.generation.ReflectionUtils.addClassInfo;
40-
import static com.sovdee.oopsk.core.generation.ReflectionUtils.addLanguageNode;
4147
import static com.sovdee.oopsk.core.generation.ReflectionUtils.disableRegistrations;
4248
import static com.sovdee.oopsk.core.generation.ReflectionUtils.enableRegistrations;
49+
import static com.sovdee.oopsk.core.generation.ReflectionUtils.getQuickAccessConverters;
4350
import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeClassInfo;
4451
import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeLanguageNode;
4552

@@ -52,21 +59,36 @@
5259
"The default value will be evaluated when the struct is created.",
5360
"Fields can be marked as constant by adding 'const' or 'constant' at the beginning of the line. Constant fields cannot be changed after the struct is created.",
5461
"Dynamic fields can be made by adding 'dynamic' to the beginning of the line. Dynamic fields require a default value and will always re-evaluate their value each time they are called. " +
55-
"This means they cannot be changed directly, but can rely on the values of other fields or even functions."
62+
"This means they cannot be changed directly, but can rely on the values of other fields or even functions.",
63+
"Converters can be defined in a 'converts to:' section. Each converter is defined in the format '<target type> via %expression%'. " +
64+
"Note that oopsk cannot generate chained converters reliably, so you should expressly define converters for all target types you wish to convert to.",
65+
"Be careful when using converters, as they can cause unexpected behavior in all of your scripts if not used properly." +
66+
"Best practice is to ensure you reload all scripts after defining or modifying struct templates to ensure all converters are registered correctly."
5667
})
5768
@Example("""
5869
struct message:
5970
sender: player
6071
message: string
6172
const timestamp: date = now
6273
attachments: objects
74+
converts to:
75+
string via this->message
6376
""")
6477
@Example("""
6578
struct Vector2:
6679
x: number
6780
y: number
6881
dynamic length: number = sqrt(this->x^2 + this->y^2)
6982
""")
83+
@Example("""
84+
struct CustomPlayer
85+
const player: player
86+
rank: string = "Member"
87+
dynamic isAdmin: boolean = whether this->rank is "Admin"
88+
converts to:
89+
player via this->player
90+
location via this->player's location
91+
""")
7092
@Since("1.0")
7193
public class StructStructTemplate extends Structure {
7294

@@ -77,6 +99,7 @@ public class StructStructTemplate extends Structure {
7799
private StructTemplate template;
78100
private EntryContainer entryContainer;
79101
private String name;
102+
private final Map<Class<?>, Converter<Struct, Object>> converters = new HashMap<>();
80103

81104
@Override
82105
public boolean init(Literal<?>[] args, int matchedPattern, SkriptParser.ParseResult parseResult, @Nullable EntryContainer entryContainer) {
@@ -123,6 +146,94 @@ public boolean preLoad() {
123146
return templateManager.addTemplate(template);
124147
}
125148

149+
private void registerConverters(SectionNode node) {
150+
// find
151+
boolean found = false;
152+
for (Node child : node) {
153+
if (child instanceof SectionNode convertersNode) {
154+
String key = ScriptLoader.replaceOptions(convertersNode.getKey());
155+
if (key != null && key.trim().equalsIgnoreCase("converts to")) {
156+
if (found) {
157+
Skript.error("Multiple 'converts to' sections found in struct " + name + ".");
158+
return;
159+
}
160+
parseConverters(convertersNode);
161+
found = true;
162+
if (this.converters.isEmpty()) {
163+
Skript.error("No valid converters found in struct " + name + "'s 'converts to' section.");
164+
return;
165+
}
166+
} else {
167+
Skript.error("Unexpected section '" + key + "' found in struct " + name + ".");
168+
return;
169+
}
170+
}
171+
}
172+
173+
// register
174+
if (found) {
175+
enableRegistrations();
176+
for (var entry : this.converters.entrySet()) {
177+
Class<?> targetClass = entry.getKey();
178+
Converter<Struct, Object> converter = entry.getValue();
179+
//noinspection unchecked
180+
Converters.registerConverter((Class<Struct>) customClass, (Class<Object>) targetClass, converter);
181+
}
182+
disableRegistrations();
183+
}
184+
185+
}
186+
187+
private static final Pattern CONVERTER_PATTERN = Pattern.compile("([\\w ]+) via (.+)");
188+
189+
private void parseConverters(@NotNull SectionNode node) {
190+
for (Node child : node) {
191+
if (child instanceof SimpleNode simpleNode) {
192+
String entry = simpleNode.getKey();
193+
if (entry == null)
194+
throw new IllegalStateException("Null node found.");
195+
// split into type and converter
196+
Matcher matcher = CONVERTER_PATTERN.matcher(entry);
197+
if (!matcher.matches()) {
198+
Skript.error("Invalid converter entry: " + entry);
199+
continue;
200+
}
201+
String typeString = matcher.group(1).trim();
202+
String converterString = matcher.group(2).trim();
203+
204+
var pair = Utils.getEnglishPlural(typeString);
205+
ClassInfo<?> targetType = Classes.getClassInfoFromUserInput(pair.getKey());
206+
if (targetType == null) {
207+
Skript.error("Invalid converter target type: " + typeString);
208+
continue;
209+
}
210+
211+
// parse the converter expression
212+
var converter = new SkriptParser(converterString, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseExpression(targetType.getC());
213+
if (converter == null || LiteralUtils.hasUnparsedLiteral(converter)) {
214+
Skript.error("Converter expression does not return the declared type of " + Classes.toString(targetType) + ": '" + converterString + "'");
215+
continue;
216+
}
217+
218+
// clear quick access converter to ensure there isn't a null entry
219+
try {
220+
//noinspection removal,SuspiciousMethodCalls
221+
getQuickAccessConverters().remove(new Pair<>(customClass, targetType.getC()));
222+
} catch (Exception e) {
223+
throw new RuntimeException(e);
224+
}
225+
226+
// register the converter
227+
converters.put(targetType.getC(), new Converter<>() {
228+
@Override
229+
public @Nullable Object convert(Struct from) {
230+
return converter.getSingle(new DynamicFieldEvalEvent(from));
231+
}
232+
});
233+
}
234+
}
235+
}
236+
126237
public Class<? extends Struct> customClass;
127238
public ClassInfo<?> customClassInfo;
128239

@@ -136,7 +247,7 @@ private void registerCustomType() {
136247
assert customClass != null;
137248

138249
// hack open the Classes class to allow re-registration
139-
addClassInfo(customClass, name);
250+
customClassInfo = addClassInfo(customClass, name);
140251
}
141252

142253
private void unregisterCustomType() {
@@ -150,7 +261,6 @@ private void unregisterCustomType() {
150261
}
151262
}
152263

153-
154264
@Override
155265
public boolean load() {
156266
var templateManager = Oopsk.getTemplateManager();
@@ -162,6 +272,8 @@ public boolean load() {
162272
unregisterCustomType();
163273
return false;
164274
}
275+
SectionNode node = entryContainer.getSource();
276+
registerConverters(node);
165277
getParser().deleteCurrentEvent();
166278

167279
return true;

0 commit comments

Comments
 (0)