Skip to content

Commit 5534d2e

Browse files
[dotnet] [bidi] Add strongly-typed LocalValue.ConvertFrom overloads (#15532)
This is the basis to support all well-known .NET types later. --------- Co-authored-by: Nikolay Borisenko <[email protected]>
1 parent 12a1593 commit 5534d2e

File tree

3 files changed

+415
-80
lines changed

3 files changed

+415
-80
lines changed

dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs

Lines changed: 196 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
// </copyright>
1919

2020
using System;
21+
using System.Collections;
2122
using System.Collections.Generic;
2223
using System.Linq;
2324
using System.Numerics;
2425
using System.Text.Json.Serialization;
26+
using System.Text.RegularExpressions;
2527

2628
namespace OpenQA.Selenium.BiDi.Modules.Script;
2729

@@ -41,10 +43,11 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
4143
[JsonDerivedType(typeof(SetLocalValue), "set")]
4244
public abstract record LocalValue
4345
{
44-
public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); }
45-
public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); }
46-
public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); }
47-
public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); }
46+
public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); }
47+
public static implicit operator LocalValue(int? value) { return ConvertFrom(value); }
48+
public static implicit operator LocalValue(double? value) { return ConvertFrom(value); }
49+
public static implicit operator LocalValue(string? value) { return ConvertFrom(value); }
50+
public static implicit operator LocalValue(DateTimeOffset? value) { return ConvertFrom(value); }
4851

4952
// TODO: Extend converting from types
5053
public static LocalValue ConvertFrom(object? value)
@@ -58,86 +61,222 @@ public static LocalValue ConvertFrom(object? value)
5861
return new NullLocalValue();
5962

6063
case bool b:
61-
return new BooleanLocalValue(b);
64+
return ConvertFrom(b);
6265

6366
case int i:
64-
return new NumberLocalValue(i);
67+
return ConvertFrom(i);
6568

6669
case double d:
67-
return new NumberLocalValue(d);
70+
return ConvertFrom(d);
6871

6972
case long l:
70-
return new NumberLocalValue(l);
73+
return ConvertFrom(l);
7174

72-
case DateTime dt:
73-
return new DateLocalValue(dt.ToString("o"));
75+
case DateTimeOffset dt:
76+
return ConvertFrom(dt);
7477

7578
case BigInteger bigInt:
76-
return new BigIntLocalValue(bigInt.ToString());
79+
return ConvertFrom(bigInt);
7780

7881
case string str:
79-
return new StringLocalValue(str);
82+
return ConvertFrom(str);
8083

81-
case IDictionary<string, string?> dictionary:
84+
case Regex regex:
85+
return ConvertFrom(regex);
86+
87+
case { } when value.GetType().GetInterfaces()
88+
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
8289
{
83-
var bidiObject = new List<List<LocalValue>>(dictionary.Count);
84-
foreach (var item in dictionary)
85-
{
86-
bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
87-
}
90+
IEnumerable set = (IEnumerable)value;
8891

89-
return new ObjectLocalValue(bidiObject);
90-
}
92+
List<LocalValue> setValues = [];
9193

92-
case IDictionary<string, object?> dictionary:
93-
{
94-
var bidiObject = new List<List<LocalValue>>(dictionary.Count);
95-
foreach (var item in dictionary)
94+
foreach (var obj in set)
9695
{
97-
bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
96+
setValues.Add(ConvertFrom(obj));
9897
}
9998

100-
return new ObjectLocalValue(bidiObject);
99+
return new SetLocalValue(setValues);
101100
}
102101

103-
case IDictionary<int, object?> dictionary:
104-
{
105-
var bidiObject = new List<List<LocalValue>>(dictionary.Count);
106-
foreach (var item in dictionary)
107-
{
108-
bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
109-
}
102+
case IDictionary dictionary:
103+
return ConvertFrom(dictionary);
110104

111-
return new MapLocalValue(bidiObject);
112-
}
105+
case IEnumerable enumerable:
106+
return ConvertFrom(enumerable);
113107

114-
case IEnumerable<object?> list:
115-
return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
108+
default:
109+
return ReflectionBasedConvertFrom(value);
110+
}
111+
}
116112

