Giter VIP home page Giter VIP logo

phpserializernet's People

Contributors

sommmen avatar stringepsilon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

phpserializernet's Issues

[Chore] Complete the unit test reorganization.

There are still some tests floating around that need sorting into the proper folder structure:

  • DeserializeObjects.cs
  • PhpDynamicObjectTest.cs
  • SerializeObjects.cs
  • SerializePrimitives.cs
  • TestDictionaries.cs
  • TestStructs.cs

Some tests in those files might also be redundant to other tests written in the meantime or significantly overlap in scope.

Bug: v7.0.1 breaks existing code

Hiya,

Thanks for your last update.
Unfortunately it seems like it broke something. Parsing the sample below works in 7.0.0 but not in 7.0.1!

a:1:{i:0;a:11:{s:14:"content_length";s:2:"63";s:13:"content_width";s:2:"63";s:14:"content_height";s:3:"2.3";s:14:"content_weight";s:5:"7.913";s:9:"belt_size";s:5:"221.6";s:5:"items";a:4:{i:558710;s:1:"2";i:558709;s:1:"2";i:558708;s:1:"3";i:558711;s:1:"2";}s:6:"length";s:2:"71";s:5:"width";s:2:"68";s:6:"height";s:3:"7.3";s:6:"weight";s:5:"8.942";s:9:"packaging";a:3:{s:2:"id";s:6:"446368";s:4:"cost";s:1:"0";s:6:"weight";s:5:"1.029";}}}
Array
(
    [0] => Array
        (
            [content_length] => 63
            [content_width] => 63
            [content_height] => 2.3
            [content_weight] => 7.913
            [belt_size] => 221.6
            [items] => Array
                (
                    [558710] => 2
                    [558709] => 2
                    [558708] => 3
                    [558711] => 2
                )
            [length] => 71
            [width] => 68
            [height] => 7.3
            [weight] => 8.942
            [packaging] => Array
                (
                    [id] => 446368
                    [cost] => 0
                    [weight] => 1.029
                )
        )
)

v7.0.0
image

v7.0.1
image

Seems like maybe the pos is incemented too much looking at your last change?

image

Feature: Debug symbols package and sourcelink

Hello,

I'd love being able to debug the library on the fly in my app and the process seems simple enough.

https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg
https://github.com/dotnet/sourcelink/blob/main/README.md

The commandline option seems to work fine to generate a symbols package.

PM> cd PhpSerializerNET
PM> dotnet pack PhpSerializerNET.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  PhpSerializerNET -> C:\Repos\PhpSerializerNET\PhpSerializerNET\bin\Debug\net6.0\PhpSerializerNET.dll
  Successfully created package 'C:\Repos\PhpSerializerNET\PhpSerializerNET\bin\Debug\PhpSerializerNET.0.9.0.nupkg'.
  Successfully created package 'C:\Repos\PhpSerializerNET\PhpSerializerNET\bin\Debug\PhpSerializerNET.0.9.0.snupkg'.

Seems like you can also embed the dbg symbols which would be easier, but that would bump the package size a bit, hence why a seperate symbols package is recommended.

Note that symbol package isn't the only strategy to make the debug symbols available to the consumers of your library. It's also possible to embed them in the dll or exe with the following project property: embedded

BUG empty string cannot be deserialized to string

Hiya,

Got another bug after tying the empty string to default feature you introduced for me.

parsing:

a:1:{i:0;s:0:"";}

To

List<string>

image

Funnily enough Activator.CreateInstance<string>() apperently does not work, since strings are immutable and cannot be created.

Also right now Activator.CreateInstance creates a new type - this is not really the 'default' since the defautl for a reference type is null. That's just a remark i can deal with new or default (but not a crash like it does now).

Also see:
https://stackoverflow.com/a/353073/4122889

I think the same also applies to Guid - but i could use a nullable there and this should work fine as-is.. Will have to test that later.

I think we can add an exception for string now, e.g.

if (token.Value == "" && this._options.EmptyStringToDefault){
	
	if(targetType == typeof(string))
		return null;
		
	// Something for Guid?

	return Activator.CreateInstance(targetType);
}

Or we can return null for all reference types (like the SO does)

Using .net6 + v7.0.2 (latest)

Bug/feature can't parse empty string in array

Hiya,

I have a model setup to parse the following string:

    public class TaxesPhpModel
    {
        /// <summary>
        /// Index seems to be either 1 or 32?
        /// </summary>
        [PhpProperty("total")]
        public Dictionary<long, double> Total { get; set; }
    }


            var str5 = "a:1:{s:5:\"total\";a:1:{i:1;s:0:\"\";}}";
            var res5 = PhpSerialization.Deserialize<TaxesPhpModel>(str5, new PhpDeserializationOptions()
            {
                AllowExcessKeys = true
            });

Which deals with strings like:

a:1:{s:5:"total";a:1:{i:1;s:6:"1.3205";}}

And

a:1:{s:5:"total";a:1:{i:32;s:6:"1.3205";}}

(for some reason i get integers 1 or 32 for the array... man php is weird)

But now it throws errors for the following string:

a:1:{s:5:"total";a:1:{i:1;s:0:"";}}
Array
(
    [total] => Array
        (
            [1] => 
        )
)

Because total is an empty string, which it cannot assign to a value. I've tried double and double? .

