Skip to content

Commit 25d8857

Browse files
committed
Add support for NaN/Infinity serialization
1 parent 9c76404 commit 25d8857

19 files changed

Lines changed: 354 additions & 13 deletions

File tree

src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Text.Json.Serialization;
99
using System.Text.Encodings.Web;
1010
using System.Text.Unicode;
11+
using DotVVM.Framework.Utils;
1112

1213
namespace DotVVM.Framework.Configuration
1314
{
@@ -41,6 +42,9 @@ private JsonSerializerOptions CreateSettings()
4142
#if !DotNetCore
4243
new DotvvmTimeOnlyJsonConverter(),
4344
new DotvvmDateOnlyJsonConverter(),
45+
#endif
46+
#if NET6_0_OR_GREATER
47+
new HalfJsonConverter(),
4448
#endif
4549
},
4650
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,

src/Framework/Framework/Resources/Scripts/metadata/coercer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { serializeDate } from "../serialization/date";
2+
import { jsonStringify } from "../serialization/serialize";
23
import { CoerceError } from "../shared-classes";
34
import { keys } from "../utils/objects";
45
import { tryCoerceEnum } from "./enums";
@@ -101,7 +102,7 @@ function tryCoerceArray(value: any, innerType: TypeDefinition, originalValue: an
101102
return { value: items, wasCoerced: true };
102103
}
103104
}
104-
return new CoerceError(`Value '${JSON.stringify(value)}' is not an array of type '${formatTypeName(innerType)}'.`);
105+
return new CoerceError(`Value '${jsonStringify(value)}' is not an array of type '${formatTypeName(innerType)}'.`);
105106
}
106107

