Skip to content

[dotnet] [bidi] Add strongly-typed LocalValue.ConvertFrom overloads #15532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 13, 2025
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
253 changes: 196 additions & 57 deletions dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace OpenQA.Selenium.BiDi.Modules.Script;

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

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

case bool b:
return new BooleanLocalValue(b);
return ConvertFrom(b);

case int i:
return new NumberLocalValue(i);
return ConvertFrom(i);

case double d:
return new NumberLocalValue(d);
return ConvertFrom(d);

case long l:
return new NumberLocalValue(l);
return ConvertFrom(l);

case DateTime dt:
return new DateLocalValue(dt.ToString("o"));
case DateTimeOffset dt:
return ConvertFrom(dt);

case BigInteger bigInt:
return new BigIntLocalValue(bigInt.ToString());
return ConvertFrom(bigInt);

case string str:
return new StringLocalValue(str);
return ConvertFrom(str);

case IDictionary dictionary:
case Regex regex:
return ConvertFrom(regex);

case { } when value.GetType().GetInterfaces()
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
{
var bidiObject = new List>(dictionary.Count);
foreach (var item in dictionary)
{
bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
}
IEnumerable set = (IEnumerable)value;

return new ObjectLocalValue(bidiObject);
}
List setValues = [];

case IDictionary dictionary:
{
var bidiObject = new List>(dictionary.Count);
foreach (var item in dictionary)
foreach (var obj in set)
{
bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
setValues.Add(ConvertFrom(obj));
}

return new ObjectLocalValue(bidiObject);
return new SetLocalValue(setValues);
}

case IDictionary dictionary:
{
var bidiObject = new List>(dictionary.Count);
foreach (var item in dictionary)
{
bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
}
case IDictionary dictionary:
return ConvertFrom(dictionary);

return new MapLocalValue(bidiObject);
}
case IEnumerable enumerable:
return ConvertFrom(enumerable);

case IEnumerable list:
return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
default:
return ReflectionBasedConvertFrom(value);
}
}

case object:
{
const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
public static LocalValue ConvertFrom(bool? value)
{
if (value is bool b)
{
return new BooleanLocalValue(b);
}

var properties = value.GetType().GetProperties(Flags);
return new NullLocalValue();
}

var values = new List>(properties.Length);
foreach (var property in properties)
{
object? propertyValue;
try
{
propertyValue = property.GetValue(value);
}
catch (Exception ex)
{
throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
}
values.Add([property.Name, ConvertFrom(propertyValue)]);
}
public static LocalValue ConvertFrom(int? value)
{
if (value is int b)
{
return new NumberLocalValue(b);
}

return new ObjectLocalValue(values);
}
return new NullLocalValue();
}

public static LocalValue ConvertFrom(double? value)
{
if (value is double b)
{
return new NumberLocalValue(b);
}

return new NullLocalValue();
}

public static LocalValue ConvertFrom(long? value)
{
if (value is long b)
{
return new NumberLocalValue(b);
}

return new NullLocalValue();
}

public static LocalValue ConvertFrom(string? value)
{
if (value is not null)
{
return new StringLocalValue(value);
}

return new NullLocalValue();
}

///
/// Converts a .NET Regex into a BiDi Regex
///
/// A .NET Regex.
/// A BiDi Regex.
///
/// Note that the .NET regular expression engine does not work the same as the JavaScript engine.
/// To minimize the differences between the two engines, it is recommended to enabled the option.
///
public static LocalValue ConvertFrom(Regex? regex)
{
if (regex is null)
{
return new NullLocalValue();
}

string? flags = RegExpValue.GetRegExpFlags(regex.Options);

return new RegExpLocalValue(new RegExpValue(regex.ToString()) { Flags = flags });
}

public static LocalValue ConvertFrom(DateTimeOffset? value)
{
if (value is null)
{
return new NullLocalValue();
}

return new DateLocalValue(value.Value.ToString("o"));
}

public static LocalValue ConvertFrom(BigInteger? value)
{
if (value is not null)
{
return new BigIntLocalValue(value.Value.ToString());
}

return new NullLocalValue();
}

public static LocalValue ConvertFrom(IEnumerable? value)
{
if (value is null)
{
return new NullLocalValue();
}

List list = [];

foreach (var element in value)
{
list.Add(ConvertFrom(element));
}

return new ArrayLocalValue(list);
}

public static LocalValue ConvertFrom(IDictionary? value)
{
if (value is null)
{
return new NullLocalValue();
}

var bidiObject = new List>(value.Count);

foreach (var key in value.Keys)
{
bidiObject.Add([ConvertFrom(key), ConvertFrom(value[key])]);
}

return new MapLocalValue(bidiObject);
}

public static LocalValue ConvertFrom(ISet? value)
{
if (value is null)
{
return new NullLocalValue();
}

LocalValue[] convertedValues = [.. value.Select(x => ConvertFrom(x))];

return new SetLocalValue(convertedValues);
}

private static LocalValue ReflectionBasedConvertFrom(object? value)
{
if (value is null)
{
return new NullLocalValue();
}

const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;

System.Reflection.PropertyInfo[] properties = value.GetType().GetProperties(Flags);

var values = new List>(properties.Length);

foreach (System.Reflection.PropertyInfo? property in properties)
{
object? propertyValue;

try
{
propertyValue = property.GetValue(value);
}
catch (Exception ex)
{
throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
}

values.Add([property.Name, ConvertFrom(propertyValue)]);
}

return new ObjectLocalValue(values);
}
}

Expand Down
53 changes: 53 additions & 0 deletions dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,62 @@
// under the License.
//

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace OpenQA.Selenium.BiDi.Modules.Script;

public record RegExpValue(string Pattern)
{
public string? Flags { get; set; }

internal static string? GetRegExpFlags(RegexOptions options)
{
if (options == RegexOptions.None)
{
return null;
}

string flags = string.Empty;
const RegexOptions NonBacktracking = (RegexOptions)1024;
#if NET8_0_OR_GREATER
Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking);
#endif
const RegexOptions NonApplicableOptions = RegexOptions.Compiled | NonBacktracking;

const RegexOptions UnsupportedOptions =
RegexOptions.ExplicitCapture |
RegexOptions.IgnorePatternWhitespace |
RegexOptions.RightToLeft |
RegexOptions.CultureInvariant;

options &= ~NonApplicableOptions;
if ((options & UnsupportedOptions) != 0)
{
throw new NotSupportedException($"The selected RegEx options are not supported in BiDi: {options & UnsupportedOptions}");
}

if ((options & RegexOptions.IgnoreCase) != 0)
{
flags += "i";
options = options & ~RegexOptions.IgnoreCase;
}

if ((options & RegexOptions.Multiline) != 0)
{
options = options & ~RegexOptions.Multiline;
flags += "m";
}

if ((options & RegexOptions.Singleline) != 0)
{
options = options & ~RegexOptions.Singleline;
flags += "s";
}

Debug.Assert(options == RegexOptions.None);

return flags;
}
}
Loading