Make custom types optional

This commit is contained in:
Daniel Cronqvist 2024-11-16 21:14:23 +01:00
parent 67876c6532
commit 8c9068cc97
21 changed files with 189 additions and 59 deletions

View file

@ -73,12 +73,12 @@ public class Program
return templateReader.ReadTemplate();
}
private static ICustomTypeDefinition ResolveCustomType(string name)
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException();
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -1,4 +1,3 @@
using System;
using System.Globalization;
using System.Linq;
using DotTiled.Serialization;
@ -57,12 +56,12 @@ public partial class MapParser : Node2D
return templateReader.ReadTemplate();
}
private static ICustomTypeDefinition ResolveCustomType(string name)
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException();
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -246,7 +246,7 @@ public class LoaderTests
}
[Fact]
public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ThrowsException()
public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ReturnsMapWithEmptyProperties()
{
// Arrange
var resourceReader = Substitute.For<IResourceReader>();
@ -270,8 +270,11 @@ public class LoaderTests
var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>();
var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions);
// Act & Assert
Assert.Throws<KeyNotFoundException>(() => loader.LoadMap("map.tmx"));
// Act
var result = loader.LoadMap("map.tmx");
// Assert
DotTiledAssert.AssertProperties([], result.Properties);
}
[Fact]

View file

