Offroaders123 / NBTify

A library to read and write NBT files on the web!
http://npm.im/nbtify
MIT License
42 stars 5 forks source link

Class-Generated Compound Tags + Object Error Handling #19

Closed Offroaders123 closed 1 year ago

Offroaders123 commented 1 year ago

Currently, only non-extended Object objects are allowed to validate as a Compound tag. I want to allow NBTify users (me included!) to be able to construct their own NBT-serializable objects, by using ES6 classes. This would allow you to create interface types for in-game NBT objects, and also write your own JavaScript/TypeScript class that will generate the exact same structure on the fly, without needing to have the game generate them.

At first, I couldn't figure out how I can distinguish serializable vs. non serializable objects, since your own custom classes won't directly have the Object prototype as the first parent, so that won't work to check the class type. This would invalidate your custom classes from being able to be read as a valid Compound tag structure. My current code is there for this, because I want to throw error handling messages when you try to serialize non-NBT kinds of objects, like RegExp, TextEncoder, or other ones like that. Essentially, any standard library objects that shouldn't be parseable down to NBT, unless you explicitly wanted to allow one to do that.

Similar to how toJSON and get [Symbol.toStringTag] work (Check out the Well-known Symbols section on MDN), I realized a really nice way to add a check for if any given object is NBT-serializable, I could check using both the original Object prototype method I am currently using, and with the use of a new property, which would be something along the lines of get [Symbol("toNBT")], or something like that. The new check would first check if the given object is a direct decendent of Object (current behavior). And if that's not the case, check if the object has the get [Symbol("toNBT")] present. If either of those are true, than attempt to parse that object as a Compound tag.

I'm not sure exactly what the Symbol will be called yet, and I'm also not sure what I want the return value of the property getter to be, either. I'm wondering if it should work like toJSON, in that you can add NBT-serializability (what a mouthful/typeful XD) to non-originally serializable objects.

In typing that last sentence, I'm curious if I should remove the forceful error handling, and make it work like how JSON.stringify() handles it, in the fact that it coalesces objects without the toJSON property as just an empty { } object. This will make the writing process less strict, but it does make it more in-line with what the JSON module does, and I'm trying to do that where applicable with NBTify too, since I want it to feel like it's part of the JS standard library.

If that last paragraph is what I go with, then I can simply serialize "unsupported" (or rather, unexpected) objects like how the JSON module does it. I would remove the error checking for this, and it would simply be enforced with user-defined get [Symbol("toNBT")] properties and TypeScript definitions. That's the middle ground I have been following with the rest of the library too, and it has made making data structures really nice too, since it's just JavaScript objects and primitives that you are working with, rather than NBT-related primitives.

Offroaders123 commented 1 year ago

Now all objects can be accepted into NBTify, build from classes or anything else. The type checking for them currently isn't working still, so you will get a TypeScript error for passing in classes that don't have the CompoundTag index signature present on them. They will indeed be readable by NBTify though. You can combat this by adding an index signature to your class, but that is not my end-all solution. This will break your type definitions for your class, since it will allow any properties to be assigned to your object, because of the index signature.

I want to have a way to validate incoming objects by using an index signature, but without requiring them to also have the index signature. So, essentially:

export type Tag = string | number | boolean | CompoundTag;

export interface CompoundTag {
  // [name: string]: satisfies Tag; <-- This is the ideal way of type-checking the incoming object
  [name: string]: Tag;
}

export declare function write(data: CompoundTag): Promise<Uint8Array>;

export class MyNBT {
  ExampleKey = 5;
  Heya = true;
  ValidNBT = "oh yeah!";
}

await write(new MyNBT());
// Argument of type 'MyNBT' is not assignable to parameter of type 'CompoundTag'.
// Index signature for type 'string' is missing in type 'MyNBT'. ts(2345)