Users have predictable LDtk entities and know the exact nature of any ldtk field they've created: its identifier, its type, and whether or not it's nullable. However, finding an instance and coercing it to the type the user knows it should be involves a lot of boilerplate with several layers of .iter().find(...)s, matches, if lets and .unwrap()s.
This PR adds many extension methods to Level and EntityInstance to take that burden from the user. For every variant of FieldValue, this adds a method (or two) for accessing a field instance at a given identifier with that type - unwrapping the variant to its internal type. For example, there is a .get_maybe_int_field("MyIdentifier") which, if the field instance exists and is an Int, will return an &Option<i32> referencing its value. There's also a .get_int_field("MyIdentifier") which unwraps it further to a &i32, if possible. If any of these assumptions doesn't hold, an Err(LdtkFieldsError) is returned instead.
Implementation
Most of these methods, and their tests, have been generated by macros. This is a good use case for macros since the code for each of the methods is very similar, though there are some hiccups:
The Bool and Color variants aren't optional. This is because it's impossible to set them as nullable in ldtk. This difference in typing made some of the macro writing difficult, and resulted in more special-case macros.
Half of the methods are for accessing plural variants whose internal type is Vec<_>. These methods return slices to the data, and their non-optional versions return a special iterator whose design I'll touch on later.
All of these methods access the data and return it by reference. This includes those whose types are cheap to copy. This makes the methods less opinionated about what is and isn't "cheap" to copy, and makes them more consistent. This consistency made the macro writing a little bit easier.
The identifier itself is passed into the methods as &str, despite the fact that each of them contains a .to_string(). This .to_string() only ever occurs in the error case, so its prevalence here doesn't affect the "happy path".
The methods for accessing Vec<Option<T>> variants in a non-optional way has some special considerations. One design question is whether to only return all elements if they are all Some, erroring otherwise, or to return all Some elements even if some of the elements are None. I went with the first option as the intention of these particular methods is to allow users to easily access the data if they've specified it as non-nullable in LDtk. All of these methods act as type coercion, and they should be homomorphic with LDtk's (very limited) field type system.
Furthermore, unlike the normal optional accessors, we cannot simply return a slice to this Vec<Option<T>> data as &[T]. This is because it is not contiguous in memory - the Option wrapping "gets in the way". Another option is to return it as a Vec<&T>, since this preserves the reference to the original data and unwraps the options. However, this involves iterating over the original collection and re-collecting it, even though the user may just want to iterate over it again. The solution chosen here is to instead return an iterator instead of a collection. Specifically, it returns an AllSomeIter<'a, T>, which is a new type added in this PR to provide some type safety and helpful naming to the return type. AllSomeIter just wraps around a Flatten<Iter<'a, Option<T>>, but guarantees that every element of the original collection is Some on construction. This preserves the original references, unwraps the options in the original data, and provides type guarantees while returning as early an intermediate result as possible.
Since it's kind of hard to see what methods have been added here due to most of them being generated by macros, here's a list:
Closes #175.
Summary
Users have predictable LDtk entities and know the exact nature of any ldtk field they've created: its identifier, its type, and whether or not it's nullable. However, finding an instance and coercing it to the type the user knows it should be involves a lot of boilerplate with several layers of
.iter().find(...)
s,match
es,if let
s and.unwrap()
s.This PR adds many extension methods to
Level
andEntityInstance
to take that burden from the user. For every variant ofFieldValue
, this adds a method (or two) for accessing a field instance at a given identifier with that type - unwrapping the variant to its internal type. For example, there is a.get_maybe_int_field("MyIdentifier")
which, if the field instance exists and is anInt
, will return an&Option<i32>
referencing its value. There's also a.get_int_field("MyIdentifier")
which unwraps it further to a&i32
, if possible. If any of these assumptions doesn't hold, anErr(LdtkFieldsError)
is returned instead.Implementation
Most of these methods, and their tests, have been generated by macros. This is a good use case for macros since the code for each of the methods is very similar, though there are some hiccups:
Bool
andColor
variants aren't optional. This is because it's impossible to set them as nullable in ldtk. This difference in typing made some of the macro writing difficult, and resulted in more special-case macros.Vec<_>
. These methods return slices to the data, and their non-optional versions return a special iterator whose design I'll touch on later.All of these methods access the data and return it by reference. This includes those whose types are cheap to copy. This makes the methods less opinionated about what is and isn't "cheap" to copy, and makes them more consistent. This consistency made the macro writing a little bit easier.
The identifier itself is passed into the methods as
&str
, despite the fact that each of them contains a.to_string()
. This.to_string()
only ever occurs in the error case, so its prevalence here doesn't affect the "happy path".The methods for accessing
Vec<Option<T>>
variants in a non-optional way has some special considerations. One design question is whether to only return all elements if they are allSome
, erroring otherwise, or to return allSome
elements even if some of the elements areNone
. I went with the first option as the intention of these particular methods is to allow users to easily access the data if they've specified it as non-nullable in LDtk. All of these methods act as type coercion, and they should be homomorphic with LDtk's (very limited) field type system.Furthermore, unlike the normal optional accessors, we cannot simply return a slice to this
Vec<Option<T>>
data as&[T]
. This is because it is not contiguous in memory - theOption
wrapping "gets in the way". Another option is to return it as aVec<&T>
, since this preserves the reference to the original data and unwraps the options. However, this involves iterating over the original collection and re-collecting it, even though the user may just want to iterate over it again. The solution chosen here is to instead return an iterator instead of a collection. Specifically, it returns anAllSomeIter<'a, T>
, which is a new type added in this PR to provide some type safety and helpful naming to the return type.AllSomeIter
just wraps around aFlatten<Iter<'a, Option<T>>
, but guarantees that every element of the original collection isSome
on construction. This preserves the original references, unwraps the options in the original data, and provides type guarantees while returning as early an intermediate result as possible.Since it's kind of hard to see what methods have been added here due to most of them being generated by macros, here's a list:
field_instances
get_field_instance
get_field
get_maybe_int_field
get_int_field
get_maybe_float_field
get_float_field
get_bool_field
get_maybe_string_field
get_string_field
get_color_field
get_maybe_file_path_field
get_file_path_field
get_maybe_enum_field
get_enum_field
get_maybe_tile_field
get_tile_field
get_maybe_entity_ref_field
get_entity_ref_field
get_maybe_point_field
get_point_field
get_maybe_ints_field
iter_ints_field
get_maybe_floats_field
iter_floats_field
get_bools_field
get_maybe_strings_field
iter_strings_field
get_colors_field
get_maybe_file_paths_field
iter_file_paths_field
get_maybe_enums_field
iter_enums_field
get_maybe_tiles_field
iter_tiles_field
get_maybe_entity_refs_field
iter_entity_refs_field
get_maybe_points_field
iter_points_field