Skip to content

Commit

Permalink
Allow (opt-in) modifying IAttributesTable (#118)
Browse files Browse the repository at this point in the history
This has to be opt-in, because it would otherwise break anyone who expects a
JsonElementAttributesTable whose JsonElement they can do stuff with.

Resolves #117
  • Loading branch information
airbreather committed Feb 9, 2023
1 parent 368733d commit 28502d5
Show file tree
Hide file tree
Showing 12 changed files with 553 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class GeoJsonConverterFactory : JsonConverterFactory

private readonly RingOrientationOption _ringOrientationOption;

private readonly bool _allowModifyingAttributesTables;

/// <summary>
/// Creates an instance of this class using the defaults.
/// </summary>
Expand Down Expand Up @@ -70,7 +72,9 @@ public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox)
/// <summary>
/// Creates an instance of this class using the provided <see cref="GeometryFactory"/>, the
/// given value for whether or not we should write out a "bbox" for a plain geometry,
/// feature and feature collection, and defaults for all other values.
/// feature and feature collection, the given "magic" string to signal when an
/// <see cref="IAttributesTable"/> property is actually filling in for a Feature's "id", and
/// defaults for all other values.
/// </summary>
/// <param name="factory"></param>
/// <param name="writeGeometryBBox"></param>
Expand All @@ -79,23 +83,47 @@ public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox,
: this(factory, writeGeometryBBox, idPropertyName, RingOrientationOption.EnforceRfc9746)
{
}

/// <summary>
/// Creates an instance of this class using the provided <see cref="GeometryFactory"/>, the
/// given value for whether or not we should write out a "bbox" for a plain geometry,
/// feature and feature collection, and the given "magic" string to signal
/// when an <see cref="IAttributesTable"/> property is actually filling in for a Feature's "id".
/// feature and feature collection, the given "magic" string to signal when an
/// <see cref="IAttributesTable"/> property is actually filling in for a Feature's "id", the
/// <see cref="RingOrientationOption"/> value that indicates how rings should be oriented
/// when writing them out, and defaults for all other values.
/// </summary>
/// <param name="factory"></param>
/// <param name="writeGeometryBBox"></param>
/// <param name="idPropertyName"></param>
/// <param name="ringOrientationOption"></param>
public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox, string idPropertyName,
RingOrientationOption ringOrientationOption)
: this(factory, writeGeometryBBox, idPropertyName, ringOrientationOption, false)
{
}

/// <summary>
/// Creates an instance of this class using the provided <see cref="GeometryFactory"/>, the
/// given value for whether or not we should write out a "bbox" for a plain geometry,
/// feature and feature collection, the given "magic" string to signal when an
/// <see cref="IAttributesTable"/> property is actually filling in for a Feature's "id", the
/// <see cref="RingOrientationOption"/> value that indicates how rings should be oriented
/// when writing them out, and a flag indicating whether or not to use a less efficient
/// implementation of <see cref="IAttributesTable"/> that can be modified in-place.
/// </summary>
/// <param name="factory"></param>
/// <param name="writeGeometryBBox"></param>
/// <param name="idPropertyName"></param>
/// <param name="ringOrientationOption"></param>
/// <param name="allowModifyingAttributesTables"></param>
public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox, string idPropertyName,
RingOrientationOption ringOrientationOption, bool allowModifyingAttributesTables)
{
_factory = factory;
_writeGeometryBBox = writeGeometryBBox;
_idPropertyName = idPropertyName ?? DefaultIdPropertyName;
_ringOrientationOption = ringOrientationOption;
_allowModifyingAttributesTables = allowModifyingAttributesTables;
}

///<inheritdoc cref="JsonConverter.CanConvert(Type)"/>
Expand All @@ -117,7 +145,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
if (typeof(IFeature).IsAssignableFrom(typeToConvert))
return new StjFeatureConverter(_idPropertyName, _writeGeometryBBox);
if (typeof(IAttributesTable).IsAssignableFrom(typeToConvert))
return new StjAttributesTableConverter(_idPropertyName);
return new StjAttributesTableConverter(_idPropertyName, _allowModifyingAttributesTables);

throw new ArgumentException(nameof(typeToConvert));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text.Json;

