dezhidki / Tommy

A single-file TOML reader and writer for C#
MIT License
212 stars 16 forks source link

Deserialize directly to a class? #9

Open MostHated opened 3 years ago

MostHated commented 3 years ago

Hey there, I was just wondering if it were possible to read/write a file directly to/from a class, or am I going to have to manually assign each field? I didn't specifically see anything about it when I looked, but just wanted to make sure I didn't miss it somewhere if the functionality exists.

Thanks, -MH

dezhidki commented 3 years ago

Hey,

thanks for the question! This library does not contain any helpers for deserializing TOML into an arbitrary object or vice-versa. This choice was inspired by SimpleJSON to keep the overall size of the library smaller. As with SimpleJSON, this library simply provides helpers to access/write values more easily, so that accesses like table["foo"]["bar"] where foo nor bar won't cause exceptions right away.

At the moment you would have to implement (de)serialization yourself, but I am considering extension methods for that.

MostHated commented 3 years ago

I appreciate the reply. It seemed like that was the case, but I just wanted to be sure, so no worries. 👍

MostHated commented 3 years ago

Late last night I decided to have a go at this to see if I could make it work by just passing in a class containing properties, as that is what I am using in my project at the moment. I started out making some basic attributes and just used a string and an int to test, but the results were promising, even if it ends up just being for my own use-case. I added several more types of test data and I am currently working my way through them to make sure I get the conversion working properly.

Code items ---
FromClass ```cs public static class ConfigUtils { public static void WriteToml() { var testData = new TestData(); TommyExtensions.FromClass(testData, "testdata.toml"); } } ```

Attributes ```cs [TommyTableName("nametest")] public class TestData { [TommyComment(" Comment for string property")] public string TestString { get; set; } = "Test String"; [TommyComment(" Comment for int property")] public int TestInt { get; set; } = 1; } ```

Test Results - testdata.toml ```toml [nametest] # Comment for string property TestString = "Test String" # Comment for int property TestInt = 1 ```

Currently testing conversions ```cs [TommyTableName("nametest")] public class TestData { [TommyComment(" Comment for string property")] public string TestString { get; set; } = "Test String"; [TommyComment(" Comment for int property")] public int TestInt { get; set; } = 1; [TommyComment(" Comment for ulong property")] public ulong TestUlong { get; set; } = 12345678901234567890; [TommyComment(" Comment for float property")] public float TestFloat { get; set; } = 123.123f; [TommyComment(" Comment for double property")] public double TestDouble { get; set; } = 1234.123; [TommyComment(" Comment for decimal property")] public decimal TestDecimal { get; set; } = new decimal(0.11); [TommyComment(" Comment for IntArray property")] public int[] TestIntArray { get; set; } = new[] {1, 2, 3, 4}; [TommyComment(" Comment for List property")] public List TestStringList { get; set; } = new List {"string1", "string2", "string3"}; } ```

MostHated commented 3 years ago

First comment

It didn't quite come out properly, but I expected as much. I am going to have to do some more digging and testing to figure out the best way to handle the data type conversion for long and float, as well as handling collections.

First try ```toml [nametest] # Comment for string property TestString = "Test String" # Comment for int property TestInt = 1 # Comment for ulong property TestUlong = 1.234567890123457E+19 # Comment for float property TestFloat = 123.12300109863281 # Comment for double property TestDouble = 1234.123 # Comment for decimal property TestDecimal = 0.11 ```

Edited: Second comment

After some more testing, I was able to get collections to work in the second section. Just need to get the actual number conversions to work correctly. Though, there seems to be some inconsistencies with the spacing between the comments/items, though. I am not sure exactly why yet. I was just following the example when writing it to file, so it must be how I am creating the nodes and adding them to the TomlTable.

(For good measure I added a second item for each one without a comment just to make sure all was working well)