107108
function tryCoercePrimitiveType(value: any, type: string): CoerceResult {

src/Framework/Framework/Resources/Scripts/metadata/primitiveTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ function validateFloat(value: any) {
108108
if (isNumber(value)) {
109109
return { value: +value, wasCoerced: value !== +value };
110110
}
111+
112+
if (value != value || value === "NaN") {
113+
return { value: NaN, wasCoerced: value == value }
114+
}
111115
}
112116

113117
function validateString(value: any) {

src/Framework/Framework/Resources/Scripts/postback/postbackCore.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { isPrimitive } from '../utils/objects';
1515
import * as stateManager from '../state-manager'
1616
import { mapUpdatableProperties } from '../serialization/deserialize';
1717
import { logError } from '../utils/logging';
18+
import { jsonStringify } from '../serialization/serialize';
1819

1920
let lastStartedPostbackId: number;
2021

@@ -77,7 +78,7 @@ export async function postbackCore(
7778
}
7879

7980
const initialUrl = getInitialUrl();
80-
let response = await http.postJSON<PostbackResponse>(initialUrl, JSON.stringify(data), options.abortSignal);
81+
let response = await http.postJSON<PostbackResponse>(initialUrl, jsonStringify(data), options.abortSignal);
8182

8283
if (response.result.action == "viewModelNotCached") {
8384
// repeat the request with full viewmodel
@@ -87,7 +88,7 @@ export async function postbackCore(
8788
delete data.viewModelCache;
8889
data.viewModel = postedViewModel;
8990

90-
response = await http.postJSON<PostbackResponse>(initialUrl, JSON.stringify(data), options.abortSignal);
91+
response = await http.postJSON<PostbackResponse>(initialUrl, jsonStringify(data), options.abortSignal);
9192
}
9293

9394
events.postbackResponseReceived.trigger({

src/Framework/Framework/Resources/Scripts/postback/staticCommand.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { serialize } from '../serialization/serialize';
1+
import { jsonStringify, serialize } from '../serialization/serialize';
22
import { getInitialUrl, getViewModel } from '../dotvvm-base';
33
import * as events from '../events';
44
import * as http from './http'
@@ -60,7 +60,7 @@ export async function staticCommandPostback(command: string, args: any[], option
6060

6161
response = await http.postJSON<DotvvmStaticCommandResponse>(
6262
getInitialUrl(),
63-
JSON.stringify(data),
63+
jsonStringify(data),
6464
options.abortSignal,
6565
{ "X-PostbackType": "StaticCommand" }
6666
);

src/Framework/Framework/Resources/Scripts/serialization/serialize.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,14 @@ function findObject(obj: any, matcher: (o: any) => boolean): string[] | null {
125125
}
126126
return null;
127127
}
128+
129+
export function jsonStringify(value: any, indent = compileConstants.debug ? " " : undefined): string {
130+
return JSON.stringify(value, (key, val) => {
131+
if (typeof val === "number") {
132+
if (!isFinite(val)) {
133+
return String(val)
134+
}
135+
}
136+
return val
137+
}, indent)
138+
}

src/Framework/Framework/Resources/Scripts/shared-classes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { jsonStringify } from "./serialization/serialize"
2+
13
export class DotvvmPostbackError {
24
constructor(public reason: DotvvmPostbackErrorReason) {
35
}
4-
toString() { return "PostbackRejectionError(" + JSON.stringify(this.reason, null, " ") + ")"}
6+
toString() { return "PostbackRejectionError(" + jsonStringify(this.reason) + ")"}
57
}
68

79
export class CoerceError extends Error implements CoerceErrorType {

src/Framework/Framework/Resources/Scripts/state-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ export class StateManager<TViewModel extends { $type?: TypeDefinition }> impleme
5656
initialState: DeepReadonly<TViewModel>,
5757
public stateUpdateEvent?: DotvvmEvent<DeepReadonly<TViewModel>>
5858
) {
59-
this._state = coerce(initialState, initialState.$type || { type: "dynamic" })
60-
this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], () => this._state, u => this.updateState(u as any))
59+
const type = initialState.$type
60+
this._state = coerce(initialState, type || { type: "dynamic" })
61+
this.stateObservable = createWrappedObservable(this._state, type, () => this._state, u => this.updateState(u as any))
6162
this.dispatchUpdate()
6263
}
6364

src/Framework/Framework/Resources/Scripts/tests/coercer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,42 @@ test("number - valid, convert from string and keep decimal places", () => {
113113
expect(result.value).toEqual(1234.56);
114114
})
115115

116+
test("float - special values, NaN from string", () => {
117+
const result = tryCoerce("NaN", "Single");
118+
expect(result.wasCoerced).toBeTruthy();
119+
expect(Number.isNaN(result.value)).toBeTruthy();
120+
})
121+
122+
test("float - special values, Infinity from string", () => {
123+
const result = tryCoerce("Infinity", "Single");
124+
expect(result.wasCoerced).toBeTruthy();
125+
expect(result.value).toEqual(Infinity);
126+
})
127+
128+
test("float - special values, -Infinity from string", () => {
129+
const result = tryCoerce("-Infinity", "Single");
130+
expect(result.wasCoerced).toBeTruthy();
131+
expect(result.value).toEqual(-Infinity);
132+
})
133+
134+
test("double - special values, NaN from string", () => {
135+
const result = tryCoerce("NaN", "Double");
136+
expect(result.wasCoerced).toBeTruthy();
137+
expect(Number.isNaN(result.value)).toBeTruthy();
138+
})
139+
140+
test("double - special values, Infinity from string", () => {
141+
const result = tryCoerce("Infinity", "Double");
142+
expect(result.wasCoerced).toBeTruthy();
143+
expect(result.value).toEqual(Infinity);
144+
})
145+
146+
test("double - special values, -Infinity from string", () => {
147+
const result = tryCoerce("-Infinity", "Double");
148+
expect(result.wasCoerced).toBeTruthy();
149+
expect(result.value).toEqual(-Infinity);
150+
})
151+
116152
test("number - invalid, out of range", () => {
117153
const result = tryCoerce(100000, "Int16");
118154
expect(result.isError).toBeTruthy();

src/Framework/Framework/Resources/Scripts/tests/serialization.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dotvvm from '../dotvvm-root'
22
import { deserialize } from '../serialization/deserialize'
3-
import { serialize } from '../serialization/serialize'
3+
import { serialize, jsonStringify } from '../serialization/serialize'
44
import { serializeDate } from '../serialization/date'
55
import { tryCoerce } from '../metadata/coercer';
66
import { createComplexObservableSubViewmodel, createComplexObservableViewmodel, ObservableHierarchy, ObservableSubHierarchy, ObservableSubSubHierarchy } from "./observableHierarchies"
@@ -676,6 +676,12 @@ describe("DotVVM.Serialization - serialize", () => {
676676
expect(d).toBe("2015-08-01T13:56:42.0000000")
677677
})
678678

679+
test("jsonStringify - float special values", () => {
680+
const data = [1, NaN, Infinity, -Infinity, 2];
681+
const result = jsonStringify(data);
682+
expect(result).toBe(`[\n 1,\n "NaN",\n "Infinity",\n "-Infinity",\n 2\n]`)
683+
})
684+
679685
test("Serialize object with Date property", () => {
680686
const obj = serialize({
681687
$type: ko.observable("t3"),

0 commit comments

Comments
 (0)