-
Notifications
You must be signed in to change notification settings - Fork 0
Dev: Anatomy of a `from_json` function
Many objects in TXEnginePY implement a weird-looking, highly decorated function called from_json
, what's up with that?
from_json
may look like a blob of nonsense, but it actually serves an important purpose: Informing TXEngine how to translate a specially-formatted JSON object directly into a proper Python object.
Lets break down an example--in this case take from Item
.
@staticmethod
@cached([LoadableMixin.CACHE_PATH.format("Item")])
def from_json(json: dict[str, any]) -> "Item":
"""
Instantiate an Item object from a JSON blob.
Args:
json: a dict-form representation of a JSON object
Returns: An Item instance with the properties defined in the JSON
Required JSON fields:
- name: str
- id: int
- value: {int, int}
- description: str
Optional JSON fields:
- max_quantity: int (default value 10)
"""
required_fields = [
("name", str), ("id", int), ("value", dict), ("description", str)
]
optional_fields = [
("max_quantity", int)
]
LoadableFactory.validate_fields(required_fields, json)
LoadableFactory.validate_fields(optional_fields, json, False, False)
kwargs = LoadableFactory.collect_optional_fields(optional_fields, json)
return Item(json['name'],
json['id'],
json['value'],
json['description'],
**kwargs
)
Let's start with @staticmethod
. Every from_json
method is stateless--that is to say that they do not depend on data from external sources or from previous runs in order to execute their logic. As such, there's no need for them to be class or instance-bound--thus the staticmethod
decorator. This way, at run time, we can cache the function and not have to worry about every instance of the class attempt to re-cache the method.
Directly below the @staticmethod
decorator is another decorator, @cached
. This is a custom decorator defined in game.cache
. This decorator automatically caches whatever it is decorating and stores it within the cache along a specified path. In our case, the path is defined by a handy shortcut (CACHE_PATH
) which can be formatted to match the current class.
For the curious, CACHE_PATH
evaluates out to "loaders.Item.from_json"
. In our cache, that would appear as follows:
cache = {
"loaders" : {
"Item" : {
"from_json" : a_reference_to_the_static_method
}
}
}
The next code we run into is a couple of lists:
required_fields = [
("name", str), ("id", int), ("value", dict), ("description", str)
]
optional_fields = [
("max_quantity", int)
]
These are lists of tuples, where index 0 of the tuple is the name of a field and index 1 of the tuple is the type of the field. These values correspond to the expected names and types of the fields inside the JSON version of the class in question.
In the case of Item
, its fairly basic. It expects a couple of str
s and int
s and a single dict
. We specify these pairings in each from_json
so that we can validate the existence and typing of required fields before executing any logic that depends on them.
Up next is our validation calls:
LoadableFactory.validate_fields(required_fields, json)
LoadableFactory.validate_fields(optional_fields, json, False, False)
LoadableFactory
implements a handy helper function that takes in a list of name-type tuples and a json object and automatically checks for that they exist (if they're required) and that they're typed correctly.
The first call checks the fields in required_fields
in required mode, and the second call checks for the contents of optional_fields
in optional mode. The final two arguments of validate_fields
are required
and implicit_fields
respectively. Setting these to false allows the validator to ignore fields that do not appear and prevents it from checking for any default fields such as 'class' that would normally be expected to appear.
Next comes the logic for collecting and bundling optional values.
kwargs = LoadableFactory.collect_optional_fields(optional_fields, json)
collect_optional_fields
parses the JSON object passed to it and grabs any specified fields, bundles them into a dict
, and then returns it. This allows us to cleanly pass these optional fields and key-word-arguments to the __init__
function of Item
.
Finally, we return a fancy new Item object, complete with support for optional fields:
return Item(json['name'],
json['id'],
json['value'],
json['description'],
**kwargs
)
Item
was a fairly simple example--it doesn't have any complex sub-items that have to be recursively built. That being said, I hope it was an interesting look into the way that TXEnginePY handles asset loading!