Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 86 additions & 4 deletions Jint.Tests.PublicInterface/InteropTests.Json.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Dynamic;
using System.Text;
using FluentAssertions;
using Jint.Runtime.Interop;

Expand Down Expand Up @@ -131,19 +132,18 @@ public void CanStringifyUsingSerializeToJson()
engine.SetValue("TimeSpan", TypeReference.CreateTypeReference<TimeSpan>(engine));
engine.SetValue("TestObject", testObject);

var expected = Serialize(TimeSpan.FromSeconds(3));
var expected = Serialize(TimeSpan.FromSeconds(3), string.Empty);
var actual = engine.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3));");
actual.AsString().Should().Be(expected);

expected = Serialize(testObject);
expected = Serialize(testObject, string.Empty);
actual = engine.Evaluate("JSON.stringify(TestObject)");
actual.AsString().Should().Be(expected);

actual = engine.Evaluate("JSON.stringify({ nestedValue: TestObject })");
actual.AsString().Should().Be($$"""{"nestedValue":{{expected}}}""");
return;

string Serialize(object o)
static string Serialize(object o, string space, string currentIndent = null)
{
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
Expand All @@ -155,4 +155,86 @@ string Serialize(object o)
return Newtonsoft.Json.JsonConvert.SerializeObject(o, settings);
}
}

[Fact]
public void CanStringifyUsingSerializeToJsonWithIndentation()
{
object testObject = new { Foo = "bar", FooBar = new { Foo = 123.45, Array = Array.Empty<int>() } };
var e = new Engine(o => o.Interop.SerializeToJson = SerializeIndentation);
e.SetValue("TestObject", testObject);
e.Evaluate("JSON.stringify(TestObject, null, 4)").AsString().Should().Be(
"""
{
"foo": "bar",
"foo_bar": {
"foo": 123.45,
"array": []
}
}
"""
);

e.Evaluate("JSON.stringify({ nestedValue: TestObject }, null, 4)").AsString().Should().Be(
"""
{
"nestedValue": {
"foo": "bar",
"foo_bar": {
"foo": 123.45,
"array": []
}
}
}
""".Replace("\r\n", "\n")
);

static string SerializeIndentation(object o, string space, string currentIndent)
{
var jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(new Newtonsoft.Json.JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver
{
NamingStrategy = new Newtonsoft.Json.Serialization.SnakeCaseNamingStrategy()
}
});

var sb = new StringBuilder(256);
var sw = new StringWriter(sb);
using var jsonWriter = string.IsNullOrEmpty(space)
? new Newtonsoft.Json.JsonTextWriter(sw)
: new(sw)
{
Formatting = Newtonsoft.Json.Formatting.Indented,
Indentation = space.Length,
IndentChar = space[0] // assuming `space` only contains one kind of character
};

jsonSerializer.Serialize(jsonWriter, o);

if (string.IsNullOrEmpty(currentIndent))
{
return sw.ToString();
}
else
{
var lines = sw.ToString().Split('\n');
var stringBuilder = new StringBuilder();

for (var i = 0; i < lines.Length; i++)
{
if (i == 0)
{
stringBuilder.AppendLine(lines[i]);
}
else
{
stringBuilder.AppendLine(currentIndent + lines[i]);
}
}

return stringBuilder.ToString().Replace("\r", string.Empty).TrimEnd('\n');
}
}
}

}
4 changes: 2 additions & 2 deletions Jint/Native/Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref V
if (objectInstance is IObjectWrapper wrapper
&& _engine.Options.Interop.SerializeToJson is { } serialize)
{
json.Append(serialize(wrapper.Target));
json.Append(serialize(wrapper.Target, _gap, _indent));
return SerializeResult.NotUndefined;
}

Expand Down Expand Up @@ -558,7 +558,7 @@ private void SerializeJSONObject(ObjectInstance value, ref ValueStringBuilder js
private enum SerializeResult
{
NotUndefined,
Undefined
Undefined,
}

private readonly struct PropertyEnumeration
Expand Down
8 changes: 5 additions & 3 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class Options

public delegate string? BuildCallStackDelegate(string shortDescription, SourceLocation location, string[]? arguments);

public delegate string SerializeToJsonDelegate(object? target, string space, string? currentIndent);

/// <summary>
/// Execution constraints for the engine.
/// </summary>
Expand Down Expand Up @@ -347,21 +349,21 @@ public class InteropOptions
/// <summary>
/// Strategy to create a CLR object to hold converted <see cref="ObjectInstance"/>.
/// </summary>
public Func<ObjectInstance, IDictionary<string, object?>>? CreateClrObject = _ => new ExpandoObject();
public Func<ObjectInstance, IDictionary<string, object?>>? CreateClrObject { get; set; } = _ => new ExpandoObject();

/// <summary>
/// Strategy to create a CLR object from TypeReference.
/// Defaults to retuning null which makes TypeReference attempt to find suitable constructor.
/// </summary>
public Func<Engine, Type, JsValue[], object?> CreateTypeReferenceObject = (_, _, _) => null;
public Func<Engine, Type, JsValue[], object?> CreateTypeReferenceObject { get; set; } = (_, _, _) => null;

internal static readonly ExceptionHandlerDelegate _defaultExceptionHandler = static exception => false;

/// <summary>
/// When not null, is used to serialize any CLR object in an
/// <see cref="IObjectWrapper"/> passing through 'JSON.stringify'.
/// </summary>
public Func<object, string>? SerializeToJson { get; set; }
public SerializeToJsonDelegate? SerializeToJson { get; set; }

/// <summary>
/// What kind of date time should be produced when JavaScript date is converted to DateTime. If Local, uses <see cref="Options.TimeZone"/>.
Expand Down