Second test Data: ```cs [TommyTableName("nametest")] public class TestData { [TommyComment(" Comment for string property")] public string TestStringComment { get; set; } = "Test String"; public string TestString { get; set; } = "Test String"; [TommyComment(" Comment for int property")] public int TestIntComment { get; set; } = 1; public int TestInt { get; set; } = 1; [TommyComment(" Comment for ulong property")] public ulong TestUlongComment { get; set; } = 12345678901234567890; public ulong TestUlong { get; set; } = 12345678901234567890; [TommyComment(" Comment for float property")] public float TestFloatComment { get; set; } = 123.123f; public float TestFloat { get; set; } = 123.123f; [TommyComment(" Comment for double property")] public double TestDoubleComment { get; set; } = 1234.123; public double TestDouble { get; set; } = 1234.123; [TommyComment(" Comment for decimal property")] public decimal TestDecimalComment { get; set; } = new decimal(0.11); public decimal TestDecimal { get; set; } = new decimal(0.11); [TommyComment(" Comment for IntArray property")] public int[] TestIntArrayComment { get; set; } = new[] {1, 2, 3, 4}; public int[] TestIntArray { get; set; } = new[] {1, 2, 3, 4}; [TommyComment(" Comment for List property")] public List TestStringListComment { get; set; } = new List {"string1", "string2", "string3"}; public List TestStringList { get; set; } = new List {"string1", "string2", "string3"}; } ``` Result: ```toml [nametest] # Comment for string property TestStringComment = "Test String" TestString = "Test String" # Comment for int property TestIntComment = 1 TestInt = 1 # Comment for ulong property TestUlongComment = 1.234567890123457E+19 TestUlong = 1.234567890123457E+19 # Comment for float property TestFloatComment = 123.12300109863281 TestFloat = 123.12300109863281 # Comment for double property TestDoubleComment = 1234.123 TestDouble = 1234.123 # Comment for decimal property TestDecimalComment = 0.11 TestDecimal = 0.11 # Comment for IntArray property TestIntArrayComment = [ 1, 2, 3, 4 ] TestIntArray = [ 1, 2, 3, 4 ] # Comment for List property TestStringListComment = [ "string1", "string2", "string3" ] TestStringList = [ "string1", "string2", "string3" ] ```
MostHated commented 3 years ago

Here is what I have so far, would definitely appreciate any feedback (not completed yet, of course). I have got the current conversions all working properly now. As of this moment, I only added the things I was actually needing for my project. I just need to add the rest of the types, dictionaries, and some additional error checking, as well as supporting fields in addition to properties, etc. https://github.com/instance-id/TommyExtensions/blob/main/TommyExtensions/TommyExtensions.cs

Just did some multiline comment testing, seems to work out well. 👍

Multiline comments ```cs [TommyComment(" Comment for string property\n Testing second line comment")] public string TestStringComment { get; set; } = "Test String"; ``` ```toml [nametest] # Comment for string property # Testing second line comment TestStringComment = "Test String" ``` As well as: ```cs [TommyComment(@" If left as default (""LiteDB.db"") database will be located in the same folder as the bot's binary file. Note, the name can be anything, but path must include/end with a filename ending in the extension .db Ex: ""C:\database.db""")] public string DatabasePath { get; set; } ``` ```toml # If left as default ("LiteDB.db") database will be located in the same folder as the bot's binary file. # Note, the name can be anything, but path must include/end with a filename ending in the extension .db # Ex: "C:\database.db" DatabasePath = "Test String" ```
MostHated commented 3 years ago

Update:

For the most part, I have both passing an instance of a data class containing properties able to write to file, as well as passing the class type and path to read from disk, which then returns a new instance of the class.

Write:

var path = "TestData.toml";
var testData = new TestData();
testData.StringProperty = "A String";

TommyExtensions.ToTomlFile(testData, path);

Read:

var path = "TestData.toml";
TestData testData  = TommyExtensions.FromTomlFile<TestData>(path);

Edit


I believe last, but definitely not least, I have standard Dictionary<K,V> working both write and read. As of now, the final out put looks as follows.