117-
case object:
118-
{
119-
const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
113+
public static LocalValue ConvertFrom(bool? value)
114+
{
115+
if (value is bool b)
116+
{
117+
return new BooleanLocalValue(b);
118+
}
120119

121-
var properties = value.GetType().GetProperties(Flags);
120+
return new NullLocalValue();
121+
}
122122

123-
var values = new List<List<LocalValue>>(properties.Length);
124-
foreach (var property in properties)
125-
{
126-
object? propertyValue;
127-
try
128-
{
129-
propertyValue = property.GetValue(value);
130-
}
131-
catch (Exception ex)
132-
{
133-
throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
134-
}
135-
values.Add([property.Name, ConvertFrom(propertyValue)]);
136-
}
123+
public static LocalValue ConvertFrom(int? value)
124+
{
125+
if (value is int b)
126+
{
127+
return new NumberLocalValue(b);
128+
}
137129

138-
return new ObjectLocalValue(values);
139-
}
130+
return new NullLocalValue();
131+
}
132+
133+
public static LocalValue ConvertFrom(double? value)
134+
{
135+
if (value is double b)
136+
{
137+
return new NumberLocalValue(b);
138+
}
139+
140+
return new NullLocalValue();
141+
}
142+
143+
public static LocalValue ConvertFrom(long? value)
144+
{
145+
if (value is long b)
146+
{
147+
return new NumberLocalValue(b);
148+
}
149+
150+
return new NullLocalValue();
151+
}
152+
153+
public static LocalValue ConvertFrom(string? value)
154+
{
155+
if (value is not null)
156+
{
157+
return new StringLocalValue(value);
158+
}
159+
160+
return new NullLocalValue();
161+
}
162+
163+
/// <summary>
164+
/// Converts a .NET Regex into a BiDi Regex
165+
/// </summary>
166+
/// <param name="regex">A .NET Regex.</param>
167+
/// <returns>A BiDi Regex.</returns>
168+
/// <remarks>
169+
/// Note that the .NET regular expression engine does not work the same as the JavaScript engine.
170+
/// To minimize the differences between the two engines, it is recommended to enabled the <see cref="RegexOptions.ECMAScript"/> option.
171+
/// </remarks>
172+
public static LocalValue ConvertFrom(Regex? regex)
173+
{
174+
if (regex is null)
175+
{
176+
return new NullLocalValue();
140177
}
178+
179+
string? flags = RegExpValue.GetRegExpFlags(regex.Options);
180+
181+
return new RegExpLocalValue(new RegExpValue(regex.ToString()) { Flags = flags });
182+
}
183+
184+
public static LocalValue ConvertFrom(DateTimeOffset? value)
185+
{
186+
if (value is null)
187+
{
188+
return new NullLocalValue();
189+
}
190+
191+
return new DateLocalValue(value.Value.ToString("o"));
192+
}
193+
194+
public static LocalValue ConvertFrom(BigInteger? value)
195+
{
196+
if (value is not null)
197+
{
198+
return new BigIntLocalValue(value.Value.ToString());
199+
}
200+
201+
return new NullLocalValue();
202+
}
203+
204+
public static LocalValue ConvertFrom(IEnumerable? value)
205+
{
206+
if (value is null)
207+
{
208+
return new NullLocalValue();
209+
}
210+
211+
List<LocalValue> list = [];
212+
213+
foreach (var element in value)
214+
{
215+
list.Add(ConvertFrom(element));
216+
}
217+
218+
return new ArrayLocalValue(list);
219+
}
220+
221+
public static LocalValue ConvertFrom(IDictionary? value)
222+
{
223+
if (value is null)
224+
{
225+
return new NullLocalValue();
226+
}
227+
228+
var bidiObject = new List<List<LocalValue>>(value.Count);
229+
230+
foreach (var key in value.Keys)
231+
{
232+
bidiObject.Add([ConvertFrom(key), ConvertFrom(value[key])]);
233+
}
234+
235+
return new MapLocalValue(bidiObject);
236+
}
237+
238+
public static LocalValue ConvertFrom<T>(ISet<T?>? value)
239+
{
240+
if (value is null)
241+
{
242+
return new NullLocalValue();
243+
}
244+
245+
LocalValue[] convertedValues = [.. value.Select(x => ConvertFrom(x))];
246+
247+
return new SetLocalValue(convertedValues);
248+
}
249+
250+
private static LocalValue ReflectionBasedConvertFrom(object? value)
251+
{
252+
if (value is null)
253+
{
254+
return new NullLocalValue();
255+
}
256+
257+
const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
258+
259+
System.Reflection.PropertyInfo[] properties = value.GetType().GetProperties(Flags);
260+
261+
var values = new List<List<LocalValue>>(properties.Length);
262+
263+
foreach (System.Reflection.PropertyInfo? property in properties)
264+
{
265+
object? propertyValue;
266+
267+
try
268+
{
269+
propertyValue = property.GetValue(value);
270+
}
271+
catch (Exception ex)
272+
{
273+
throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
274+
}
275+
276+
values.Add([property.Name, ConvertFrom(propertyValue)]);
277+
}
278+
279+
return new ObjectLocalValue(values);
141280
}
142281
}
143282

dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,62 @@
1717
// under the License.
1818
// </copyright>
1919

20+
using System;
21+
using System.Diagnostics;
22+
using System.Text.RegularExpressions;
23+
2024
namespace OpenQA.Selenium.BiDi.Modules.Script;
2125

2226
public record RegExpValue(string Pattern)
2327
{
2428
public string? Flags { get; set; }
29+
30+
internal static string? GetRegExpFlags(RegexOptions options)
31+
{
32+
if (options == RegexOptions.None)
33+
{
34+
return null;
35+
}
36+
37+
string flags = string.Empty;
38+
const RegexOptions NonBacktracking = (RegexOptions)1024;
39+
#if NET8_0_OR_GREATER
40+
Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking);
41+
#endif
42+
const RegexOptions NonApplicableOptions = RegexOptions.Compiled | NonBacktracking;
43+
44+
const RegexOptions UnsupportedOptions =
45+
RegexOptions.ExplicitCapture |
46+
RegexOptions.IgnorePatternWhitespace |
47+
RegexOptions.RightToLeft |
48+
RegexOptions.CultureInvariant;
49+
50+
options &= ~NonApplicableOptions;
51+
if ((options & UnsupportedOptions) != 0)
52+
{
53+
throw new NotSupportedException($"The selected RegEx options are not supported in BiDi: {options & UnsupportedOptions}");
54+
}
55+
56+
if ((options & RegexOptions.IgnoreCase) != 0)
57+
{
58+
flags += "i";
59+
options = options & ~RegexOptions.IgnoreCase;
60+
}
61+
62+
if ((options & RegexOptions.Multiline) != 0)
63+
{
64+
options = options & ~RegexOptions.Multiline;
65+
flags += "m";
66+
}
67+
68+
if ((options & RegexOptions.Singleline) != 0)
69+
{
70+
options = options & ~RegexOptions.Singleline;
71+
flags += "s";
72+
}
73+
74+
Debug.Assert(options == RegexOptions.None);
75+
76+
return flags;
77+
}
2578
}

0 commit comments

Comments
 (0)