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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Text.Json.Serialization;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Configuration
{
Expand Down Expand Up @@ -41,6 +42,9 @@ private JsonSerializerOptions CreateSettings()
#if !DotNetCore
new DotvvmTimeOnlyJsonConverter(),
new DotvvmDateOnlyJsonConverter(),
#endif
#if NET6_0_OR_GREATER
new HalfJsonConverter(),
#endif
},
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { serializeDate } from "../serialization/date";
import { jsonStringify } from "../serialization/serialize";
import { CoerceError } from "../shared-classes";
import { keys } from "../utils/objects";
import { tryCoerceEnum } from "./enums";
Expand Down Expand Up @@ -101,7 +102,7 @@ function tryCoerceArray(value: any, innerType: TypeDefinition, originalValue: an
return { value: items, wasCoerced: true };
}
}
return new CoerceError(`Value '${JSON.stringify(value)}' is not an array of type '${formatTypeName(innerType)}'.`);
return new CoerceError(`Value '${jsonStringify(value)}' is not an array of type '${formatTypeName(innerType)}'.`);
}

function tryCoercePrimitiveType(value: any, type: string): CoerceResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ function validateInt(value: any, min: number, max: number) {

function validateFloat(value: any) {
if (isNumber(value)) {
return { value: +value, wasCoerced: value !== +value };
return { value: +value, wasCoerced: value !== +value }
}

const isNaN = Number.isNaN(value)
if (isNaN || value === "NaN") {
return { value: NaN, wasCoerced: !isNaN }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isPrimitive } from '../utils/objects';
import * as stateManager from '../state-manager'
import { mapUpdatableProperties } from '../serialization/deserialize';
import { logError } from '../utils/logging';
import { jsonStringify } from '../serialization/serialize';

let lastStartedPostbackId: number;

Expand Down Expand Up @@ -77,7 +78,7 @@ export async function postbackCore(
}

const initialUrl = getInitialUrl();
let response = await http.postJSON<PostbackResponse>(initialUrl, JSON.stringify(data), options.abortSignal);
let response = await http.postJSON<PostbackResponse>(initialUrl, jsonStringify(data), options.abortSignal);

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

response = await http.postJSON<PostbackResponse>(initialUrl, JSON.stringify(data), options.abortSignal);
response = await http.postJSON<PostbackResponse>(initialUrl, jsonStringify(data), options.abortSignal);
}

events.postbackResponseReceived.trigger({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { serialize } from '../serialization/serialize';
import { jsonStringify, serialize } from '../serialization/serialize';
import { getInitialUrl, getViewModel } from '../dotvvm-base';
import * as events from '../events';
import * as http from './http'
Expand Down Expand Up @@ -60,7 +60,7 @@ export async function staticCommandPostback(command: string, args: any[], option

response = await http.postJSON<DotvvmStaticCommandResponse>(
getInitialUrl(),
JSON.stringify(data),
jsonStringify(data),
options.abortSignal,
{ "X-PostbackType": "StaticCommand" }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,14 @@ function findObject(obj: any, matcher: (o: any) => boolean): string[] | null {
}
return null;
}

export function jsonStringify(value: any, indent = compileConstants.debug ? " " : undefined): string {
return JSON.stringify(value, (key, val) => {
if (typeof val === "number") {
if (!isFinite(val)) {
return String(val)
}
}
return val
}, indent)
}
4 changes: 3 additions & 1 deletion src/Framework/Framework/Resources/Scripts/shared-classes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { jsonStringify } from "./serialization/serialize"

export class DotvvmPostbackError {
constructor(public reason: DotvvmPostbackErrorReason) {
}
toString() { return "PostbackRejectionError(" + JSON.stringify(this.reason, null, " ") + ")"}
toString() { return "PostbackRejectionError(" + jsonStringify(this.reason) + ")"}
}

export class CoerceError extends Error implements CoerceErrorType {
Expand Down
5 changes: 3 additions & 2 deletions src/Framework/Framework/Resources/Scripts/state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export class StateManager<TViewModel extends { $type?: TypeDefinition }> impleme
initialState: DeepReadonly<TViewModel>,
public stateUpdateEvent?: DotvvmEvent<DeepReadonly<TViewModel>>
) {
this._state = coerce(initialState, initialState.$type || { type: "dynamic" })
this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], () => this._state, u => this.updateState(u as any))
const type = initialState.$type
this._state = coerce(initialState, type || { type: "dynamic" })
this.stateObservable = createWrappedObservable(this._state, type, () => this._state, u => this.updateState(u as any))
this.dispatchUpdate()
}

Expand Down
36 changes: 36 additions & 0 deletions src/Framework/Framework/Resources/Scripts/tests/coercer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,42 @@ test("number - valid, convert from string and keep decimal places", () => {
expect(result.value).toEqual(1234.56);
})

test("float - special values, NaN from string", () => {
const result = tryCoerce("NaN", "Single");
expect(result.wasCoerced).toBeTruthy();
expect(Number.isNaN(result.value)).toBeTruthy();
})

test("float - special values, Infinity from string", () => {
const result = tryCoerce("Infinity", "Single");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(Infinity);
})

test("float - special values, -Infinity from string", () => {
const result = tryCoerce("-Infinity", "Single");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(-Infinity);
})

test("double - special values, NaN from string", () => {
const result = tryCoerce("NaN", "Double");
expect(result.wasCoerced).toBeTruthy();
expect(Number.isNaN(result.value)).toBeTruthy();
})

test("double - special values, Infinity from string", () => {
const result = tryCoerce("Infinity", "Double");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(Infinity);
})

test("double - special values, -Infinity from string", () => {
const result = tryCoerce("-Infinity", "Double");
expect(result.wasCoerced).toBeTruthy();
expect(result.value).toEqual(-Infinity);
})

test("number - invalid, out of range", () => {
const result = tryCoerce(100000, "Int16");
expect(result.isError).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dotvvm from '../dotvvm-root'
import { deserialize } from '../serialization/deserialize'
import { serialize } from '../serialization/serialize'
import { serialize, jsonStringify } from '../serialization/serialize'
import { serializeDate } from '../serialization/date'
import { tryCoerce } from '../metadata/coercer';
import { createComplexObservableSubViewmodel, createComplexObservableViewmodel, ObservableHierarchy, ObservableSubHierarchy, ObservableSubSubHierarchy } from "./observableHierarchies"
Expand Down Expand Up @@ -676,6 +676,12 @@ describe("DotVVM.Serialization - serialize", () => {
expect(d).toBe("2015-08-01T13:56:42.0000000")
})

test("jsonStringify - float special values", () => {
const data = [1, NaN, Infinity, -Infinity, 2];
const result = jsonStringify(data);
expect(result).toBe(`[\n 1,\n "NaN",\n "Infinity",\n "-Infinity",\n 2\n]`)
})

test("Serialize object with Date property", () => {
const obj = serialize({
$type: ko.observable("t3"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ initDotvvm({
]
}
}
},
tFloats: {
type: "object",
properties: {
FloatNaN: { type: "Double" },
FloatInfinity: { type: "Double" },
FloatNegativeInfinity: { type: "Single" }
}
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,62 @@ test("Initial knockout ViewModel", () => {
expect(warnMock).toHaveBeenCalledTimes(0);
})

test("NaN/Infinity values in state and observables", () => {
printTheWarning = true

// Create a separate state manager for this test to avoid interfering with the global one
const testStateManager = new StateManager({
$type: "tFloats",
FloatNaN: "NaN",
FloatInfinity: Infinity,
FloatNegativeInfinity: -Infinity
} as any)

const testVm = testStateManager.stateObservable as any
testStateManager.doUpdateNow()

// Check initial values in observables
expect(Number.isNaN(testVm().FloatNaN())).toBeTruthy()
expect(testVm().FloatInfinity()).toBe(Infinity)
expect(testVm().FloatNegativeInfinity()).toBe(-Infinity)

// Check that values are correctly stored in dotvvm.state
expect(Number.isNaN((testStateManager.state as any).FloatNaN)).toBeTruthy()
expect((testStateManager.state as any).FloatInfinity).toBe(Infinity)
expect((testStateManager.state as any).FloatNegativeInfinity).toBe(-Infinity)

// Update state with new NaN/Infinity values
testStateManager.setState({
...testStateManager.state,
FloatNaN: -Infinity,
FloatInfinity: NaN,
FloatNegativeInfinity: "Infinity"
} as any)
testStateManager.doUpdateNow()

// Check that observables are correctly updated
expect(testVm().FloatNaN()).toBe(-Infinity)
expect(Number.isNaN(testVm().FloatInfinity())).toBeTruthy()
expect(testVm().FloatNegativeInfinity()).toBe(Infinity)

// Check that state is correctly updated
expect((testStateManager.state as any).FloatNaN).toBe(-Infinity)
expect(Number.isNaN((testStateManager.state as any).FloatInfinity)).toBeTruthy()
expect((testStateManager.state as any).FloatNegativeInfinity).toBe(Infinity)

// Update via observables and check state
testVm().FloatNaN(NaN)
testVm().FloatInfinity(Infinity)
testVm().FloatNegativeInfinity(-Infinity)

expect(Number.isNaN((testStateManager.state as any).FloatNaN)).toBeTruthy()
expect((testStateManager.state as any).FloatInfinity).toBe(Infinity)
expect((testStateManager.state as any).FloatNegativeInfinity).toBe(-Infinity)

expect(warnMock).toHaveBeenCalledTimes(0)
})


test("Dirty flag", () => {
expect(s.isDirty).toBeFalsy()
s.setState(s.state) // same state should do nothing
Expand Down
40 changes: 39 additions & 1 deletion src/Framework/Framework/Utils/SystemTextJsonUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DotVVM.Framework.Utils
{
Expand Down Expand Up @@ -133,6 +134,34 @@ public static int GetValueLength(this in Utf8JsonReader reader)
return reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length;
}

public static float GetFloat32Value(this in Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.String)
return (float)reader.GetStringFloatValue();
return reader.GetSingle();
}

public static double GetFloat64Value(this in Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.String)
return reader.GetStringFloatValue();
return reader.GetDouble();
}

public static double GetStringFloatValue(this in Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.String)
{
var stringValue = reader.GetString()!;
return stringValue switch {
"Infinity" => double.PositiveInfinity,
"-Infinity" => double.NegativeInfinity,
_ => double.Parse(stringValue)
};
}
return reader.GetDouble();
}

public static void WriteFloatValue(Utf8JsonWriter writer, double number)
{
#if DotNetCore
Expand Down Expand Up @@ -161,7 +190,7 @@ static void WriteNonFiniteFloatValue(Utf8JsonWriter writer, float number)
if (double.IsNaN(number))
writer.WriteStringValue("NaN"u8);
else if (double.IsPositiveInfinity(number))
writer.WriteStringValue("+Infinity"u8);
writer.WriteStringValue("Infinity"u8);
else if (double.IsNegativeInfinity(number))
writer.WriteStringValue("-Infinity"u8);
else
Expand Down Expand Up @@ -204,4 +233,13 @@ public static T Deserialize<T>(ref Utf8JsonReader reader, JsonSerializerOptions
return JsonSerializer.Deserialize<T>(ref reader, options)!;
}
}

#if NET6_0_OR_GREATER
public class HalfJsonConverter : JsonConverter<Half>
{
public override Half Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => (Half)reader.GetFloat32Value();

public override void Write(Utf8JsonWriter writer, Half value, JsonSerializerOptions options) => SystemTextJsonUtils.WriteFloatValue(writer, (float)value);
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,10 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio
if (type == typeof(decimal))
return Call(reader, "GetDecimal", Type.EmptyTypes);
if (type == typeof(double))
return Call(reader, "GetDouble", Type.EmptyTypes);
return Call(
typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.GetFloat64Value))!,
reader
);
if (type == typeof(Guid))
return Call(reader, "GetGuid", Type.EmptyTypes);
if (type == typeof(short))
Expand All @@ -610,7 +613,17 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio
if (type == typeof(sbyte))
return Call(reader, "GetSByte", Type.EmptyTypes);
if (type == typeof(float))
return Call(reader, "GetSingle", Type.EmptyTypes);
return Call(
typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.GetFloat32Value))!,
reader
);
#if NET6_0_OR_GREATER
if (type == typeof(Half))
return Convert(Call(
typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.GetFloat32Value))!,
reader
), typeof(Half));
#endif
if (type == typeof(string))
return Call(reader, "GetString", Type.EmptyTypes);
if (type == typeof(ushort))
Expand All @@ -636,6 +649,13 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio
typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.WriteFloatValue), [ typeof(Utf8JsonWriter), type ])!,
writer, value
);
#if NET6_0_OR_GREATER
if (type == typeof(Half))
return Call(
typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.WriteFloatValue), [ typeof(Utf8JsonWriter), typeof(float) ])!,
writer, Convert(value, typeof(float))
);
#endif
if (type == typeof(decimal) ||
type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong))
return Call(writer, "WriteNumberValue", Type.EmptyTypes, value);
Expand Down
Loading