Input class instance ```cs [TommyTableName("tablename")] public class TestData { [TommyIgnore] public string TestIgnoreProperty { get; set; } = "I should not show up in the created file"; [TommyComment(" Comment for date property")] public DateTime TestDateComment { get; set; } = DateTime.Now; [TommyComment(" Comment for Dictionary property")] public Dictionary TestDictionaryComment { get; set; } = new Dictionary{{"string1Key", "string1Value"}, {"string2Key", "string2Value"}}; [TommyComment(" Comment for string property\n Testing second line comment\n" + "This and subsequent items should appear after the sorted properties")] public string TestStringComment { get; set; } = "Test String"; [TommyComment(@" This item should be a blank string : Testing null value")] public string TestNullString { get; set; } [TommyComment(@" Comment testing multiline verbatim strings #1 Comment testing multiline verbatim strings #2 Comment testing multiline verbatim strings #3")] public string TestComment { get; set; } = "Test String"; [TommyComment(" Comment for bool property")] public bool TestBoolComment { get; set; } = true; public bool TestBool { get; set; } [TommyComment(" Comment for int property")] public int TestIntComment { get; set; } = 1; public int TestInt { get; set; } = 1; [TommySortOrder(1)] [TommyComment(@" Comment for ulong property This item should appear second as it's sort order is : 1")] public ulong TestUlongComment { get; set; } = 448543646457048970; public ulong TestUlong { get; set; } = 448543646457048970; [TommySortOrder(2)] [TommyComment(@" Comment for float property This item should appear third as it's sort order is : 2")] public float TestFloatComment { get; set; } = 123.123f; public float TestFloat { get; set; } = 123.123f; [TommyComment(" Comment for double property")] public double TestDoubleComment { get; set; } = 1234.123; public double TestDouble { get; set; } = 1234.123; [TommyComment(" Comment for decimal property")] public decimal TestDecimalComment { get; set; } = new decimal(0.11); public decimal TestDecimal { get; set; } = new decimal(0.11); [TommyComment(" Comment for IntArray property")] public int[] TestIntArrayComment { get; set; } = new[] {1, 2, 3, 4}; [TommySortOrder(0)] [TommyComment(@" This item should appear first as it's sort order is : 0")] public int[] TestIntArray { get; set; } = new[] {1, 2, 3, 4}; [TommyComment(@" Comment for List property")] public List TestStringListComment { get; set; } = new List {"string1", "string2", "string3"}; public List TestStringList { get; set; } = new List {"string1", "string2", "string3"}; [TommyComment(@" Comment for ulong array property")] public ulong[] TestULongArray { get; set; } = new ulong[] {448543646457048001, 448543646457048002, 448543646457048003, 448543646457048004}; [TommyComment(@" Comment for List property")] public List TestULongList { get; set; } = new List {448543646457048001, 448543646457048002, 448543646457048003}; } ```
Output file ```toml [tablename] # This item should appear first as it's sort order is : 0 TestIntArray = [ 1, 2, 3, 4 ] # Comment for ulong property # This item should appear second as it's sort order is : 1 TestUlongComment = 448543646457048970 # Comment for float property # This item should appear third as it's sort order is : 2 TestFloatComment = 123.12300109863281 # Comment for date property TestDateComment = 2020-12-10 01:15:36 # Comment for string property # Testing second line comment # This and subsequent items should appear after the sorted properties TestStringComment = "Test String" # This item should be a blank string : Testing null value TestNullString = "" # Comment testing multiline verbatim strings #1 # Comment testing multiline verbatim strings #2 # Comment testing multiline verbatim strings #3 TestComment = "Test String" # Comment for bool property TestBoolComment = true TestBool = false # Comment for int property TestIntComment = 1 TestInt = 1 TestUlong = 448543646457048970 TestFloat = 123.12300109863281 # Comment for double property TestDoubleComment = 1234.123 TestDouble = 1234.123 # Comment for decimal property TestDecimalComment = 0.11 TestDecimal = 0.11 # Comment for IntArray property TestIntArrayComment = [ 1, 2, 3, 4 ] # Comment for List property TestStringListComment = [ "string1", "string2", "string3" ] TestStringList = [ "string1", "string2", "string3" ] # Comment for ulong array property TestULongArray = [ 448543646457048001, 448543646457048002, 448543646457048003, 448543646457048004 ] # Comment for List property TestULongList = [ 448543646457048001, 448543646457048002, 448543646457048003 ] # Comment for Dictionary property [tablename.TestDictionaryComment] DictionaryKeys = [ "string1Key", "string2Key" ] DictionaryValues = [ "string1Value", "string2Value" ] ```
dezhidki commented 3 years ago

