YamlDotNet icon indicating copy to clipboard operation
YamlDotNet copied to clipboard

Combination of EventEmitter and TypeConverter

Open hoyau opened this issue 4 years ago • 5 comments

Is there a way to combine a EventEmitter with a TypeConverter ?

The use case is that all string values should have quotes, and that a DateTime should be displayed without time.

To quote string values I'm using the approach from: https://github.com/aaubry/YamlDotNet/issues/428#issuecomment-525822859

The problem is, that the Custom ChainedEventEmitter isn't invoked when the DateTimeConverter detects a DateTime value, which leads to weird behavior:

Output:

StringValue: "2.0"
Date: 2020-04-02
"IntValue": 3
"Nested":
  StringValue1: "abc"
  StringArray:
  - "1.0"
  - "two"
  StringValue2: "abc"
"OtherStringValue": test

Desired Output:

StringValue: "2.0"
Date: "2020-04-02"
IntValue: 3
Nested:
  StringValue1: "abc"
  StringArray:
  - "1.0"
  - "two"
  StringValue2: "abc"
OtherStringValue: "test"

Example Code

using System;
    using System.Collections.Generic;
    using YamlDotNet.Core;
    using YamlDotNet.Serialization;
    using YamlDotNet.Serialization.Converters;
    using YamlDotNet.Serialization.EventEmitters;

    public class Program
    {
        public static void Main()
        {
            var serializer = new SerializerBuilder()
                .WithEventEmitter(next => new ForceQuotedStringValuesEventEmitter(next))
                .WithTypeConverter(new DateTimeConverter(DateTimeKind.Unspecified, null, new[] { "yyyy-MM-dd" }))
                .Build();

            serializer.Serialize(Console.Out, new
            {
                StringValue = "2.0",
                Date = new DateTime(2020, 4, 3),
                IntValue = 3,
                Nested = new
                {
                    StringValue1 = "abc",
                    StringArray = new[] { "1.0", "two" },
                    StringValue2 = "abc"
                },
                OtherStringValue = "test"
            });

            Console.ReadKey();
        }

        public class ForceQuotedStringValuesEventEmitter : ChainedEventEmitter
        {
            private class EmitterState
            {
                private int valuePeriod;
                private int currentIndex;

                public EmitterState(int valuePeriod)
                {
                    this.valuePeriod = valuePeriod;
                }

                public bool VisitNext()
                {
                    ++currentIndex;
                    return (currentIndex % valuePeriod) == 0;
                }
            }

            private readonly Stack<EmitterState> state = new Stack<EmitterState>();

            public ForceQuotedStringValuesEventEmitter(IEventEmitter nextEmitter) : base(nextEmitter)
            {
                this.state.Push(new EmitterState(1));
            }

            public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
            {
                if (this.state.Peek().VisitNext())
                {
                    if (eventInfo.Source.Type == typeof(string))
                    {
                        eventInfo.Style = ScalarStyle.DoubleQuoted;
                    }
                }

                base.Emit(eventInfo, emitter);
            }

            public override void Emit(MappingStartEventInfo eventInfo, IEmitter emitter)
            {
                this.state.Peek().VisitNext();
                this.state.Push(new EmitterState(2));
                base.Emit(eventInfo, emitter);
            }

            public override void Emit(MappingEndEventInfo eventInfo, IEmitter emitter)
            {
                this.state.Pop();
                base.Emit(eventInfo, emitter);
            }

            public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter)
            {
                this.state.Peek().VisitNext();
                this.state.Push(new EmitterState(1));
                base.Emit(eventInfo, emitter);
            }

            public override void Emit(SequenceEndEventInfo eventInfo, IEmitter emitter)
            {
                this.state.Pop();
                base.Emit(eventInfo, emitter);
            }
        }

    }

hoyau avatar Mar 04 '20 13:03 hoyau

That's not possible because IYamlTypeConverter does not use IEventEmitter due to legacy reasons. One option is to register your own implementation of DateTimeConverter that forces quotes.

aaubry avatar Mar 05 '20 15:03 aaubry

The problem remains, even if I register my own implementation of DateTimeConverter, the quotes will be set at the wrong place (because the EventEmitter isn't invoked).

StringValue: "2.0"
Date: "2020-04-02"
"IntValue": 3
"NextStringValue": anyString
"NextDate": "2020-04-03"
NextStringValue2: "anyString"

Is there a generic solution for this?

hoyau avatar Mar 06 '20 11:03 hoyau

I don't know how you got that result. Can you share the code ?

aaubry avatar Mar 06 '20 11:03 aaubry

Woops sorry, here is the example https://dotnetfiddle.net/3t83cZ

hoyau avatar Mar 06 '20 13:03 hoyau

Oh I see. Basically ForceQuotedStringValuesEventEmitter is a hack. It has to maintain state to know whether it is emitting a key or a value, and writing to the IEmitter directly messes with that. As you observed, it could work if DateTimeConverter could use IEventEmitter, but there's currently no good way to pass it around. If you're willing to make your code messy, you could capture the IEventEmitter and pass it to the DateTimeConverter, but I really don't recommend that:

IEventEmitter eventEmitter = null;
var serializer = new SerializerBuilder()
    .WithEventEmitter(next => eventEmitter = new ForceQuotedStringValuesEventEmitter(next))
    .WithTypeConverter(new CustomDateTimeConverter(() => eventEmitter))
    .Build();

aaubry avatar Mar 06 '20 20:03 aaubry