Skip to content

Commit

Permalink
Merge pull request #1 from djluck/map-partial-locations
Browse files Browse the repository at this point in the history
Extending `LocationResult` API to return information on the last succ…
  • Loading branch information
djluck authored Sep 19, 2022
2 parents ad998bb + b9ded6f commit e3d3962
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 193 deletions.
49 changes: 29 additions & 20 deletions src/YamlDotNet.Locations/Locator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using YamlDotNet.Core;
Expand All @@ -19,10 +20,12 @@ public interface ILocator<TDeserialized>
/// </summary>
/// <param name="Location">If <see cref="IsSuccess"/> is true, contains the location of the requested object.</param>
/// <param name="ErrorMessage">If <see cref="IsSuccess"/> is false, contains an error message describing why the query failed.</param>
public record struct LocationResult(Location? Location, string? ErrorMessage)
/// <param name="ExecutedOperations">Contains the query operations that we successfully executed. When <see cref="IsSuccess"/> is true, this is equal to all of the query operations specified.</param>
/// <param name="LastLocation">If <see cref="IsSuccess"/> is false, contains the last successfully resolved location. Can be null when <see cref="ExecutedOperations"/> is empty.</param>
public record struct LocationResult(Location? Location, ImmutableArray<IQueryOp> ExecutedOperations, string? ErrorMessage, Location? LastLocation = null)
{
public LocationResult(Location location) : this(location, null) {}
public LocationResult(string errorMessage) : this(null, errorMessage) {}
public LocationResult(Location location, ImmutableArray<IQueryOp> executedOperations) : this(location, executedOperations, null) {}
public LocationResult(string errorMessage, ImmutableArray<IQueryOp> executedOperations, Location? lastLocation) : this(null, executedOperations, errorMessage, lastLocation) {}

/// <summary>
/// Returns true if a location was successfully found.
Expand All @@ -36,8 +39,11 @@ public override string ToString()
if (IsSuccess)
return Location.ToString();

return $"Location query failed: {ErrorMessage}";
return $"Location query failed: {ErrorMessage}. {GetAttemptedOps()}";
}

private string GetAttemptedOps() =>
$"Executed query operations: {(ExecutedOperations.Length == 0 ? "<none>" : string.Join(" -> ", ExecutedOperations))}";
}

public record Location(Mark Start, Mark End)
Expand All @@ -57,23 +63,18 @@ public Locator(IYamlNode? root)
LocationResult ILocator<TDeserialized>.GetLocation(ICollection<IQueryOp> query)
{
OpResult GetMismatchedStructureResult(IQueryOp failedOp, IYamlNode node) => new OpResult(
$"Query did not match deserialized structure, expected {failedOp.GetType().Name.Replace("Query", "")} but found {node.GetType().Name}. "
+ GetExecutedOps(failedOp)
$"Query did not match deserialized structure, expected {failedOp.GetType().Name.Replace("Query", "")} but found {node.GetType().Name}"
);

string GetExecutedOps(IQueryOp failedOp)
{
var successfulOps = query.TakeWhile(x => x != failedOp).Select(x => x.ToString()).ToArray();
return $"Executed query operations: {(successfulOps.Length == 0 ? "<none>" : string.Join(" -> ", successfulOps))}";
}

if (query.Count < 1)
throw new ArgumentException("Must provide at least one query operation", nameof(query));

IYamlNode? current = _root;
IYamlNode? last = null;
if (current == null)
return new LocationResult("Deserialized object was null");

return new LocationResult("Deserialized object was null", ImmutableArray<IQueryOp>.Empty, null);

var executed = new List<IQueryOp>();
foreach (var op in query)
{
var next = op switch
Expand All @@ -82,30 +83,38 @@ string GetExecutedOps(IQueryOp failedOp)
current is Sequence s ?
s.TryGet(index, out var node) ?
new OpResult(node) :
new OpResult($"The requested sequence index '{index}' did not exist! " + GetExecutedOps(op))
new OpResult($"The requested sequence index '{index}' did not exist")
: GetMismatchedStructureResult(op, current),

QueryMap(object key) =>
current is Map m ?
m.TryGet(key, out var node) ?
new OpResult(node) :
new OpResult($"The requested map key '{key}' did not exist! " + GetExecutedOps(op))
new OpResult($"The requested map key '{key}' did not exist")
: GetMismatchedStructureResult(op, current),

QueryValue => new OpResult(current),
_ => throw new InvalidOperationException($"Unsupported query op '{op}'")
};

last = current;

if (!next.IsSuccess)
return new LocationResult(next.ErrorMessage);
return new LocationResult(next.ErrorMessage, executed.ToImmutableArray(), last == null ? null : ToLocation(last));

executed.Add(op);
current = next.Node;
}

if (!current.End.HasValue)
throw new InvalidOperationException("Node was partially located!");
return new LocationResult(ToLocation(current), executed.ToImmutableArray());
}

return new LocationResult(new Location(current.Start, current.End.Value));
private static Location ToLocation(IYamlNode n)
{
if (!n.End.HasValue)
throw new InvalidOperationException("Node was partially located!");

return new Location(n.Start, n.End.Value);
}

private record struct OpResult(IYamlNode? Node, string? ErrorMessage)
Expand Down
18 changes: 14 additions & 4 deletions tests/YamlDotNet.Locations.Tests/LocatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ public void Collection_Query_Over_Object_Fails()

result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("expected Sequence but found Map");
result.ExecutedOperations.Should().BeEmpty();
result.LastLocation.ToString().Should().Be("(2:1)-(6:1)");
}

[Test]
Expand All @@ -271,7 +273,9 @@ public void Object_Query_Over_Value_Fails()

result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("expected Map but found Scalar");
result.ErrorMessage.Should().Contain("Executed query operations: Map[Collection] -> Sequence[0]");
result.ToString().Should().Contain("Executed query operations: Map[Collection] -> Sequence[0]");
result.ExecutedOperations.Should().Equal(new QueryMap("Collection"), new QuerySequence(0));
result.LastLocation.ToString().Should().Be("(3:5)-(3:8)");
}

[Test]
Expand All @@ -284,7 +288,9 @@ public void Collection_Out_Of_Bounds_Fails()
});

result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("The requested sequence index '3' did not exist!");
result.ErrorMessage.Should().Contain("The requested sequence index '3' did not exist");
result.ExecutedOperations.Should().Equal(new QueryMap("Collection"));
result.LastLocation.ToString().Should().Be("(3:3)-(5:1)");
}

[Test]
Expand All @@ -296,7 +302,9 @@ public void Non_Existent_Map_Key_Fails()
});

result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("The requested map key 'IDontExist' did not exist!");
result.ErrorMessage.Should().Contain("The requested map key 'IDontExist' did not exist");
result.ExecutedOperations.Should().BeEmpty();
result.LastLocation.ToString().Should().Be("(2:1)-(6:1)");
}

[Test]
Expand All @@ -310,7 +318,9 @@ public void Object_Query_Over_Collection_Fails()

result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("expected Map but found Sequence");
result.ErrorMessage.Should().Contain("Executed query operations: Map[Collection]");
result.ToString().Should().Contain("Executed query operations: Map[Collection]");
result.ExecutedOperations.Should().Equal(new QueryMap("Collection"));
result.LastLocation.ToString().Should().Be("(3:3)-(5:1)");
}

private ILocator<T> Deserialize<T>(string yaml)
Expand Down
169 changes: 0 additions & 169 deletions tests/YamlDotNet.Locations.Tests/PerfTests.cs

This file was deleted.

0 comments on commit e3d3962

Please sign in to comment.