This looks awesome!

Are you looking into merging this into Tommy repo directly? If so, I think it'd be better if you instead created a fork and made a pull request as this thread is looking less like an issue and more like feature implementation.

Moreover, with pull requests I'd be able to see and comment on the code (again, assuming you are looking into actually getting this merged).

MostHated commented 3 years ago

Yeah, I would like to. I mostly just wanted to make sure I was doing things the proper and most efficient way, and things were working correctly before I did a PR. I still have a little cleanup to do, but as long as nothing crazy comes up, I should have that done tomorrow.

dezhidki commented 3 years ago

I mostly just wanted to make sure I was doing things the proper and most efficient way, and things were working correctly before I did a PR

In that case you can mark a PR as a draft. Either way, it would allow to review and comment the code and the PR in general. I will certainly do that once you actually post the PR, but it just may mean twice the work if there is something to change.

MostHated commented 3 years ago

That sounds perfect, I will do that. I am just trying to figure out what might be the best way to go about actually adding it to the current project. While I was building it I had a solution with a project containing the actual extensions and one for the demo that added reference to the extensions. https://github.com/instance-id/TommyExtensions

What do you feel would be ideal, adding a new file or folder somewhere, or just taking my new code and adding it to your current Tommy/TommyExtensions.cs?

dezhidki commented 3 years ago

Looking at the project you linked, I think there are two options both of which are fine to me

  1. You keep your serializer/deserializer in your own repo. In that case you'll be able to easy update it and you keep the code. In that case I would ask for the following changes:
    • Rename the repo to something more fitting, as there is already TommyExtensions file in this repo (which also gets packaged into the NuGet package)
    • Change target frameworks to net35;netstandard2 (or at least add net35 to the current list of targets) so that the project is usable everywhere alongside the main library
    • Optionally, push the package on NuGet once it's done and released
  2. Fork this repo and create a new separate project for the (de)serializer extension then make a PR. This way users who don't need the thing can leave it out easily and I'll be able to also build a separate NuGet package out of it.

If you go with the first option, I'll be more than happy to add a link to the final repo to the README of this repo so people can find it. Either way, I feel like it's better to keep the object (de)serializer as opt-in for users. Even right now I'm a bit unhappy with how I bundle TommyExtensions.cs with the main Tommy.cs in the NuGet package because that unnecessarily bloats the DLL for users who need simple low-level access to TOML.

Which of the two are you more comfortable with? As I mentioned, both work fine with me as long as in the end there will be a .cs file or a NuGet package one can easily include into the project alongside Tommy.cs to have the feature enabled.

MostHated commented 3 years ago

You will have to forgive me, some of this is relatively new to me as I primarily do my development in C# for the Unity game engine and it is usually just small projects I make myself, simply pushing to my repo and call it a day, so I have been trying to use this as a bit of a learning experience.

That said, ever since your reply I have been trying to get net35 to work/add as a target but for whatever reason, it's just not happening in either Rider or VS. Windows says I have it, VS Installer says I have it, Rider even shows it added under dependencies, but I keep getting messages saying I don't have it when I try to build. With netstandard 2.0 it works fine, but something is up with 3.5. I will keep trying to get it figured out.

If for whatever reason I just can't get it to work I will make a PR instead with only the extensions (I created a new solution when things weren't working with 3.5, and renamed it to Tommy.Serializer and I renamed the actual class to TommySerializer, with the methods still being ToTomlFile and FromTomlFile)

MostHated commented 3 years ago

Ok, finally got it worked out. Turns out, the issue ended up being not my IDE or configuration setup, but the .Net SDK itself. I kept trying to create new solutions, new projects, sometimes it might half work sometimes it would not even recognize the system assemblies, etc. I installed a different .Net SDK and then it worked almost straight away. I just had to make some slight adjustments to how I was getting attributes to work with .net 3.5, but as of now it is targeting and building for both 3.5 and netstandard2.0.