Skip to content

Commit 2edd331

Browse files
authored
🚸 make package netstandard2.0 compatible (#7)
1 parent 077ae31 commit 2edd331

File tree

10 files changed

+253
-79
lines changed

10 files changed

+253
-79
lines changed

QsNet.Tests/UtilsTests.cs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,36 +1354,6 @@ public void ToDictionary_Returns_Same_Instance_When_Already_ObjectKeyed()
13541354
res.Should().BeSameAs(map);
13551355
}
13561356

1357-
[Fact]
1358-
public void ToStringKeyedDictionary_Converts_Keys_To_String()
1359-
{
1360-
var src = new OrderedDictionary
1361-
{
1362-
{ "a", 1 }, // string key
1363-
{ 2, "b" }, // int key
1364-
{ "", 3 } // null key
1365-
};
1366-
1367-
var dict =
1368-
(Dictionary<string, object?>)
1369-
typeof(Utils)
1370-
.GetMethod(
1371-
"ToStringKeyedDictionary",
1372-
BindingFlags.NonPublic | BindingFlags.Static
1373-
)!
1374-
.Invoke(null, [src])!;
1375-
1376-
dict.Should()
1377-
.BeEquivalentTo(
1378-
new Dictionary<string, object?>
1379-
{
1380-
["a"] = 1,
1381-
["2"] = "b", // int key → "2"
1382-
[""] = 3 // null key → ""
1383-
}
1384-
);
1385-
}
1386-
13871357
[Fact]
13881358
public void ConvertDictionaryToStringKeyed_Does_Not_Copy_If_Already_StringKeyed()
13891359
{

QsNet/Compat/IsExternalInit.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#if NETSTANDARD2_0
2+
namespace System.Runtime.CompilerServices
3+
{
4+
/// <summary>
5+
/// Polyfill for init-only setters on netstandard2.0.
6+
/// </summary>
7+
internal static class IsExternalInit
8+
{
9+
}
10+
}
11+
#endif

QsNet/Enums/ListFormat.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public enum ListFormat
4141
/// </summary>
4242
public static class ListFormatExtensions
4343
{
44+
private static readonly ListFormatGenerator BracketsGen = (p, _) => $"{p}[]";
45+
private static readonly ListFormatGenerator CommaGen = (p, _) => p;
46+
private static readonly ListFormatGenerator RepeatGen = (p, _) => p;
47+
private static readonly ListFormatGenerator IndicesGen = (p, k) => $"{p}[{k}]";
48+
4449
/// <summary>
4550
/// Gets the generator function for the specified list format.
4651
/// </summary>
@@ -50,10 +55,10 @@ public static ListFormatGenerator GetGenerator(this ListFormat format)
5055
{
5156
return format switch
5257
{
53-
ListFormat.Brackets => (prefix, _) => $"{prefix}[]",
54-
ListFormat.Comma => (prefix, _) => prefix,
55-
ListFormat.Repeat => (prefix, _) => prefix,
56-
ListFormat.Indices => (prefix, key) => $"{prefix}[{key}]",
58+
ListFormat.Brackets => BracketsGen,
59+
ListFormat.Comma => CommaGen,
60+
ListFormat.Repeat => RepeatGen,
61+
ListFormat.Indices => IndicesGen,
5762
_ => throw new ArgumentOutOfRangeException(nameof(format))
5863
};
5964
}

QsNet/Internal/Decoder.cs

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,42 @@ namespace QsNet.Internal;
1212
/// <summary>
1313
/// A helper class for decoding query strings into structured data.
1414
/// </summary>
15+
#if NETSTANDARD2_0
16+
internal static class Decoder
17+
#else
1518
internal static partial class Decoder
19+
#endif
1620
{
1721
/// <summary>
1822
/// Regular expression to match dots followed by non-dot and non-bracket characters.
1923
/// This is used to replace dots in keys with brackets for parsing.
2024
/// </summary>
2125
private static readonly Regex DotToBracket = MyRegex();
2226

27+
#if NETSTANDARD2_0
28+
private static readonly Regex MyRegexInstance = new(@"\.([^.\[]+)", RegexOptions.Compiled);
29+
private static Regex MyRegex()
30+
{
31+
return MyRegexInstance;
32+
}
33+
#else
2334
[GeneratedRegex(@"\.([^.\[]+)", RegexOptions.Compiled)]
2435
private static partial Regex MyRegex();
36+
#endif
37+
38+
private static Encoding Latin1Encoding =>
39+
#if NETSTANDARD2_0
40+
Encoding.GetEncoding(28591);
41+
#else
42+
Encoding.Latin1;
43+
#endif
44+
45+
private static bool IsLatin1(Encoding e) =>
46+
#if NETSTANDARD2_0
47+
e is { CodePage: 28591 };
48+
#else
49+
Equals(e, Encoding.Latin1);
50+
#endif
2551

2652
/// <summary>
2753
/// Parses a list value from a string or any other type, applying the options provided.
@@ -70,9 +96,15 @@ int currentListLength
7096
options ??= new DecodeOptions();
7197
var obj = new Dictionary<string, object?>();
7298

99+
#if NETSTANDARD2_0
100+
var cleanStr = options.IgnoreQueryPrefix ? str.TrimStart('?') : str;
101+
cleanStr = ReplaceOrdinalIgnoreCase(cleanStr, "%5B", "[");
102+
cleanStr = ReplaceOrdinalIgnoreCase(cleanStr, "%5D", "]");
103+
#else
73104
var cleanStr = (options.IgnoreQueryPrefix ? str.TrimStart('?') : str)
74105
.Replace("%5B", "[", StringComparison.OrdinalIgnoreCase)
75106
.Replace("%5D", "]", StringComparison.OrdinalIgnoreCase);
107+
#endif
76108

77109
var limit = options.ParameterLimit == int.MaxValue ? (int?)null : options.ParameterLimit;
78110

@@ -106,7 +138,7 @@ int currentListLength
106138
charset = parts[i] switch
107139
{
108140
var p when p == Sentinel.Charset.GetEncoded() => Encoding.UTF8,
109-
var p when p == Sentinel.Iso.GetEncoded() => Encoding.Latin1,
141+
var p when p == Sentinel.Iso.GetEncoded() => Latin1Encoding,
110142
_ => charset
111143
};
112144
skipIndex = i;
@@ -132,21 +164,32 @@ int currentListLength
132164
}
133165
else
134166
{
167+
#if NETSTANDARD2_0
168+
key = options.GetDecoder(part.Substring(0, pos), charset)?.ToString() ?? string.Empty;
169+
#else
135170
key = options.GetDecoder(part[..pos], charset)?.ToString() ?? string.Empty;
171+
#endif
136172
var currentLength =
137173
obj.TryGetValue(key, out var val) && val is IList<object?> list ? list.Count : 0;
138174

175+
#if NETSTANDARD2_0
176+
value = Utils.Apply<object?>(
177+
ParseListValue(part.Substring(pos + 1), options, currentLength),
178+
v => options.GetDecoder(v?.ToString(), charset)
179+
);
180+
#else
139181
value = Utils.Apply<object?>(
140182
ParseListValue(part[(pos + 1)..], options, currentLength),
141183
v => options.GetDecoder(v?.ToString(), charset)
142184
);
185+
#endif
143186
}
144187

145188
if (
146189
value != null
147190
&& !Utils.IsEmpty(value)
148191
&& options.InterpretNumericEntities
149-
&& Equals(charset, Encoding.Latin1)
192+
&& IsLatin1(charset)
150193
)
151194
value = Utils.InterpretNumericEntities(
152195
value switch
@@ -190,7 +233,13 @@ bool valuesParsed
190233
)
191234
{
192235
var currentListLength = 0;
193-
if (chain.Count > 0 && chain[^1] == "[]")
236+
if (chain.Count > 0 &&
237+
#if NETSTANDARD2_0
238+
chain[chain.Count - 1] == "[]"
239+
#else
240+
chain[^1] == "[]"
241+
#endif
242+
)
194243
{
195244
var parentKeyStr = string.Join("", chain.Take(chain.Count - 1));
196245
if (
@@ -232,7 +281,13 @@ bool valuesParsed
232281
else
233282
{
234283
// Unwrap [ ... ] and (optionally) decode %2E -> .
284+
#if NETSTANDARD2_0
285+
var cleanRoot = root.StartsWith("[") && root.EndsWith("]")
286+
? root.Substring(1, root.Length - 2)
287+
: root;
288+
#else
235289
var cleanRoot = root.StartsWith('[') && root.EndsWith(']') ? root[1..^1] : root;
290+
#endif
236291
var decodedRoot = options.DecodeDotInKeys
237292
? cleanRoot.Replace("%2E", ".")
238293
: cleanRoot;
@@ -298,7 +353,7 @@ bool valuesParsed
298353
return null;
299354

300355
var segments = SplitKeyIntoSegments(
301-
givenKey,
356+
givenKey!,
302357
options.AllowDots,
303358
options.Depth,
304359
options.StrictDepth
@@ -335,7 +390,11 @@ bool strictDepth
335390
var segments = new List<string>();
336391

337392
var first = key.IndexOf('[');
393+
#if NETSTANDARD2_0
394+
var parent = first >= 0 ? key.Substring(0, first) : key;
395+
#else
338396
var parent = first >= 0 ? key[..first] : key;
397+
#endif
339398
if (!string.IsNullOrEmpty(parent))
340399
segments.Add(parent);
341400

@@ -346,7 +405,11 @@ bool strictDepth
346405
var close = key.IndexOf(']', open + 1);
347406
if (close < 0)
348407
break;
408+
#if NETSTANDARD2_0
409+
segments.Add(key.Substring(open, close + 1 - open)); // e.g. "[p]" or "[]"
410+
#else
349411
segments.Add(key[open..(close + 1)]); // e.g. "[p]" or "[]"
412+
#endif
350413
depth++;
351414
open = key.IndexOf('[', close + 1);
352415
}
@@ -357,9 +420,39 @@ bool strictDepth
357420
throw new IndexOutOfRangeException(
358421
$"Input depth exceeded depth option of {maxDepth} and strictDepth is true"
359422
);
360-
// Stash the remainder as a single segment.
423+
#if NETSTANDARD2_0
424+
segments.Add("[" + key.Substring(open) + "]");
425+
#else
361426
segments.Add("[" + key[open..] + "]");
427+
#endif
362428

363429
return segments;
364430
}
431+
432+
#if NETSTANDARD2_0
433+
// Efficient case-insensitive ordinal string replace for NETSTANDARD2_0 (no regex, no allocations beyond matches)
434+
private static string ReplaceOrdinalIgnoreCase(string input, string oldValue, string newValue)
435+
{
436+
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(oldValue))
437+
return input;
438+
439+
var startIndex = 0;
440+
StringBuilder? sb = null;
441+
while (true)
442+
{
443+
var idx = input.IndexOf(oldValue, startIndex, StringComparison.OrdinalIgnoreCase);
444+
if (idx < 0)
445+
{
446+
if (sb == null) return input;
447+
sb.Append(input, startIndex, input.Length - startIndex);
448+
return sb.ToString();
449+
}
450+
451+
sb ??= new StringBuilder(input.Length);
452+
sb.Append(input, startIndex, idx - startIndex);
453+
sb.Append(newValue);
454+
startIndex = idx + oldValue.Length;
455+
}
456+
}
457+
#endif
365458
}

0 commit comments

Comments
 (0)