namespace NetTopologySuite.Features
{
/// <summary>
/// An <see cref="IAttributesTable"/> that has been <b>partially</b> deserialized to a strongly-
/// typed CLR object model, but which still contains some remnants of the JSON source that
/// produced it which may require the consumer to tell us more about what types they expected.
/// <para/>
/// Due to an intentional limitation in <c>System.Text.Json</c>, there is no way to produce a
/// standalone GeoJSON object that includes enough information to produce an object graph that's
/// complete with nested members of arbitrary types.
/// <para/>
/// In that spirit, this interface allows you to pick up where this library left off and use the
/// Feature's attributes in a more strongly-typed fashion using your own knowledge of whatever
/// internal structure the GeoJSON object is expected to have.
/// </summary>
public interface IPartiallyDeserializedAttributesTable : IAttributesTable
{
/// <summary>
/// Attempts to convert this entire table to a strongly-typed CLR object.
/// <para>
/// Modifications to the result <b>WILL NOT</b> propagate back to this table, or vice-versa.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to convert to.
/// </typeparam>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
bool TryDeserializeJsonObject<T>(JsonSerializerOptions options, out T deserialized);

/// <summary>
/// Attempts to get a strongly-typed CLR object that corresponds to a single property that's
/// present in this table.
/// <para>
/// Modifications to the result <b>WILL NOT</b> propagate back to this table, or vice-versa.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to retrieve.
/// </typeparam>
/// <param name="propertyName">
/// The name of the property in this table to get as the specified type.
/// </param>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
bool TryGetJsonObjectPropertyValue<T>(string propertyName, JsonSerializerOptions options, out T deserialized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;

using NetTopologySuite.IO.Converters;

namespace NetTopologySuite.Features
{
internal sealed class JsonArrayInAttributesTableWrapper : IList<object>, IReadOnlyList<object>
{
private readonly JsonArray _array;

private readonly JsonSerializerOptions _serializerOptions;

public JsonArrayInAttributesTableWrapper(JsonArray array, JsonSerializerOptions serializerOptions)
{
_array = array;
_serializerOptions = serializerOptions;
}

public object this[int index]
{
get => Utility.ObjectFromJsonNode(_array[index], _serializerOptions);
set => _array[index] = Utility.ObjectToJsonNode(value, _serializerOptions);
}

public int Count => _array.Count;

bool ICollection<object>.IsReadOnly => false;

public void Add(object item)
{
_array.Add(Utility.ObjectToJsonNode(item, _serializerOptions));
}

public void Clear()
{
_array.Clear();
}

public bool Contains(object item)
{
foreach (JsonNode node in _array)
{
object obj = Utility.ObjectFromJsonNode(node, _serializerOptions);
if (Equals(item, obj))
{
return true;
}
}

return false;
}

public void CopyTo(object[] array, int arrayIndex)
{
foreach (JsonNode node in _array)
{
array[arrayIndex++] = Utility.ObjectFromJsonNode(node, _serializerOptions);
}
}

public IEnumerator<object> GetEnumerator()
{
return _array.Select(node => Utility.ObjectFromJsonNode(node, _serializerOptions)).GetEnumerator();
}

public int IndexOf(object item)
{
for (int i = 0; i < _array.Count; i++)
{
object obj = Utility.ObjectFromJsonNode(_array[i], _serializerOptions);
if (Equals(item, obj))
{
return i;
}
}

return -1;
}

public void Insert(int index, object item)
{
_array.Insert(index, Utility.ObjectToJsonNode(item, _serializerOptions));
}

public bool Remove(object item)
{
for (int i = 0; i < _array.Count; i++)
{
object obj = Utility.ObjectFromJsonNode(_array[i], _serializerOptions);
if (Equals(item, obj))
{
_array.RemoveAt(i);
return true;
}
}

return false;
}

public void RemoveAt(int index)
{
_array.RemoveAt(index);
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace NetTopologySuite.Features
/// JSON <see cref="JsonValueKind.Object"/>-valued properties are wrapped in their own
/// <see cref="JsonElementAttributesTable"/> objects.
/// </remarks>
public sealed class JsonElementAttributesTable : IAttributesTable
public sealed class JsonElementAttributesTable : IPartiallyDeserializedAttributesTable
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonElementAttributesTable"/> class.
Expand Down Expand Up @@ -119,62 +119,13 @@ public object[] GetValues()
.ToArray();
}

/// <summary>
/// Attempts to convert this table to a strongly-typed value.
/// <para>
/// This is essentially just a way of calling
/// <see cref="JsonSerializer.Deserialize{TValue}(ref Utf8JsonReader, JsonSerializerOptions)"/>
/// on a Feature's <c>"properties"</c> object.
/// </para>
/// <para>
/// <c>System.Text.Json</c> intentionally omits the functionality that would let us do this
/// automatically, for security reasons, so this is the workaround for now.
/// </para>.
/// </summary>
/// <typeparam name="T">
/// The type of object to convert to.
/// </typeparam>.
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
/// <inheritdoc />
public bool TryDeserializeJsonObject<T>(JsonSerializerOptions options, out T deserialized)
{
return TryDeserializeElement(this.RootElement, options, out deserialized);
}

/// <summary>
/// Attempts to get a strongly-typed value for that corresponds to a property of this table.
/// <para>
/// This is essentially just a way of calling
/// <see cref="JsonSerializer.Deserialize{TValue}(ref Utf8JsonReader, JsonSerializerOptions)"/>
/// on one of the individual items from a Feature's <c>"properties"</c>.
/// </para>
/// <para>
/// <c>System.Text.Json</c> intentionally omits the functionality that would let us do this
/// automatically, for security reasons, so this is the workaround for now.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to retrieve.
/// </typeparam>
/// <param name="propertyName">
/// The name of the property in this table to get as the specified type.
/// </param>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
/// <inheritdoc />
public bool TryGetJsonObjectPropertyValue<T>(string propertyName, JsonSerializerOptions options, out T deserialized)
{
if (!this.RootElement.TryGetProperty(propertyName, out var elementToTransform))
Expand Down Expand Up @@ -582,7 +533,7 @@ private static bool TryDeserializeElement<T>(JsonElement elementToTransform, Jso
}
}

private static object ConvertValue(JsonElement prop)
internal static object ConvertValue(JsonElement prop)
{
switch (prop.ValueKind)
{
Expand Down
Loading

0 comments on commit 28502d5

Please sign in to comment.