Skip to content

Commit

Permalink
[dotnet] Make classic WebDriver commands/responses AOT compatible (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
nvborisenko committed Oct 28, 2024
1 parent a2a9baf commit 1cb48a2
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 63 deletions.
41 changes: 36 additions & 5 deletions dotnet/src/webdriver/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ namespace OpenQA.Selenium
/// </summary>
public class Command
{
private SessionId commandSessionId;
private string commandName;
private Dictionary<string, object> commandParameters = new Dictionary<string, object>();

private readonly static JsonSerializerOptions s_jsonSerializerOptions = new()
{
TypeInfoResolver = CommandJsonSerializerContext.Default,
Converters = { new ResponseValueJsonConverter() }
};

private SessionId commandSessionId;
private string commandName;
private Dictionary<string, object> commandParameters = new Dictionary<string, object>();

/// <summary>
/// Initializes a new instance of the <see cref="Command"/> class using a command name and a JSON-encoded string for the parameters.
/// </summary>
Expand Down Expand Up @@ -101,7 +102,7 @@ public string ParametersAsJsonString
string parametersString = string.Empty;
if (this.commandParameters != null && this.commandParameters.Count > 0)
{
parametersString = JsonSerializer.Serialize(this.commandParameters);
parametersString = JsonSerializer.Serialize(this.commandParameters, s_jsonSerializerOptions);
}

if (string.IsNullOrEmpty(parametersString))
Expand Down Expand Up @@ -133,4 +134,34 @@ private static Dictionary<string, object> ConvertParametersFromJson(string value
return parameters;
}
}

// Built-in types
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(byte))]
[JsonSerializable(typeof(sbyte))]
[JsonSerializable(typeof(char))]
[JsonSerializable(typeof(decimal))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(nint))]
[JsonSerializable(typeof(nuint))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(ulong))]
[JsonSerializable(typeof(short))]
[JsonSerializable(typeof(ushort))]

[JsonSerializable(typeof(string))]

// Selenium WebDriver types
[JsonSerializable(typeof(char[]))]
[JsonSerializable(typeof(byte[]))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(Cookie))]
[JsonSerializable(typeof(Proxy))]
internal partial class CommandJsonSerializerContext : JsonSerializerContext
{

}
}
146 changes: 89 additions & 57 deletions dotnet/src/webdriver/Internal/ResponseValueJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,75 +30,107 @@ internal class ResponseValueJsonConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return this.ProcessToken(ref reader, options);
return ProcessReadToken(ref reader, options);
}

public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
switch (value)
{
case null:
writer.WriteNullValue();
break;
case Enum:
writer.WriteNumberValue(Convert.ToInt64(value));
break;
case IEnumerable<object> list:
writer.WriteStartArray();
foreach (var item in list)
{
Write(writer, item, options);
}
writer.WriteEndArray();
break;
case IDictionary<string, object> dictionary:
writer.WriteStartObject();
foreach (var pair in dictionary)
{
writer.WritePropertyName(pair.Key);
Write(writer, pair.Value, options);
}
writer.WriteEndObject();
break;
case object obj:
JsonSerializer.Serialize(writer, obj, options.GetTypeInfo(obj.GetType()));
break;
}
}

private object ProcessToken(ref Utf8JsonReader reader, JsonSerializerOptions options)
private static object ProcessReadToken(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
// Recursively processes a token. This is required for elements that next other elements.
object processedObject = null;
object processedObject;

if (reader.TokenType == JsonTokenType.StartObject)
switch (reader.TokenType)
{
Dictionary<string, object> dictionaryValue = new Dictionary<string, object>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
string elementKey = reader.GetString();
reader.Read();
dictionaryValue.Add(elementKey, this.ProcessToken(ref reader, options));
}
case JsonTokenType.StartObject:
{
Dictionary<string, object> dictionaryValue = [];
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
string elementKey = reader.GetString();
reader.Read();
dictionaryValue.Add(elementKey, ProcessReadToken(ref reader, options));
}

processedObject = dictionaryValue;
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
List<object> arrayValue = new List<object>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
arrayValue.Add(this.ProcessToken(ref reader, options));
}
processedObject = dictionaryValue;
break;
}

processedObject = arrayValue.ToArray();
}
else if (reader.TokenType == JsonTokenType.Null)
{
processedObject = null;
}
else if (reader.TokenType == JsonTokenType.False)
{
processedObject = false;
}
else if (reader.TokenType == JsonTokenType.True)
{
processedObject = true;
}
else if (reader.TokenType == JsonTokenType.String)
{
processedObject = reader.GetString();
}
else if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt64(out long longValue))
{
processedObject = longValue;
}
else if (reader.TryGetDouble(out double doubleValue))
{
processedObject = doubleValue;
}
else
{
throw new JsonException($"Unrecognized '{JsonElement.ParseValue(ref reader)}' token as a number value.");
}
}
else
{
throw new JsonException($"Unrecognized '{reader.TokenType}' token type while parsing command response.");
case JsonTokenType.StartArray:
{
List<object> arrayValue = [];
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
arrayValue.Add(ProcessReadToken(ref reader, options));
}

processedObject = arrayValue.ToArray();
break;
}

case JsonTokenType.Null:
processedObject = null;
break;
case JsonTokenType.False:
processedObject = false;
break;
case JsonTokenType.True:
processedObject = true;
break;
case JsonTokenType.String:
processedObject = reader.GetString();
break;
case JsonTokenType.Number:
{
if (reader.TryGetInt64(out long longValue))
{
processedObject = longValue;
}
else if (reader.TryGetDouble(out double doubleValue))
{
processedObject = doubleValue;
}
else
{
throw new JsonException($"Unrecognized '{JsonElement.ParseValue(ref reader)}' token as a number value.");
}

break;
}

default:
throw new JsonException($"Unrecognized '{reader.TokenType}' token type while parsing command response.");
}

return processedObject;
Expand Down
10 changes: 9 additions & 1 deletion dotnet/src/webdriver/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace OpenQA.Selenium
{
Expand All @@ -31,7 +32,8 @@ public class Response
{
private readonly static JsonSerializerOptions s_jsonSerializerOptions = new()
{
Converters = { new ResponseValueJsonConverter() }
TypeInfoResolver = ResponseJsonSerializerContext.Default,
Converters = { new ResponseValueJsonConverter() } // we still need it to make `Object` as `Dictionary`
};

private object responseValue;
Expand Down Expand Up @@ -208,4 +210,10 @@ public override string ToString()
return string.Format(CultureInfo.InvariantCulture, "({0} {1}: {2})", this.SessionId, this.Status, this.Value);
}
}

[JsonSerializable(typeof(Dictionary<string, object>))]
internal partial class ResponseJsonSerializerContext : JsonSerializerContext
{

}
}

0 comments on commit 1cb48a2

Please sign in to comment.