@ -32,9 +32,14 @@ public partial class MapReaderTests
using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -28,9 +28,14 @@ public partial class TmjMapReaderTests
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -28,9 +28,14 @@ public partial class TmxMapReaderTests
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -86,13 +86,16 @@ internal static partial class Helpers
};
}
internal static List<IProperty> ResolveClassProperties(string className, Func<string, ICustomTypeDefinition> customTypeResolver)
internal static List<IProperty> ResolveClassProperties(string className, Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
if (string.IsNullOrWhiteSpace(className))
return null;
var customType = customTypeResolver(className) ?? throw new InvalidOperationException($"Could not resolve custom type '{className}'.");
if (customType is not CustomClassDefinition ccd)
if (!customType.HasValue)
return null;
if (customType.Value is not CustomClassDefinition ccd)
throw new InvalidOperationException($"Custom type '{className}' is not a class.");
return CreateInstanceOfCustomClass(ccd, customTypeResolver);
@ -100,17 +103,31 @@ internal static partial class Helpers
internal static List<IProperty> CreateInstanceOfCustomClass(
CustomClassDefinition customClassDefinition,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
return customClassDefinition.Members.Select(x =>
{
if (x is ClassProperty cp)
{
var resolvedType = customTypeResolver(cp.PropertyType);
if (!resolvedType.HasValue)
{
return new ClassProperty
{
Name = cp.Name,
PropertyType = cp.PropertyType,
Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver)
Value = []
};
}
if (resolvedType.Value is not CustomClassDefinition ccd)
throw new InvalidOperationException($"Custom type '{cp.PropertyType}' is not a class.");
return new ClassProperty
{
Name = cp.Name,
PropertyType = cp.PropertyType,
Value = CreateInstanceOfCustomClass(ccd, customTypeResolver)
};
}

View file

@ -12,7 +12,7 @@ public class Loader
{
private readonly IResourceReader _resourceReader;
private readonly IResourceCache _resourceCache;
private readonly IDictionary<string, ICustomTypeDefinition> _customTypeDefinitions;
private readonly Dictionary<string, ICustomTypeDefinition> _customTypeDefinitions;
/// <summary>
/// Initializes a new instance of the <see cref="Loader"/> class with the given <paramref name="resourceReader"/>, <paramref name="resourceCache"/>, and <paramref name="customTypeDefinitions"/>.
@ -114,5 +114,11 @@ public class Loader
return templateReader.ReadTemplate();
});
private ICustomTypeDefinition CustomTypeResolver(string name) => _customTypeDefinitions[name];
private Optional<ICustomTypeDefinition> CustomTypeResolver(string name)
{
if (_customTypeDefinitions.TryGetValue(name, out var customTypeDefinition))
return new Optional<ICustomTypeDefinition>(customTypeDefinition);
return Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -14,7 +14,7 @@ public class MapReader : IMapReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _mapStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class MapReader : IMapReader
string map,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TemplateReader : ITemplateReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _templateStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TemplateReader : ITemplateReader
string template,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TilesetReader : ITilesetReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _tilesetStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TilesetReader : ITilesetReader
string tileset,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -19,7 +19,7 @@ public class TjTemplateReader : TmjReaderBase, ITemplateReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -19,7 +19,7 @@ public class TmjMapReader : TmjReaderBase, IMapReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@ -63,8 +64,28 @@ public abstract partial class TmjReaderBase
var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
// If the custom class definition is not found,
// we assume an empty class definition.
if (!customTypeDef.HasValue)
{
if (!element.TryGetProperty("value", out var valueElement))
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Value = ReadPropertiesInsideClass(valueElement, new CustomClassDefinition
{
Name = propertyType,
Members = []
})
};
}
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new JsonException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]);
var mergedProps = Helpers.MergeProperties(propsInType, props);
@ -77,9 +98,6 @@ public abstract partial class TmjReaderBase
};
}
throw new JsonException($"Unknown custom class '{propertyType}'.");
}
internal List<IProperty> ReadPropertiesInsideClass(
JsonElement element,
CustomClassDefinition customClassDefinition)
@ -91,6 +109,33 @@ public abstract partial class TmjReaderBase
if (!element.TryGetProperty(prop.Name, out var propElement))
continue;
if (prop is ClassProperty classProp)
{
var resolvedCustomType = _customTypeResolver(classProp.PropertyType);
if (!resolvedCustomType.HasValue)
{
resultingProps.Add(new ClassProperty
{
Name = classProp.Name,
PropertyType = classProp.PropertyType,
Value = []
});
continue;
}
if (resolvedCustomType.Value is not CustomClassDefinition ccd)
throw new JsonException($"Custom type '{classProp.PropertyType}' is not a class.");
var readProps = ReadPropertiesInsideClass(propElement, ccd);
resultingProps.Add(new ClassProperty
{
Name = classProp.Name,
PropertyType = classProp.PropertyType,
Value = readProps
});
continue;
}
IProperty property = prop.Type switch
{
PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
@ -100,8 +145,8 @@ public abstract partial class TmjReaderBase
PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs<string>(), CultureInfo.InvariantCulture) },
PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() },
PropertyType.Class => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) },
PropertyType.Enum => ReadEnumProperty(propElement),
PropertyType.Class => throw new NotImplementedException("Class properties should be handled elsewhere"),
_ => throw new JsonException("Invalid property type")
};
@ -115,7 +160,7 @@ public abstract partial class TmjReaderBase
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var typeInXml = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch
var typeInJson = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch
{
"string" => PropertyType.String,
"int" => PropertyType.Int,
@ -123,8 +168,24 @@ public abstract partial class TmjReaderBase
}).GetValueOr(PropertyType.String);
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced)
throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined");
if (!customTypeDef.HasValue)
{
if (typeInJson == PropertyType.String)
{
var value = element.GetRequiredProperty<string>("value");
var values = value.Split(',').Select(v => v.Trim()).ToHashSet();
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
else
{
var value = element.GetRequiredProperty<int>("value");
var values = new HashSet<string> { value.ToString(CultureInfo.InvariantCulture) };
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
}
if (customTypeDef.Value is not CustomEnumDefinition ced)
throw new JsonException($"Custom type '{propertyType}' is not an enum.");
if (ced.StorageType == CustomEnumStorageType.String)
{

View file

@ -13,7 +13,7 @@ public abstract partial class TmjReaderBase : IDisposable
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
/// <summary>
/// The root element of the JSON document being read.
@ -34,7 +34,7 @@ public abstract partial class TmjReaderBase : IDisposable
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement;
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -19,7 +19,7 @@ public class TsjTilesetReader : TmjReaderBase, ITilesetReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -16,7 +16,7 @@ public class TmxMapReader : TmxReaderBase, IMapReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
@ -66,25 +67,35 @@ public abstract partial class TmxReaderBase
var propertyType = _reader.GetRequiredAttribute("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
// If the custom class definition is not found,
// we assume an empty class definition.
if (!customTypeDef.HasValue)
{
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var props = ReadProperties();
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = props };
}
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
}
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new XmlException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var props = ReadProperties();
var mergedProps = Helpers.MergeProperties(propsInType, props);
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps };
}
else
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
}
}
throw new XmlException($"Unkonwn custom class definition: {propertyType}");
return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
}
internal EnumProperty ReadEnumProperty()
@ -99,8 +110,26 @@ public abstract partial class TmxReaderBase
}) ?? PropertyType.String;
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced)
throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined");
// If the custom enum definition is not found,
// we assume an empty enum definition.
if (!customTypeDef.HasValue)
{
if (typeInXml == PropertyType.String)
{
var value = _reader.GetRequiredAttribute("value");
var values = value.Split(',').Select(v => v.Trim()).ToHashSet();
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
else
{
var value = _reader.GetRequiredAttributeParseable<int>("value");
var values = new HashSet<string> { value.ToString(CultureInfo.InvariantCulture) };
return new EnumProperty { Name = name, PropertyType = propertyType, Value = values };
}
}
if (customTypeDef.Value is not CustomEnumDefinition ced)
throw new XmlException($"Custom defined type {propertyType} is not an enum.");
if (ced.StorageType == CustomEnumStorageType.String)
{
@ -144,6 +173,6 @@ public abstract partial class TmxReaderBase
}
}
throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}");
throw new XmlException($"Unable to read enum property {name} with type {propertyType}");
}
}

View file

@ -11,7 +11,7 @@ public abstract partial class TmxReaderBase : IDisposable
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly XmlReader _reader;
private bool disposedValue;
@ -28,7 +28,7 @@ public abstract partial class TmxReaderBase : IDisposable
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -16,7 +16,7 @@ public class TsxTilesetReader : TmxReaderBase, ITilesetReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -16,7 +16,7 @@ public class TxTemplateReader : TmxReaderBase, ITemplateReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }