Skip to content

Commit

Permalink
Implements Full Support for System.Text.Json - Closes #527 (#533)
Browse files Browse the repository at this point in the history
* feat(json-support): Add Support for System.Text.Json.JsonElement #527

- Introduced JsonElementExtensions.cs to convert JsonElements into Scriban objects
- Updated ScriptArray.cs and ScriptObject.cs to handle JsonElements
- Enhanced ScriptObjectExtensions.cs with methods for importing JsonElements into script objects
- Extended Template.cs with a Render method that accepts a JsonElement as input
- Added new test cases to validate JSON support

* feat(json-support): Add object.form_json and object.to_json functions #527

- Implemented FromJson function to convert JSON to Scriban value
- Implemented ToJson function to convert Scriban value to JSON
- Added new test cases for JSON parsing and conversion in TestObjectFunctions.cs

* feat(json-support): Updating docs to show new functionality

* feat(json-support): Removed unnecessary JSON support from ScriptArray and ScriptObject

- Deleted methods that added JsonElement to ScriptArray and ScriptObject
- Removed JsonElement handling in Add and SetValue methods of ScriptObject
- Erased test cases for adding json values in TestScriptArrayJson.cs and TestScriptObjectJson.cs

* chore(json-support): Fix Docs unit test - Updated JSON formatting in builtins.md

* feat(json-support): Move JSON handling to import logic

- Moved JSON conversion logic to JsonElementExtensions.cs
- Removed JSON handling code from ScriptObject.cs to ScriptObjectExtensions.cs import
- Added ConvertValue method to handle Scriban value conversions

* fix(json-support): Remove explicit ScriptObject.From(JsonElement) method

- Deleted method for creating a ScriptObject from a JsonElement as this functionality can be achieved using generic from object method
- Removed unused using directives in ScriptObject.cs and ScriptObjectExtensions.cs

* fix(json-support): Remove explicit Template.Render JsonElement method

- Removed Render method with JsonElement as this can be achieved using the generic render object method
- Deleted System.Text.Json usage in Scriban.Template.cs
  • Loading branch information
