Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is to possible to resolve deserialized objects to their line/ char positions? #590

Open
MaorDavidzon opened this issue Mar 3, 2021 · 3 comments

Comments

@MaorDavidzon
Copy link

Is it possible to achieve that if I will implement INodeDeserializer ?

@EdwardCooke
Copy link
Collaborator

Can you give more details on your use case and what you’re looking for?

@djluck
Copy link

djluck commented Sep 6, 2022

I'd also like to request this feature. As a workaround, I've developed the following approach:

void Main()
{
    var content = @"
---
my_obj:
  name: jimmy
  # double jimmy proves same string contents at different locations works
  lastname: jimmy
  # Won't work on value types!
  age: 15
  string_list:
    - item1
    - item2
  object_list:
    - key: one
      value: a
    - key: two
      value: |
        multine
        value
";
    var (obj, locations) = YamlSerializer.DeserializeWithLocator<Dictionary<string, object>>(new StringReader(content));
    obj.Dump();
	locations.Dump();
	
}

public static class YamlSerializer
{
    public static (T result, Dictionary<object, Location> locator) DeserializeWithLocator<T>(TextReader reader)
    {
        var locations = new Dictionary<object, Location>(ReferenceEqualityComparer.Instance);

        var parser = new PreviousEventParser(new Parser(reader));
        var yamlDeserializer = new DeserializerBuilder()
            .WithNodeDeserializer(
                nd => new NodeLocationDeserializer<ObjectNodeDeserializer>(nd, locations),
                x => x.InsteadOf<ObjectNodeDeserializer>())
            .WithNodeDeserializer(nd => new NodeLocationDeserializer<DictionaryNodeDeserializer>(nd, locations),
                x => x.InsteadOf<DictionaryNodeDeserializer>())
            .WithNodeDeserializer(nd => new NodeLocationDeserializer<ArrayNodeDeserializer>(nd, locations),
                x => x.InsteadOf<ArrayNodeDeserializer>())
            .WithNodeDeserializer(nd => new NodeLocationDeserializer<CollectionNodeDeserializer>(nd, locations),
                x => x.InsteadOf<CollectionNodeDeserializer>())
            .WithNodeDeserializer(nd => new NodeLocationDeserializer<EnumerableNodeDeserializer>(nd, locations),
				x => x.InsteadOf<EnumerableNodeDeserializer>())
				.WithNodeDeserializer(nd => new NodeLocationDeserializer<ScalarNodeDeserializer>(nd, locations),
				x => x.InsteadOf<ScalarNodeDeserializer>())
			.Build();
            
        var result = yamlDeserializer.Deserialize<T>(parser);
        return (result: result, locations);
    }

    /// <summary>
    /// As the YAML parser is advanced to the next unconsumable event for a <see cref="INodeDeserializer"/>, in order to generate
    /// accurate location information we need a parser that can track the last successfully consumed event. 
    /// </summary>
    public class PreviousEventParser : IParser
    {
        private readonly IParser _wrapped;

        public PreviousEventParser(IParser wrapped)
        {
            _wrapped = wrapped;
        }

        public bool MoveNext()
        {
            Previous = Current;
            return _wrapped.MoveNext();
        }

        public ParsingEvent Previous { get; private set; }
        public ParsingEvent Current => _wrapped.Current;

        ParsingEvent IParser.Current => _wrapped.Current;
    }

    /// <summary>
    /// Wraps an existing <see cref="INodeDeserializer"/>, extracting <see cref="Location"/> information for deserialized objects
    /// </summary>
    /// <typeparam name="T">This generic type is indirectly used to ensure multiple location deserializers can be registered.</typeparam>
    public class NodeLocationDeserializer<T> : INodeDeserializer
        where T : INodeDeserializer
    {
        private readonly INodeDeserializer _wrapped;
        private readonly Dictionary<object, Location> _locations;

        public NodeLocationDeserializer(INodeDeserializer wrapped, Dictionary<object, Location> locations)
        {
            _wrapped = wrapped;
            _locations = locations;
        }

        public bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object> nestedObjectDeserializer, out object value)
        {
            var start = reader.Current.Start;
            var success = _wrapped.Deserialize(reader, expectedType, nestedObjectDeserializer, out value);
            if (success)
            {
                var positionParser = reader as PreviousEventParser;
                var end = positionParser != null ? positionParser.Previous.End : reader.Current.Start;
                var location = new Location(new Position(start.Line, start.Column), new Position(end.Line, end.Column));

                _locations.TryAdd(value, location);
            }

            return success;
        }
    }

    public record Position(int Line, int Column);
	
    public record Location(Position From, Position To);
}

The idea of this code is that we wrap the various NodeDeserializer subclasses with NodeLocationDeserializer- this allows us to extrac the location of deserialized keys and objects. It's not perfect (repeated value types will share the same location) but it's something.

I'm sure this could be refined somehow, be interested in hearing other peoples thoughts on this approach.

@djluck
Copy link

djluck commented Sep 12, 2022

I ended up writing a project to help achieve the request- https://github.com/djluck/YamlDotNet.Locations. it's an early release so any feedback would be appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants