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

Introduce support for deserializing via annotated constructor #653

Closed
wants to merge 1 commit into from

Conversation

Arakade
Copy link

@Arakade Arakade commented Nov 20, 2021

Introduce support for deserializing via annotated constructor call to allow read-only fields and properties.

Annotate a constructor with the new YamlConstructorAttribute and it will be used in preference to property and field setting.

Unit test added. It's largely copied from ObjectNodeDeserializer plus some tests to prove certain desirable behaviours. It's showing 81% coverage of the new code AnnotatedConstructorObjectNodeDeserializer. Missed lines are all corner cases (errors, the IValuePromise bits and non-use code-paths).

@aaubry
Copy link
Owner

aaubry commented Nov 22, 2021

Hi! Thanks for this. I did some experiments and overall it's working quite well.

I found some problems, though:

  • When there is only one constructor on the class, I think the YamlConstructor attribute should not be necessary. This would reduce the friction and make the feature easier to use.
  • This solution does not allow to assign properties in addition to calling the constructor:
    [Fact]
    public void Deserialize_using_constructor_allows_properties()
    {
        var yaml = Yaml.Text(@"
            readOnly: from constructor
            readWrite: from property
        ");
        var sut = new DeserializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .Build();
        var result = sut.Deserialize<TypeWithConstructorAndProperties>(yaml);
        Assert.Equal("from constructor", result.ReadOnly);
        Assert.Equal("from property", result.ReadWrite);
    }
    
    class TypeWithConstructorAndProperties
    {
        [YamlConstructor]
        public TypeWithConstructorAndProperties(string readOnly)
        {
            ReadOnly = readOnly;
        }
        public string ReadOnly { get; }
        public string? ReadWrite { get; set; }
    }
  • It doesn't play well with naming conventions:
    [Fact]
    public void Deserialize_using_constructor_allows_naming_conventions()
    {
        var yaml = Yaml.Text(@"
            name: Jack
            moment_of_birth: 1983-04-21T20:21:03.0041599Z
            cars:
            - name: Mercedes
              year: 2018
        ");
    
        var sut = new DeserializerBuilder()
            .WithNamingConvention(UnderscoredNamingConvention.Instance)
            .WithTypeMapping<ICar, Car>()
            .Build();
    
        var person = sut.Deserialize<Person>(yaml);
        person.Name.Should().Be("Jack");
        person.MomentOfBirth.Kind.Should().Be(DateTimeKind.Utc);
        person.MomentOfBirth.ToUniversalTime().Year.Should().Be(1983);
        person.MomentOfBirth.ToUniversalTime().Month.Should().Be(4);
        person.MomentOfBirth.ToUniversalTime().Day.Should().Be(21);
        person.MomentOfBirth.ToUniversalTime().Hour.Should().Be(20);
        person.MomentOfBirth.ToUniversalTime().Minute.Should().Be(21);
        person.MomentOfBirth.ToUniversalTime().Second.Should().Be(3);
    }

I also tested anchors and even indirect circular references, which work correctly.

Let me know what are your thoughts on the above. Thanks

@Arakade
Copy link
Author

Arakade commented Nov 22, 2021

Hi! Thanks for this. I did some experiments and overall it's working quite well.

Great!

I'll reply to each inline but the summary would be:
I agree on all points and propose we add the functionality as is for now then extend it with the extra bits later.

* When there is only one constructor on the class, I think the `YamlConstructor` attribute should not be necessary. This would reduce the friction and make the feature easier to use.

Agreed. How would you prefer we determine the need to use the constructor (rather than ObjectNodeDeserializer)?
Perhaps: If a sole constructor exists and more than zero arguments?

* This solution does not allow to assign properties in addition to calling the constructor:

Funny you should mention that, I was thinking exactly the same right after submitting my PR! 😁
My use cases so far have all been read-only objects.

Would you prefer adding the needed functionality to the new AnnotatedConstructorObjectNodeDeserializer, probably duplicating it straight from ObjectNodeDeserializer or... ?

* It doesn't play well with naming conventions:

Fair. I figured the best way to address would be a PropertyDescriptor (IIRC) that wraps a constructor argument then duplicate similar code from the ObjectNodeDeserializer. Sound reasonable?

I also tested anchors and even indirect circular references, which work correctly.

Ooh! Great. I hadn't looked at either of those so that's great to hear!

With my local copy sufficing for my needs for now, I've moved away from the YamlDotNet source for now. For this reason I worry that if we block integration of the functionality as is, it just delays its use by others. Depending on workload and complexity of solutions proposed, I'm happy to add the extra bits above but I hesitate to commit to timeline above "the next few months". Perhaps create GH "Issue" for all/each and assign to me and I'll pick them off as I go (or on-demand if users vote their desire)?

Thanks for such a useful library btw! We're prototyping using it as persistence format for an indie game we're developing.
TTYS.

@Arakade
Copy link
Author

Arakade commented Jan 23, 2022

Doing a little more work on this. Updates inline.

* When there is only one constructor on the class, I think the `YamlConstructor` attribute should not be necessary. This would reduce the friction and make the feature easier to use.

Agreed. How would you prefer we determine the need to use the constructor (rather than ObjectNodeDeserializer)? Perhaps: If a sole constructor exists and more than zero arguments?

I have this working locally now.

* This solution does not allow to assign properties in addition to calling the constructor:

Funny you should mention that, I was thinking exactly the same right after submitting my PR! 😁 My use cases so far have all been read-only objects.

Would you prefer adding the needed functionality to the new AnnotatedConstructorObjectNodeDeserializer, probably duplicating it straight from ObjectNodeDeserializer or... ?

Working on this next.

* It doesn't play well with naming conventions:

Fair. I figured the best way to address would be a PropertyDescriptor (IIRC) that wraps a constructor argument then duplicate similar code from the ObjectNodeDeserializer. Sound reasonable?

Not yet looked at this. Might you accept an updated PR that contains the other two to start?

@aaubry aaubry mentioned this pull request Apr 29, 2022
@aaubry aaubry force-pushed the master branch 8 times, most recently from a0f8359 to 78b1ab3 Compare December 2, 2022 22:39
@EdwardCooke
Copy link
Collaborator

This PR appears to be abandoned, it's nearly a year old. I'm going to close it.

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

Successfully merging this pull request may close these issues.

None yet

3 participants