This works for Dictionary<string, string> or Dictionary<long, string>.

It would be nice if i could get the default value for an empty string so i'm not forced to use string rather than double.

net core 5 v7.0.0

Test sample:

    public class TaxesPhpModel
    {
        /// <summary>
        /// Index seems to be either 1 or 32?
        /// </summary>
        [PhpProperty("total")]
        public Dictionary<long, double> Total { get; set; }
    }

    public class TaxesPhpModel2
    {
        /// <summary>
        /// Index seems to be either 1 or 32?
        /// </summary>
        [PhpProperty("total")]
        public Dictionary<long, string> Total { get; set; }
    }
    public class TaxesPhpModel3
    {
        /// <summary>
        /// Index seems to be either 1 or 32?
        /// </summary>
        [PhpProperty("total")]
        public Dictionary<string, string> Total { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {

            var str5 = "a:1:{s:5:\"total\";a:1:{i:1;s:0:\"\";}}";

            var phpDeserializationOptions = new PhpDeserializationOptions()
            {
                AllowExcessKeys = true
            };

            var res4 = PhpSerialization.Deserialize(str5); // Works
            var res3 = PhpSerialization.Deserialize<TaxesPhpModel3>(str5, phpDeserializationOptions); // Works
            var res2 = PhpSerialization.Deserialize<TaxesPhpModel2>(str5, phpDeserializationOptions); // Doesnt work
            var res = PhpSerialization.Deserialize<TaxesPhpModel>(str5, phpDeserializationOptions); // Doesnt work
        }
    }

Support arrays

Failing test:

[TestMethod]
public void ExplicitToArray() {
	var result = PhpSerialization.Deserialize<string[]>("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}");

	CollectionAssert.AreEqual(new string[]{ "Hello", "World", "12345" }, result);
}

Basically: Support object[] and so on.

IPhpObject serialization is not consistent.

Failing test:

public class MyPhpObject : IPhpObject {
	public string GetClassName() => "MyPhpObject";
	public void SetClassName(string className) {};
	public string Foo {get;set;}
}

[TestMethod]
public void SerializeIPhpObject() {
	Assert.AreEqual( // strings:
		"O:11:\"MyPhpObject\":{s:3:\"Foo\";s:0:\"\"}",
		PhpSerialization.Serialize( new MyPhpObject() {Foo =""})
	);
}

Noticed this while writing the documentation for the interface.

[Feature request] Attribute similar to `PhpIgnore` but more like "ignore if null"

I'm looking to have an object that I'm using as a data model which will eventually be serialized. But I want to mark some fields as ignored, but not PhpIgnore (which skips it unconditionally) but something along the lines of "Ignore, but only if it's null".

For example:

public class MyClass {
    [PhpProperty("Foo")]
    public string Foo {get;set;};
    
    // ... Other stuff
}

If Foo is null, it will serialize along the lines of: s:3:"Foo";N;

I would like this output to be skipped entirely if it is null.

Maybe something like

public class MyClass {
    [PhpProperty("Foo")]
    [PhpIgnoreNull]   // Or similar
    public string Foo {get;set;};
    
    // ... Other stuff
}

BUG property not being set under certain conditions

Hiya,

I just encountered something weird.

I'm parsing this string which has different objects:

a:11:{i:0;O:31:"KPS\Logistics\Status\To\Delayed":5:{s:12:"delay_reason";s:12:"out_of_stock";s:11:"delay_until";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-28 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:4:"from";s:7:"delayed";s:2:"to";s:7:"delayed";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-04 07:57:02.880620";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:1;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:10:"processing";s:2:"to";s:10:"processing";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-28 06:59:16.556841";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:2;O:31:"KPS\Logistics\Status\To\Delayed":5:{s:12:"delay_reason";s:12:"out_of_stock";s:11:"delay_until";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:4:"from";s:7:"delayed";s:2:"to";s:7:"delayed";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-28 08:54:25.005632";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:3;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:10:"processing";s:2:"to";s:10:"processing";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-28 08:54:25.037250";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:4;O:31:"KPS\Logistics\Status\To\Delayed":5:{s:12:"delay_reason";s:12:"out_of_stock";s:11:"delay_until";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:4:"from";s:7:"delayed";s:2:"to";s:7:"delayed";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-10-28 11:25:48.219544";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:5;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:10:"processing";s:2:"to";s:10:"processing";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 07:04:58.147908";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:6;O:31:"KPS\Logistics\Status\To\Delayed":5:{s:12:"delay_reason";s:12:"out_of_stock";s:11:"delay_until";O:8:"DateTime":3:{s:4:"date";s:26:"2021-12-24 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:4:"from";s:7:"delayed";s:2:"to";s:7:"delayed";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 13:50:07.229584";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:7;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:10:"processing";s:2:"to";s:10:"processing";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 13:50:07.262880";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:8;O:31:"KPS\Logistics\Status\To\Delayed":5:{s:12:"delay_reason";s:12:"out_of_stock";s:11:"delay_until";O:8:"DateTime":3:{s:4:"date";s:26:"2021-12-24 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:4:"from";s:7:"delayed";s:2:"to";s:7:"delayed";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-25 13:50:16.825377";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:9;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:8:"canceled";s:2:"to";s:8:"canceled";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-26 09:06:38.775093";s:13:"timezone_type";i:3;s:8:"timezone";s:16:"Europe/Amsterdam";}}i:10;O:32:"KPS\Logistics\Status\To\Canceled":4:{s:2:"to";s:8:"canceled";s:13:"cancel_reason";s:6:"refund";s:4:"from";s:8:"canceled";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-11-26 08:06:38.796815";s:13:"timezone_type";i:3;s:8:"timezone";s:3:"UTC";}}}
Array
(
    [0] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Delayed
            [delay_reason] => out_of_stock
            [delay_until] => DateTime Object
                (
                    [date] => 2021-10-28 00:00:00.000000
                    [timezone_type] => 1
                    [timezone] => +00:00
                )

            [from] => delayed
            [to] => delayed
            [date] => DateTime Object
                (
                    [date] => 2021-10-04 07:57:02.880620
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [1] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Regular
            [from] => processing
            [to] => processing
            [date] => DateTime Object
                (
                    [date] => 2021-10-28 06:59:16.556841
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [2] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Delayed
            [delay_reason] => out_of_stock
            [delay_until] => DateTime Object
                (
                    [date] => 2021-11-25 00:00:00.000000
                    [timezone_type] => 1
                    [timezone] => +00:00
                )

            [from] => delayed
            [to] => delayed
            [date] => DateTime Object
                (
                    [date] => 2021-10-28 08:54:25.005632
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [3] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Regular
            [from] => processing
            [to] => processing
            [date] => DateTime Object
                (
                    [date] => 2021-10-28 08:54:25.037250
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [4] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Delayed
            [delay_reason] => out_of_stock
            [delay_until] => DateTime Object
                (
                    [date] => 2021-11-25 00:00:00.000000
                    [timezone_type] => 1
                    [timezone] => +00:00
                )

            [from] => delayed
            [to] => delayed
            [date] => DateTime Object
                (
                    [date] => 2021-10-28 11:25:48.219544
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [5] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Regular
            [from] => processing
            [to] => processing
            [date] => DateTime Object
                (
                    [date] => 2021-11-25 07:04:58.147908
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [6] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Delayed
            [delay_reason] => out_of_stock
            [delay_until] => DateTime Object
                (
                    [date] => 2021-12-24 00:00:00.000000
                    [timezone_type] => 1
                    [timezone] => +00:00
                )

            [from] => delayed
            [to] => delayed
            [date] => DateTime Object
                (
                    [date] => 2021-11-25 13:50:07.229584
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [7] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Regular
            [from] => processing
            [to] => processing
            [date] => DateTime Object
                (
                    [date] => 2021-11-25 13:50:07.262880
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [8] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Delayed
            [delay_reason] => out_of_stock
            [delay_until] => DateTime Object
                (
                    [date] => 2021-12-24 00:00:00.000000
                    [timezone_type] => 1
                    [timezone] => +00:00
                )

            [from] => delayed
            [to] => delayed
            [date] => DateTime Object
                (
                    [date] => 2021-11-25 13:50:16.825377
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [9] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Regular
            [from] => canceled
            [to] => canceled
            [date] => DateTime Object
                (
                    [date] => 2021-11-26 09:06:38.775093
                    [timezone_type] => 3
                    [timezone] => Europe/Amsterdam
                )

        )

    [10] => __PHP_Incomplete_Class Object
        (
            [__PHP_Incomplete_Class_Name] => KPS\Logistics\Status\To\Canceled
            [to] => canceled
            [cancel_reason] => refund
            [from] => canceled
            [date] => DateTime Object
                (
                    [date] => 2021-11-26 08:06:38.796815
                    [timezone_type] => 3
                    [timezone] => UTC
                )

        )

)

This is my model:

public class OrderItemStatusLogWpModel : IPhpObject
{
    private string? _className;

    [PhpProperty("cancel_reason")]
    public string? CancelReason { get; set; }

    [PhpProperty("delay_reason")]
    public string? DelayReason { get; set; }

    [PhpProperty("delay_until")]
    public PhpDateTime? DelayUntil { get; set; }

    [PhpProperty("from")]
    public string? From { get; set; }

    [PhpProperty("to")]
    public string? To { get; set; }

    [PhpProperty("date")]
    public PhpDateTime? Date { get; set; }

    #region IPhpObject

    public string GetClassName() => _className ?? throw new InvalidOperationException($"{nameof(_className)} was not set yet, call {nameof(SetClassName)} first!");

    public void SetClassName(string className) =>
        _className = className;

    #endregion
}

image

Now i remove the CancelReason property, and run the same code:

public class OrderItemStatusLogWpModel : IPhpObject
{
    private string? _className;

    //[PhpProperty("cancel_reason")]
    //public string? CancelReason { get; set; }

    [PhpProperty("delay_reason")]
    public string? DelayReason { get; set; }

    [PhpProperty("delay_until")]
    public PhpDateTime? DelayUntil { get; set; }

    [PhpProperty("from")]
    public string? From { get; set; }

    [PhpProperty("to")]
    public string? To { get; set; }

    [PhpProperty("date")]
    public PhpDateTime? Date { get; set; }

    #region IPhpObject

    public string GetClassName() => _className ?? throw new InvalidOperationException($"{nameof(_className)} was not set yet, call {nameof(SetClassName)} first!");

    public void SetClassName(string className) =>
        _className = className;

    #endregion
}

And now suddenly the Date property is not being set:

image

It seems to only be the last entry that is having difficulties:

image

Running v0.10.0

[Bug] Double: Infinity values don't work with explicit typing.

Failing tests:

[TestMethod]
public void Explicit_DeserializesInfinity() {
	Assert.AreEqual(
		double.PositiveInfinity,
		PhpSerialization.Deserialize<double>("d:INF;")
	);
}

[TestMethod]
public void Explicit_DeserializesNegativeInfinity() {
	Assert.AreEqual(
		double.NegativeInfinity,
		PhpSerialization.Deserialize<double>("d:-INF;")
	);
}

[TestMethod]
public void Explicit_Nullable_DeserializesInfinity() {
	Assert.AreEqual(
		double.PositiveInfinity,
		PhpSerialization.Deserialize<double?>("d:INF;")
	);
}

[TestMethod]
public void Explicit_Nullable_DeserializesNegativeInfinity() {
	Assert.AreEqual(
		double.NegativeInfinity,
		PhpSerialization.Deserialize<double?>("d:-INF;")
	);
}

float (aka Single) has the same problem.

unrecognized token

Hiya,

I just ran into the following issue and i was wondering if you could help me:

Input string:

a:1:{i:0;O:31:"KPS\Logistics\Status\To\Regular":3:{s:4:"from";s:8:"produced";s:2:"to";s:8:"produced";s:4:"date";O:8:"DateTime":3:{s:4:"date";s:26:"2021-08-18 09:10:23.441055";s:13:"timezone_type";i:3;s:8:"timezone";s:3:"UTC";}}}

Which gave me the following exception:

PhpSerializerNET.DeserializationException: 'Unexpected token 'O' at position 9.'

                        var statusLogString = items.First().statusLogString;
                        var deserialized = PhpSerializer.Deserialize(statusLogString, new PhpDeserializationOptions()
                        {
                            CaseSensitiveProperties = false
                        });

Add support for object/class names

Also something i'm missing, when i have an object (and im using 'EnableTypeLookup' == false here ) it gets serialized into a dictionary - this is fine, however in that case i lose the classname - which i need to make sense of the object.

Would it be possible to add a key '__PHP_OBJECT_CLASSNAME' with the classname for an object?
Or perhaps you could do something like this:

class PhpSerializedObjectDictionary : Dictionary<string, object>
{
        public string PhpObjectName { get; set; }
}

Feature request: Support String to guid conversion

Sample:

a:1:{i:0;s:36:"82e2ebf0-43e6-4c10-82cf-57d60383a6be";}

It would be nice if i could translate the LabelId property here to a guid directly. Right now it throws unfortunately:

image

As a sidenote, i can do new GuidConverter().ConvertFromInvariantString(null, "82e2ebf0-43e6-4c10-82cf-57d60383a6be"); and it works fine.

Apperently Guid does not implement IConvertible.
https://stackoverflow.com/a/13066991/4122889

I'm also thinking general TypeConverter support would be cool so you can write custom converters - but idk about performance and so on for that one. Seems like supporting a Guid string is a broad enough use-case which would fit nicely in this library tho.

Feature: convert PhpDateTime into native System.Datetime object

Perhaps a cool feature would be to allow for Php datetime objects to be directly translated to native System.DateTime.

I don't need this myself but it should be easy enough to implement - i have this code-snippet that does the job halfway already which we can use - so just posting this as a nice to have backlog item ^^.

private static DateTime? ParsePhpDateTimeStringSafe(string dateString, string timezone, long timezoneType)
{
    try
    {
        return ParsePhpDateTimeString(dateString, timezone, timezoneType);
    }
    catch
    {
        // We're swallowing exceptions because we'll be handling null values instead.
        return null;
    }
}

private static DateTime ParsePhpDateTimeString(string dateString, string timezone, long timezoneType)
{
    var localDateTimePattern = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss.ffffff");
    var localDateTime = localDateTimePattern.Parse(dateString).GetValueOrThrow();

    // See: https://stackoverflow.com/a/17711005/4122889
    //  Type 1; A UTC offset, such as in new DateTime("17 July 2013 -0300");
    //  Type 2; A timezone abbreviation, such as in new DateTime("17 July 2013 GMT");
    //  Type 3: A timezone identifier, such as in new DateTime("17 July 2013", new DateTimeZone("Europe/London"));

    switch (timezoneType)
    {
        case 1:
            var offSetPattern = OffsetPattern.CreateWithInvariantCulture("+HH:mm");
            var offset = offSetPattern.Parse(timezone).Value;
            var zonedDateTimeFromOffset = localDateTime.InZoneStrictly(DateTimeZone.ForOffset(offset));
            return zonedDateTimeFromOffset.ToDateTimeUtc();
        case 2:
            throw new NotSupportedException("Not (Yet) support converting from timeZonetype 2 - but doable to add in!");
        case 3:
            var dateTimeZone = DateTimeZoneProviders.Tzdb[timezone];
            var zonedDateTime = dateTimeZone.AtStrictly(localDateTime);
            var dateTimeUtc = zonedDateTime.ToDateTimeUtc();
            return dateTimeUtc;
        default:
            throw new ArgumentOutOfRangeException(nameof(timezoneType));
    }


}

This does however use the NodaTime package which may not be a dependency we'd like. Optionally we could move this to a seperate package to give consumers more control over this.

We may also be able to get the code working without NodaTime, perhaps with something like https://github.com/mattjohnsonpint/TimeZoneNames for the timezonedb values but i have not given this much thought.

Enhancement: Better target type validation

Hiya,

I just ran into an issue where i tried to assign an array of strings to a string. This caused a weird error in MakeObject() because it was trying to construct a string.

a:1:{i:0;s:1:"a";}

To string.

Anyhow i spent some time what the issue was (from a lib consumer pov) and then found out the target type was just wrong. I feel like we can improve the validation a bit in this regard, where if i get a php array string and try to assign it to a string or int or w/e i get a specialized exception rather than something vague. (in this case it tried to construct the string in .MakeObject() which it can't.

Bug: classname not being set

Hiya,

It seems like the class name is not being set:

image

var phpString = "a:4:{i:0;O:31:\"KPS\\Logistics\\Status\\To\\Regular\":3:{s:4:\"from\";s:10:\"processing\";s:2:\"to\";s:8:\"produced\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-11-05 06:03:27.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}i:1;O:31:\"KPS\\Logistics\\Status\\To\\Delayed\":5:{s:4:\"from\";s:8:\"produced\";s:2:\"to\";s:7:\"delayed\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-11-05 07:10:22.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}s:11:\"delay_until\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-11-06 00:00:00.000000\";s:13:\"timezone_type\";i:1;s:8:\"timezone\";s:6:\"+00:00\";}s:12:\"delay_reason\";s:12:\"out_of_stock\";}i:2;O:31:\"KPS\\Logistics\\Status\\To\\Regular\":3:{s:4:\"from\";s:7:\"delayed\";s:2:\"to\";s:8:\"produced\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-11-06 09:26:30.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}i:3;O:31:\"KPS\\Logistics\\Status\\To\\Regular\":3:{s:4:\"from\";s:8:\"produced\";s:2:\"to\";s:6:\"picked\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2018-11-06 14:13:52.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}}";
var phpString2 = "a:3:{i:0;O:31:\"KPS\\Logistics\\Status\\To\\Delayed\":5:{s:12:\"delay_reason\";s:12:\"out_of_stock\";s:11:\"delay_until\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2019-06-18 00:00:00.000000\";s:13:\"timezone_type\";i:1;s:8:\"timezone\";s:6:\"+00:00\";}s:4:\"from\";s:10:\"processing\";s:2:\"to\";s:7:\"delayed\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2019-06-12 11:44:26.124104\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}i:1;O:31:\"KPS\\Logistics\\Status\\To\\Regular\":3:{s:4:\"from\";s:7:\"delayed\";s:2:\"to\";s:10:\"processing\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2019-06-18 04:54:41.453555\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}i:2;O:31:\"KPS\\Logistics\\Status\\To\\Regular\":3:{s:4:\"from\";s:10:\"processing\";s:2:\"to\";s:8:\"produced\";s:4:\"date\";O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2019-06-18 09:50:40.847519\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}}}";


var deserialize = PhpSerialization.Deserialize(phpString2);

var orderItemStatusLogWpModels = PhpSerialization.Deserialize<List<OrderItemStatusLogWpModel>>(phpString, new PhpDeserializationOptions()
{
    AllowExcessKeys = true,
});

var orderItemStatusLogWpModels2 = PhpSerialization.Deserialize<List<OrderItemStatusLogWpModel>>(phpString2, new PhpDeserializationOptions()
{
    AllowExcessKeys = true
});
public class OrderItemStatusLogWpModel : IPhpObject
{
    private string? _className;

    [PhpProperty("delay_reason")]
    public string? DelayReason { get; set; }

    [PhpProperty("delay_until")]
    public PhpDateTime? DelayUntil { get; set; }

    [PhpProperty("from")]
    public string? From { get; set; }

    [PhpProperty("to")]
    public string? To { get; set; }

    [PhpProperty("date")]
    public PhpDateTime? Date { get; set; }

    #region IPhpObject

    public string GetClassName() => _className!;

    public void SetClassName(string className) => 
        _className = className;

    #endregion
}

Also is there constructor injection support? (haven't looked) because that would allow for my models to not have to be nullable everywhere (in a nullable enabled context).

Any way to specify that an array property should be serialized without a property key?

I'm writing an application that needs to be compatible with a legacy client whose code I can't change.

I have an issue where I have an object that has a nested array. The client expects the array to be serialized without a string key. It's just indexed starting with i:0.

E.g.

My class:

public class MyClass
{
     // How to avoid the `objects` key prefix?
    public List<Objects>? objects { get; set; }

    [PhpProperty("some_string")]
    public string? some_string{ get; set; }

    [PhpProperty("some_string2")]
    public string? some_string2{ get; set; }

    [PhpProperty("some_string3")]
    public string? some_string3{ get; set; }

    [PhpProperty("some_string4")]
    public string? some_string4{ get; set; }
}

another option I tried was just using an object but not a list:

     // How to have this be an integer index?
    public Objects? objects { get; set; }

The output I'm hoping to match would look like this:

// Notice the array index `0` is used without any string key
a:5:{i:0;a:2:

What I'm actually getting [List version]:

a:5:{s:7:"objects";a:1:{i:0;a:2:

What I'm getting [without List version]:

a:5:{s:7:"objects";a:2:

Thanks in advance.

Write proper documentation

Unit tests as examples in the meantime:

https://github.com/StringEpsilon/PhpSerializerNET/blob/main/PhpSerializerNET.Test/

TODOs:

  • Usage
    • Deserializing data
      • Unspecified target type - basic.
      • Unspecified target type - Arrays
      • Unspecified target type - Objects
    • Serializing data
  • Attributes
    • PhpClass
    • PhpIgnore
    • PhpProperty
  • PhpDeserializationOptions
    • CaseSensitiveProperties
    • AllowExcessKeys
    • UseLists
    • EmptyStringToDefault
    • NumberStringToBool
    • InputEncoding
    • StdClass
    • EnableTypeLookup
    • TypeCache
  • ListOptions
    • Default
    • OnAllIntegerKeys
    • Never
  • StdClassOption
    • Dictionary
    • Dynamic
    • Throw
  • TypeCacheFlag (Deactivated, ClassNames, PropertyInfo)
  • Data types
    • PhpObjectDictionary
    • PhpDynamicObject
    • PhpDateTime
    • IPhpObject
  • PhpSerializiationOptions
    • ThrowOnCircularReferences
    • NumericEnums

Deserialization .UseLists = Listoption.Never breaks parsing

Hiya,

I'm trying to work with the following string:

a:1:{i:0;a:11:{s:14:"content_length";s:2:"96";s:13:"content_width";s:4:"14.5";s:14:"content_height";s:3:"1.5";s:14:"content_weight";s:5:"2.094";s:9:"belt_size";s:3:"146";s:5:"items";a:3:{i:108192;s:1:"1";i:108191;s:1:"1";i:108190;s:1:"1";}s:6:"length";s:3:"104";s:5:"width";s:2:"17";s:6:"height";s:1:"4";s:6:"weight";s:5:"2.362";s:9:"packaging";a:3:{s:2:"id";s:5:"84565";s:4:"cost";s:1:"0";s:6:"weight";s:5:"0.268";}}}

Which should parse fine:

Array
(
    [0] => Array
        (
            [content_length] => 96
            [content_width] => 14.5
            [content_height] => 1.5
            [content_weight] => 2.094
            [belt_size] => 146
            [items] => Array
                (
                    [108192] => 1
                    [108191] => 1
                    [108190] => 1
                )

            [length] => 104
            [width] => 17
            [height] => 4
            [weight] => 2.362
            [packaging] => Array
                (
                    [id] => 84565
                    [cost] => 0
                    [weight] => 0.268
                )
        )
)

https://www.unserialize.com/s/7bab51d5-6a0c-a748-bb0f-00004aac1852

Using the library - this works when UseLists is the default value, but when i set it to Listoption.Never, because 'items' is a dictionary and not a list (this is weird but out of my control..) it simply returns null;

            var parcelAllocations = PhpSerialization.Deserialize(parcelAllocationMetaValue, new PhpDeserializationOptions
            {
                // Needed because of a b.u.g in the library TODO 27092021 typelookup issue seems to be fixed, classnames are also added so we can refactor the parsing to be somewhat nicer.
                EnableTypeLookup = false,

                // Needed because items is dictionary of integers and will be parsed as a list instead of a dict.
                UseLists = ListOptions.Never
            }) as List<object>;

Feature: Custom name on enum fields

Hiya,

When deserializing to an enum right now its mandatory to have the fields match the string exactly (AFAIK).
This however stops me from adhering to proper C# guidelines concerning naming and so on.

For example:

public enum OrderItemCouponType
{
    Bol = 0,

    // ReSharper disable InconsistentNaming 
    percent = 1,
    fixed_product = 2,
    fixed_cart = 3,
    percent_product = 4,
    // ReSharper restore InconsistentNaming
    
    Unknown = 5,
    Eol = 6

}

Should really be:

public enum OrderItemCouponType
{
    Bol = 0,

    Percent = 1,
    FixedProduct = 2,
    FixedCart = 3,
    PercentProduct = 4,
    
    Unknown = 5,
    Eol = 6

}

I'd imagine reusing the PhpProperty for this like

public enum OrderItemCouponType
{
    Bol = 0,

    [PhpProperty("percent")] 
    Percent = 1,
    [PhpProperty("fixes_product")] 
    FixedProduct = 2,
    [PhpProperty("fixed_cart")] 
    FixedCart = 3,
    [PhpProperty("percent_product")] 
    PercentProduct = 4,
    
    Unknown = 5,
    Eol = 6

}

Let me know what you think.

Bug : 'Unexpected token '}'

Sample:

a:1:{i:0;a:7:{s:7:"labelId";s:36:"639a8673-c117-4dff-8e19-7c20bd8a98c1";s:11:"trackerCode";s:24:"JVGL06202840001797672669";s:10:"parcelType";s:5:"SMALL";s:11:"pieceNumber";i:1;s:6:"weight";d:1.0509999999999999;s:9:"labelType";s:20:"B2X_Generic_A4_Third";s:10:"dimensions";a:3:{s:6:"length";i:8;s:5:"width";d:2.5;s:6:"height";d:2.5;}}}
Array
(
    [0] => Array
        (
            [labelId] => 639a8673-c117-4dff-8e19-7c20bd8a98c1
            [trackerCode] => JVGL06202840001797672669
            [parcelType] => SMALL
            [pieceNumber] => 1
            [weight] => 1.051
            [labelType] => B2X_Generic_A4_Third
            [dimensions] => Array
                (
                    [length] => 8
                    [width] => 2.5
                    [height] => 2.5
                )

        )

)

This string is throwing exceptions, i tried using a DTO to deserialize into and without any options.

            var s = "a:1:{i:0;a:7:{s:7:\"labelId\";s:36:\"639a8673-c117-4dff-8e19-7c20bd8a98c1\";s:11:\"trackerCode\";s:24:\"JVGL06202840001797672669\";s:10:\"parcelType\";s:5:\"SMALL\";s:11:\"pieceNumber\";i:1;s:6:\"weight\";d:1.0509999999999999;s:9:\"labelType\";s:20:\"B2X_Generic_A4_Third\";s:10:\"dimensions\";a:3:{s:6:\"length\";i:8;s:5:\"width\";d:2.5;s:6:\"height\";d:2.5;}}}";
            var des = PhpSerialization.Deserialize(s);
	public class Program
    {
        public static PhpDeserializationOptions PhpDeserializationOptions { get; set; } = new()
        {// Needed for ParcelAllocation, which may or may not have 'supports'.
            AllowExcessKeys = true
        };
        public static void Main()
        {
            var s = "a:1:{i:0;a:7:{s:7:\"labelId\";s:36:\"639a8673-c117-4dff-8e19-7c20bd8a98c1\";s:11:\"trackerCode\";s:24:\"JVGL06202840001797672669\";s:10:\"parcelType\";s:5:\"SMALL\";s:11:\"pieceNumber\";i:1;s:6:\"weight\";d:1.0509999999999999;s:9:\"labelType\";s:20:\"B2X_Generic_A4_Third\";s:10:\"dimensions\";a:3:{s:6:\"length\";i:8;s:5:\"width\";d:2.5;s:6:\"height\";d:2.5;}}}";
            var des = PhpSerialization.Deserialize<KpsShippingExtraMetaPhpModel>(s, PhpDeserializationOptions);
            Console.WriteLine(des);
        }
    }

    public class KpsShippingExtraMetaPhpModel
    {
        [PhpProperty("labelId")]
        public string LabelId { get; set; } // TODO PHP lib guid support ?

        [PhpProperty("trackerCode")]
        public string TrackerCode { get; set; }

        [PhpProperty("parcelType")]
        public int ParcelType { get; set; }

        [PhpProperty("pieceNumber")]
        public string PieceNumber { get; set; }

        [PhpProperty("weight")]
        public double Weight { get; set; }

        [PhpProperty("labelType")]
        public string LabelType { get; set; }

        [PhpProperty("dimensions")]
        public KpsShippingExtraMetaDimensionsPhpModel Dimensions { get; set; }
    }

    public class KpsShippingExtraMetaDimensionsPhpModel
    {
        [PhpProperty("length")]
        public double Length { get; set; }

        [PhpProperty("width")]
        public double Width { get; set; }

        [PhpProperty("height")]
        public double Height { get; set; }
    }

Apperently the validate format faults.

.Net core 5 with v7.0.0

Feature request: Option to allow for optional properties

I've got a case where sometimes a property is present in my php string and sometimes it is not.

Take these 2 strings as samples:

With supports data

a:1:{i:0;a:12:{s:14:"content_length";s:5:"112.5";s:13:"content_width";s:2:"28";s:14:"content_height";s:1:"5";s:14:"content_weight";s:5:"7.222";s:9:"belt_size";s:5:"206.5";s:5:"items";a:1:{i:222897;s:1:"5";}s:6:"length";s:5:"120.5";s:5:"width";s:2:"33";s:6:"height";s:2:"10";s:6:"weight";s:5:"8.167";s:9:"packaging";a:3:{s:2:"id";s:6:"150514";s:4:"cost";s:1:"0";s:6:"weight";s:5:"0.945";}s:8:"supports";a:5:{s:8:"material";s:9:"honeycomb";s:6:"length";s:5:"112.5";s:5:"width";s:2:"28";s:6:"height";s:1:"3";s:6:"weight";s:5:"0.337";}}}
Array
(
    [0] => Array
        (
            [content_length] => 112.5
            [content_width] => 28
            [content_height] => 5
            [content_weight] => 7.222
            [belt_size] => 206.5
            [items] => Array
                (
                    [222897] => 5
                )
            [length] => 120.5
            [width] => 33
            [height] => 10
            [weight] => 8.167
            [packaging] => Array
                (
                    [id] => 150514
                    [cost] => 0
                    [weight] => 0.945
                )
            [supports] => Array
                (
                    [material] => honeycomb
                    [length] => 112.5
                    [width] => 28
                    [height] => 3
                    [weight] => 0.337
                )
        )
)

And without supports

a:1:{i:0;a:11:{s:14:"content_length";s:2:"96";s:13:"content_width";s:4:"14.5";s:14:"content_height";s:3:"1.5";s:14:"content_weight";s:5:"2.094";s:9:"belt_size";s:3:"146";s:5:"items";a:3:{i:108192;s:1:"1";i:108191;s:1:"1";i:108190;s:1:"1";}s:6:"length";s:3:"104";s:5:"width";s:2:"17";s:6:"height";s:1:"4";s:6:"weight";s:5:"2.362";s:9:"packaging";a:3:{s:2:"id";s:5:"84565";s:4:"cost";s:1:"0";s:6:"weight";s:5:"0.268";}}}
Array
(
    [0] => Array
        (
            [content_length] => 96
            [content_width] => 14.5
            [content_height] => 1.5
            [content_weight] => 2.094
            [belt_size] => 146
            [items] => Array
                (
                    [108192] => 1
                    [108191] => 1
                    [108190] => 1
                )
            [length] => 104
            [width] => 17
            [height] => 4
            [weight] => 2.362
            [packaging] => Array
                (
                    [id] => 84565
                    [cost] => 0
                    [weight] => 0.268
                )
        )
)

Right now i'm mapping these objects like so:

    public class ParcelAllocationWithSupportPhpModel : ParcelAllocationPhpModel
    {
        public ParcelAllocationSupportsPhpModel supports { get; set; }
        public static bool TryDeserializeFromList(string input, out List<ParcelAllocationWithSupportPhpModel> parcelAllocationPhpModels)
        {
            try
            {
                parcelAllocationPhpModels = DeserializeFromList(input);
                return true;
            }
            catch (Exception)
            {
                parcelAllocationPhpModels = null;
                return false;
            }
        }

        public static List<ParcelAllocationWithSupportPhpModel> DeserializeFromList(string input) =>
            PhpSerialization.Deserialize<List<ParcelAllocationWithSupportPhpModel>>(input);

        public static bool TryDeserialize(string input, out ParcelAllocationWithSupportPhpModel parcelAllocationPhpModels)
        {
            try
            {
                parcelAllocationPhpModels = Deserialize(input);
                return true;
            }
            catch (Exception)
            {
                parcelAllocationPhpModels = null;
                return false;
            }
        }

        public static ParcelAllocationWithSupportPhpModel Deserialize(string input) =>
            PhpSerialization.Deserialize<ParcelAllocationWithSupportPhpModel>(input);

        public static string Serialize(ParcelAllocationWithSupportPhpModel model) =>
            PhpSerialization.Serialize(model);
    }

    public class ParcelAllocationPhpModel
    {
        public double content_length { get; set; }
        public double content_width { get; set; }
        public double content_height { get; set; }
        public double content_weight { get; set; }
        public double belt_size { get; set; }
        public Dictionary<long, int> items { get; set; } // TODO sometimes ints, sometimes strings (guids from marketplace) - Change to Dict<string,int> so that we can parse them both!
        public double length { get; set; }
        public double width { get; set; }
        public double height { get; set; }
        public double weight { get; set; }
        public ParcelAllocationPackagingPhpModel packaging { get; set; }
        
        public static bool TryDeserializeFromList(string input, out List<ParcelAllocationPhpModel> parcelAllocationPhpModels)
        {
            try
            {
                parcelAllocationPhpModels = DeserializeFromList(input);
                return true;
            }
            catch (Exception)
            {
                parcelAllocationPhpModels = null;
                return false;
            }
        }

        public static List<ParcelAllocationPhpModel> DeserializeFromList(string input) =>
            PhpSerialization.Deserialize<List<ParcelAllocationPhpModel>>(input);

        public static bool TryDeserialize(string input, out ParcelAllocationPhpModel parcelAllocationPhpModels)
        {
            try
            {
                parcelAllocationPhpModels = Deserialize(input);
                return true;
            }
            catch (Exception)
            {
                parcelAllocationPhpModels = null;
                return false;
            }
        }

        public static ParcelAllocationPhpModel Deserialize(string input) =>
            PhpSerialization.Deserialize<ParcelAllocationPhpModel>(input);

        public static string Serialize(ParcelAllocationPackagingPhpModel model) =>
            PhpSerialization.Serialize(model);
    }
List<ParcelAllocationPhpModel> parcelAllocationPhpModels = null;
if (!ParcelAllocationPhpModel.TryDeserializeFromList(parcelAllocationMetaValue, out parcelAllocationPhpModels))
{
    if (ParcelAllocationWithSupportPhpModel.TryDeserializeFromList(parcelAllocationMetaValue, out var parcelAllocationWithSupportPhpModels))
    {
        parcelAllocationPhpModels = parcelAllocationWithSupportPhpModels.Cast<ParcelAllocationPhpModel>().ToList();
    }
}

This is a) a mess and b) has the sideeffect that i no longer can read the supports property.

What i'd like is to be able to have any properties that are not present in the PHP string to be set to their default value. In my case supports would be null - or have a ParcelAllocationSupportsPhpModel if that was present. I think this would also mimick the behvaiour of newtonsoft Json.Net.

Anyhow i'd imagine my code to become something like:

var parcelAllocationPhpModels = ParcelAllocationPhpModel.DeserializeFromList(input);
var firstSupports = parcelAllocationPhpModels[0].supports; // default (null) or an object

Depending on how the serializer works we can also work with [required] and [optional] attributes. There are some in the std lib but perhaps to not attach dependencies we can roll our own.

@StringEpsilon i can imagine you're busy so any guidance on self implementing this would be cool.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.