Skip to content

Commit e4699c0

Browse files
committed
✅ add QsNet.Comparison project for qs Node.js compatibility
1 parent cda89ac commit e4699c0

File tree

8 files changed

+560
-0
lines changed

8 files changed

+560
-0
lines changed

QsNet.Comparison/Program.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text;
3+
using STJ = System.Text.Json;
4+
using NJ = Newtonsoft.Json;
5+
6+
namespace QsNet.Comparison;
7+
8+
[ExcludeFromCodeCoverage]
9+
internal abstract class Program
10+
{
11+
private static void Main()
12+
{
13+
Console.OutputEncoding = new UTF8Encoding(false);
14+
15+
var jsonPath = Path.Combine(AppContext.BaseDirectory, "js", "test_cases.json");
16+
if (!File.Exists(jsonPath))
17+
{
18+
Console.Error.WriteLine($"Missing test_cases.json at {jsonPath}");
19+
Environment.Exit(1);
20+
}
21+
22+
var json = File.ReadAllText(jsonPath);
23+
24+
var cases = STJ.JsonSerializer.Deserialize<List<TestCase>>(json, new STJ.JsonSerializerOptions
25+
{
26+
PropertyNameCaseInsensitive = true
27+
});
28+
29+
if (cases is null)
30+
{
31+
Console.Error.WriteLine("No test cases loaded.");
32+
Environment.Exit(1);
33+
}
34+
35+
var percentEncodeBrackets = true;
36+
37+
foreach (var c in cases)
38+
{
39+
// Convert JSON element -> CLR object graph
40+
var dataObj = c.Data.HasValue ? FromJsonElement(c.Data.Value) : null;
41+
42+
// Encode (optionally percent-encode '[' and ']')
43+
var encodedOut = Qs.Encode(dataObj ?? new Dictionary<string, object?>());
44+
if (percentEncodeBrackets)
45+
encodedOut = encodedOut.Replace("[", "%5B").Replace("]", "%5D");
46+
Console.WriteLine($"Encoded: {encodedOut}");
47+
48+
// Decode and JSON-serialize with Newtonsoft (keeps emojis as real characters)
49+
var decodedOut = Qs.Decode(c.Encoded!);
50+
Console.WriteLine($"Decoded: {CanonJson(decodedOut)}");
51+
}
52+
}
53+
54+
// Use Newtonsoft so emojis aren’t turned into surrogate-pair escapes
55+
private static string CanonJson(object? v)
56+
{
57+
return NJ.JsonConvert.SerializeObject(
58+
v,
59+
NJ.Formatting.None,
60+
new NJ.JsonSerializerSettings
61+
{
62+
// Default doesn't escape non-ASCII; keep it explicit in case of future changes
63+
StringEscapeHandling = NJ.StringEscapeHandling.Default
64+
}
65+
);
66+
}
67+
68+
private static object? FromJsonElement(STJ.JsonElement e)
69+
{
70+
switch (e.ValueKind)
71+
{
72+
case STJ.JsonValueKind.Null:
73+
case STJ.JsonValueKind.Undefined:
74+
return null;
75+
76+
case STJ.JsonValueKind.String:
77+
return e.GetString();
78+
79+
case STJ.JsonValueKind.Number:
80+
// keep numbers as strings to match qs parse/stringify behavior
81+
return e.GetRawText().Trim('"');
82+
83+
case STJ.JsonValueKind.True:
84+
case STJ.JsonValueKind.False:
85+
return e.GetBoolean();
86+
87+
case STJ.JsonValueKind.Array:
88+
{
89+
var list = new List<object?>();
90+
foreach (var item in e.EnumerateArray())
91+
list.Add(FromJsonElement(item));
92+
return list;
93+
}
94+
95+
case STJ.JsonValueKind.Object:
96+
{
97+
var dict = new Dictionary<string, object?>();
98+
foreach (var prop in e.EnumerateObject())
99+
dict[prop.Name] = FromJsonElement(prop.Value);
100+
return dict;
101+
}
102+
103+
default:
104+
return e.GetRawText();
105+
}
106+
}
107+
108+
private class TestCase
109+
{
110+
public string? Encoded { get; set; }
111+
public STJ.JsonElement? Data { get; set; }
112+
}
113+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
9+
<IsPublishable>false</IsPublishable>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<ProjectReference Include="..\QsNet\QsNet.csproj" />
13+
<None Include="js\test_cases.json" CopyToOutputDirectory="PreserveNewest" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
17+
</ItemGroup>
18+
</Project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
script_dir=$(dirname "$0")
3+
4+
node_output=$(node "$script_dir/js/qs.js")
5+
cs_output=$(dotnet run --project "$script_dir/../QsNet.Comparison" -c Release)
6+
7+
if [ "$node_output" == "$cs_output" ]; then
8+
echo "The outputs are identical."
9+
exit 0
10+
else
11+
echo "The outputs are different."
12+
diff <(echo "$node_output") <(echo "$cs_output")
13+
exit 1
14+
fi

QsNet.Comparison/js/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
package-lock.json

QsNet.Comparison/js/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "qs_comparison",
3+
"version": "1.0.0",
4+
"description": "A comparison of query string parsing libraries",
5+
"author": "Klemen Tusar",
6+
"license": "BSD-3-Clause",
7+
"dependencies": {
8+
"qs": "^6.14.0"
9+
}
10+
}

QsNet.Comparison/js/qs.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const qs = require('qs');
4+
5+
// Use path.join to combine __dirname with the relative path to test_cases.json
6+
const filePath = path.join(__dirname, 'test_cases.json');
7+
const e2eTestCases = JSON.parse(fs.readFileSync(filePath).toString());
8+
9+
e2eTestCases.forEach(testCase => {
10+
console.log('Encoded:', qs.stringify(testCase.data));
11+
console.log('Decoded:', JSON.stringify(qs.parse(testCase.encoded)));
12+
});

0 commit comments

Comments
 (0)