r-Larch committed Feb 16, 2024
1 parent 24a4985 commit f2f3cf4
Show file tree
Hide file tree
Showing 12 changed files with 789 additions and 10 deletions.
70 changes: 70 additions & 0 deletions doc/builtins.md
Expand Up @@ -1656,6 +1656,8 @@ Object functions available through the builtin object 'object'.
- [`object.typeof`](#objecttypeof)
- [`object.kind`](#objectkind)
- [`object.values`](#objectvalues)
- [`object.from_json`](#objectfrom_json)
- [`object.to_json`](#objectto_json)

[:top:](#builtins)
### `object.default`
Expand Down Expand Up @@ -2031,6 +2033,74 @@ A list with the member values of the input object
```html
["fruit", "Orange"]
```

[:top:](#builtins)
### `object.from_json`

```
object.from_json <value>
```

#### Description

Converts a JSON string to a scriban value.

#### Arguments

- `value`: The input JSON string.

#### Returns

The scriban value.

#### Examples

> **input**
```scriban-html
{{
obj = `{ "foo": 123 }` | object.from_json
obj.foo
}}
```
> **output**
```html
123
```

[:top:](#builtins)
### `object.to_json`

```
object.to_json <value>
```

#### Description

Converts a scriban value to a JSON string.

#### Arguments

- `value`: The input value.

#### Returns

A JSON representation of the value.

#### Examples

> **input**
```scriban-html
{{ { foo: "bar", baz: [1, 2, 3] } | object.to_json }}
{{ true | object.to_json }}
{{ null | object.to_json }}
```
> **output**
```html
{"foo":"bar","baz":[1,2,3]}
true
null
```

[:top:](#builtins)

## `regex` functions
Expand Down
30 changes: 30 additions & 0 deletions doc/runtime.md
Expand Up @@ -228,6 +228,36 @@ A `ScriptObject` is mainly an extended version of a `IDictionary<string, object>

Note that any `IDictionary<string, object>` put as a property will be accessible as well.

#### Imports System.Text.Json.JsonElement

A `ScriptObject` or `ScriptArray` can import `JsonElement`.

```C#
// objects with ScriptObject
JsonElement json = JsonSerializer.Deserialize<JsonElement>("""{ "foo": "bar" }""");
var model = ScriptObject.From(json);

// arrays with ScriptArray
JsonElement json = JsonSerializer.Deserialize<JsonElement>("""[1, 2, 3]""");
var model = ScriptArray.From(json);

// import to an existing object
var model = new ScriptObject();
model.Import(jsonElement);

// add to an existing object
var model = new ScriptObject();
model.Add("foo", jsonElement);

// render using JsonElement directly
JsonElement model = JsonSerializer.Deserialize<JsonElement>("""{ "foo": "bar" }""");
var template = Template.Parse("foo: `{{foo}}`");
var result = template.Render(model);
// Prints: foo: `bar`
```

**Note**: JsonElement is also supported in properties of custom classes and structs.

#### Imports a .NET delegate

Via `ScriptObject.Import(member, Delegate)`. Here we import a `Func<string>`:
Expand Down
2 changes: 2 additions & 0 deletions src/Scriban.Tests/TestFiles/400-builtins/400-builtins.out.txt
Expand Up @@ -110,11 +110,13 @@ List of all the builtin <object> functions:
eval
eval_template
format
from_json
has_key
has_value
keys
kind
size
to_json
typeof
values

Expand Down
101 changes: 101 additions & 0 deletions src/Scriban.Tests/TestJsonSupport/TestModelWithJsonElement.cs
@@ -0,0 +1,101 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Licensed under the BSD-Clause 2 license. See license.txt file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Text.Json;
using Scriban.Runtime;


namespace Scriban.Tests.TestJsonSupport;

[TestFixture]
public class TestModelWithJsonElement {
[TestCase("""null""", "")]
[TestCase("""true""", "true")]
[TestCase("""false""", "false")]
[TestCase("""123.45""", "123.45")]
[TestCase("\"bar\"", "bar")]
[TestCase("""[1, 2, 3]""", "[1, 2, 3]")]
[TestCase("""{ "foo": "bar" }""", "{foo: \"bar\"}")]
public void Can_import_JsonElement_property(string json, string expected)
{
var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);

var model = new {
foo = jsonElement,
};

var result = RenderHelper.Render(
script: "{{ foo }}",
scriptObject: ScriptObject.From(model)
);

Assert.AreEqual(expected, result);
}

[Test]
public void Can_import_boxed_jsonElement()
{
var model = JsonSerializer.Deserialize<Dictionary<string, object>>("""{ "model": { "foo": "bar" } }""");

// ensure we have a boxed JsonElement:
Assert.AreEqual(typeof(string), model.GetType().GetGenericArguments()[0]);
Assert.AreEqual(typeof(object), model.GetType().GetGenericArguments()[1]);
Assert.AreEqual(typeof(JsonElement), model["model"].GetType());

var result = RenderHelper.Render(
script: "{{ model.foo }}",
scriptObject: ScriptObject.From(model)
);

Assert.AreEqual("bar", result);
}

[Test]
public void Can_import_jsonElement_in_typed_class()
{
var data = JsonSerializer.Deserialize<JsonElement>("""{ "foo": "bar" }""");

var model = new MyClass("name", data);

var result = RenderHelper.Render(
script: """
Name: {{ name }}
Data.Foo: {{ data.foo }}
""",
scriptObject: ScriptObject.From(model)
);

Assert.AreEqual("Name: name\r\nData.Foo: bar", result);
}

[Test]
public void Can_import_jsonElement_in_typed_struct()
{
var data = JsonSerializer.Deserialize<JsonElement>("""{ "foo": "bar" }""");

var model = new MyStruct("name", data);

var result = RenderHelper.Render(
script: """
Name: {{ name }}
Data.Foo: {{ data.foo }}
""",
scriptObject: ScriptObject.From(model)
);

Assert.AreEqual("Name: name\r\nData.Foo: bar", result);
}


private record MyClass(
string Name,
JsonElement Data
);

private record struct MyStruct(
string Name,
JsonElement Data
);
}
144 changes: 144 additions & 0 deletions src/Scriban.Tests/TestJsonSupport/TestObjectFunctions.cs
@@ -0,0 +1,144 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Licensed under the BSD-Clause 2 license. See license.txt file in the project root for full license information.

using System;
using Scriban.Runtime;
using Scriban.Syntax;


namespace Scriban.Tests.TestJsonSupport;

[TestFixture]
public class TestObjectFunctions {
[Test]
public void Can_parse_json()
{
var template = Template.Parse("""
{{
json = `{ "foo": { "bar": [{ "baz": 1 }, { "baz": 2 }, { "baz": 3 }] } }`
obj = json | object.from_json
obj.foo.bar[1].baz
}}
"""
);

var result = template.Render();

Assert.AreEqual("2", result);
}

[TestCase("""
null
""", """
null
""")]
[TestCase("""
true
""", """
true
""")]
[TestCase("""
false
""", """
false
""")]
[TestCase("""
"string"
""", """
"string"
""")]
[TestCase("""
123
""", """
123
""")]
[TestCase("""
123.45
""", """
123.45
""")]
[TestCase("""
[1, 2, 3, {foo: "bar"}, { "baz": 123 }]
""", """
[1,2,3,{"foo":"bar"},{"baz":123}]
""")]
[TestCase("""
{ foo: { bar: [{ baz: 1 }, { baz: 2 }, { baz: 3 }] } }
""", """
{"foo":{"bar":[{"baz":1},{"baz":2},{"baz":3}]}}
""")]
public void Can_convert_ScribanValue_to_json(string scriban, string json)
{
var template = Template.Parse($$$"""
{{ {{{scriban}}} | object.to_json }}
"""
);

var result = template.Render();

Assert.AreEqual(json, result);
}

[Test]
public void Can_convert_TypedModel_to_json()
{
var template = Template.Parse("""
{{ model | object.to_json }}
"""
);

var result = template.Render(new {
Model = new {
Foo = "bar",
Baz = new[] { 1, 2, 3 }
}
});

Assert.AreEqual("""
{"foo":"bar","baz":[1,2,3]}
""", result);
}

[Test]
public void Can_handle_MemberRenamer_when_writing_json()
{
var template = Template.Parse("""
{{ Model | object.to_json }}
"""
);

var model = new {
Model = new {
Foo = "bar",
Baz = new[] { 1, 2, 3 }
}
};

var result = template.Render(model, member => member.Name);

Assert.AreEqual("""
{"Foo":"bar","Baz":[1,2,3]}
""", result);
}

[Test]
public void Throws_when_serializing_function_to_json()
{
var template = Template.Parse("""
{{
func myFunc()
ret 1
end

object.to_json @myFunc
}}
"""
);

var ex = Assert.Throws<ScriptRuntimeException>(() => {
var result = template.Render();
})!;

Assert.AreEqual("<input>(6,20) : error : Can not serialize functions to JSON. (Parameter 'value')", ex.Message);
}
}

0 comments on commit f2f3cf4

Please sign in to comment.