Skip to content

Commit 72c2cb6

Browse files
Merge pull request #117 from SixLabors/js/injectable-culture-converters
Make converters injectable and respect culture
2 parents b4b3fd5 + d0b2e10 commit 72c2cb6

33 files changed

+918
-423
lines changed

src/ImageSharp.Web/Commands/CommandDescriptor.cs

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/ImageSharp.Web/Commands/CommandParser.cs

Lines changed: 38 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Globalization;
7+
using System.Linq;
78
using System.Net;
9+
using System.Runtime.CompilerServices;
810
using SixLabors.ImageSharp.Web.Commands.Converters;
911

1012
namespace SixLabors.ImageSharp.Web.Commands
@@ -14,44 +16,18 @@ namespace SixLabors.ImageSharp.Web.Commands
1416
/// </summary>
1517
public sealed class CommandParser
1618
{
17-
/// <summary>
18-
/// A new instance of the <see cref="CommandParser"/> class with lazy initialization.
19-
/// </summary>
20-
private static readonly Lazy<CommandParser> Lazy = new Lazy<CommandParser>(() => new CommandParser());
19+
private readonly ICommandConverter[] converters;
2120

2221
/// <summary>
23-
/// Prevents a default instance of the <see cref="CommandParser"/> class from being created.
22+
/// Initializes a new instance of the <see cref="CommandParser"/> class.
2423
/// </summary>
25-
private CommandParser()
24+
/// <param name="converters">The collection of command converters.</param>
25+
public CommandParser(IEnumerable<ICommandConverter> converters)
2626
{
27-
this.AddSimpleConverters();
28-
this.AddListConverters();
29-
this.AddArrayConverters();
30-
this.AddColorConverters();
27+
Guard.NotNull(converters, nameof(converters));
28+
this.converters = converters.ToArray();
3129
}
3230

33-
/// <summary>
34-
/// Gets the current <see cref="CommandParser"/> instance.
35-
/// </summary>
36-
public static CommandParser Instance => Lazy.Value;
37-
38-
/// <summary>
39-
/// Adds a command converter to the parser.
40-
/// </summary>
41-
/// <param name="type">The <see cref="Type"/> to add a converter for. </param>
42-
/// <param name="converterType">The type of <see cref="CommandConverter"/> to add.</param>
43-
public void AddConverter(Type type, Type converterType) => CommandDescriptor.AddConverter(type, converterType);
44-
45-
/// <summary>
46-
/// Parses the given string value converting it to the given using the invariant culture.
47-
/// </summary>
48-
/// <param name="value">The string value to parse.</param>
49-
/// <typeparam name="T">
50-
/// The <see cref="Type"/> to convert the string to.
51-
/// </typeparam>
52-
/// <returns>The converted instance or the default.</returns>
53-
public T ParseValue<T>(string value) => this.ParseValue<T>(value, CultureInfo.InvariantCulture);
54-
5531
/// <summary>
5632
/// Parses the given string value converting it to the given type.
5733
/// </summary>
@@ -61,99 +37,46 @@ private CommandParser()
6137
/// The <see cref="Type"/> to convert the string to.
6238
/// </typeparam>
6339
/// <returns>The converted instance or the default.</returns>
40+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6441
public T ParseValue<T>(string value, CultureInfo culture)
6542
{
66-
culture ??= CultureInfo.InvariantCulture;
43+
DebugGuard.NotNull(culture, nameof(culture));
6744

6845
Type type = typeof(T);
69-
ICommandConverter converter = CommandDescriptor.GetConverter(type);
70-
try
46+
ICommandConverter converter = Array.Find(this.converters, x => x.Type.Equals(type));
47+
48+
if (converter != null)
7149
{
72-
return (T)converter.ConvertFrom(culture, WebUtility.UrlDecode(value), type);
50+
return ((ICommandConverter<T>)converter).ConvertFrom(
51+
this,
52+
culture,
53+
WebUtility.UrlDecode(value),
54+
type);
7355
}
74-
catch
56+
57+
// This special case allows us to reuse the same converter for infinite enum types
58+
// if one has not already been configured.
59+
if (type.IsEnum)
7560
{
76-
return default;
61+
converter = Array.Find(this.converters, x => x.Type.Equals(typeof(Enum)));
62+
if (converter != null)
63+
{
64+
return (T)((ICommandConverter<object>)converter).ConvertFrom(
65+
this,
66+
culture,
67+
WebUtility.UrlDecode(value),
68+
type);
69+
}
7770
}
78-
}
79-
80-
/// <summary>
81-
/// Add the generic converters.
82-
/// </summary>
83-
private void AddSimpleConverters()
84-
{
85-
this.AddConverter(TypeConstants.Sbyte, typeof(IntegralNumberConverter<sbyte>));
86-
this.AddConverter(TypeConstants.Byte, typeof(IntegralNumberConverter<byte>));
87-
88-
this.AddConverter(TypeConstants.Short, typeof(IntegralNumberConverter<short>));
89-
this.AddConverter(TypeConstants.UShort, typeof(IntegralNumberConverter<ushort>));
90-
91-
this.AddConverter(TypeConstants.Int, typeof(IntegralNumberConverter<int>));
92-
this.AddConverter(TypeConstants.UInt, typeof(IntegralNumberConverter<uint>));
9371

94-
this.AddConverter(TypeConstants.Long, typeof(IntegralNumberConverter<long>));
95-
this.AddConverter(TypeConstants.ULong, typeof(IntegralNumberConverter<ulong>));
96-
97-
this.AddConverter(typeof(decimal), typeof(SimpleCommandConverter<decimal>));
98-
this.AddConverter(typeof(float), typeof(SimpleCommandConverter<float>));
99-
100-
this.AddConverter(typeof(double), typeof(SimpleCommandConverter<double>));
101-
this.AddConverter(typeof(string), typeof(SimpleCommandConverter<string>));
102-
103-
this.AddConverter(typeof(bool), typeof(SimpleCommandConverter<bool>));
104-
}
105-
106-
/// <summary>
107-
/// Adds a selection of default list type converters.
108-
/// </summary>
109-
private void AddListConverters()
110-
{
111-
this.AddConverter(typeof(List<sbyte>), typeof(ListConverter<sbyte>));
112-
this.AddConverter(typeof(List<byte>), typeof(ListConverter<byte>));
113-
114-
this.AddConverter(typeof(List<short>), typeof(ListConverter<short>));
115-
this.AddConverter(typeof(List<ushort>), typeof(ListConverter<ushort>));
116-
117-
this.AddConverter(typeof(List<int>), typeof(ListConverter<int>));
118-
this.AddConverter(typeof(List<uint>), typeof(ListConverter<uint>));
119-
120-
this.AddConverter(typeof(List<long>), typeof(ListConverter<long>));
121-
this.AddConverter(typeof(List<ulong>), typeof(ListConverter<ulong>));
122-
123-
this.AddConverter(typeof(List<decimal>), typeof(ListConverter<decimal>));
124-
this.AddConverter(typeof(List<float>), typeof(ListConverter<float>));
125-
this.AddConverter(typeof(List<double>), typeof(ListConverter<double>));
126-
127-
this.AddConverter(typeof(List<string>), typeof(ListConverter<string>));
72+
// We don't actually return here.
73+
// The compiler just cannot see our exception.
74+
ThrowNotSupported(type);
75+
return default;
12876
}
12977

130-
/// <summary>
131-
/// Adds a selection of default array type converters.
132-
/// </summary>
133-
private void AddArrayConverters()
134-
{
135-
this.AddConverter(typeof(sbyte[]), typeof(ArrayConverter<sbyte>));
136-
this.AddConverter(typeof(byte[]), typeof(ArrayConverter<byte>));
137-
138-
this.AddConverter(typeof(short[]), typeof(ArrayConverter<short>));
139-
this.AddConverter(typeof(ushort[]), typeof(ArrayConverter<ushort>));
140-
141-
this.AddConverter(typeof(int[]), typeof(ArrayConverter<int>));
142-
this.AddConverter(typeof(uint[]), typeof(ArrayConverter<uint>));
143-
144-
this.AddConverter(typeof(long[]), typeof(ArrayConverter<long>));
145-
this.AddConverter(typeof(ulong[]), typeof(ArrayConverter<ulong>));
146-
147-
this.AddConverter(typeof(decimal[]), typeof(ArrayConverter<decimal>));
148-
this.AddConverter(typeof(float[]), typeof(ArrayConverter<float>));
149-
this.AddConverter(typeof(double[]), typeof(ArrayConverter<double>));
150-
151-
this.AddConverter(typeof(string[]), typeof(ArrayConverter<string>));
152-
}
153-
154-
/// <summary>
155-
/// Adds the default color converters.
156-
/// </summary>
157-
private void AddColorConverters() => this.AddConverter(TypeConstants.Color, typeof(ColorConverter));
78+
[MethodImpl(MethodImplOptions.NoInlining)]
79+
private static void ThrowNotSupported(Type type)
80+
=> throw new NotSupportedException($"Cannot convert to {type.FullName}.");
15881
}
15982
}

src/ImageSharp.Web/Commands/Converters/ArrayConverter{T}.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,51 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Globalization;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
79

810
namespace SixLabors.ImageSharp.Web.Commands.Converters
911
{
1012
/// <summary>
11-
/// Converts the value of an string to a generic array.
13+
/// Converts the value of a string to a generic array.
1214
/// </summary>
1315
/// <typeparam name="T">The parameter type to convert to.</typeparam>
14-
internal sealed class ArrayConverter<T> : ListConverter<T>
16+
internal sealed class ArrayConverter<T> : ICommandConverter<T[]>
1517
{
1618
/// <inheritdoc/>
17-
public override object ConvertFrom(CultureInfo culture, string value, Type propertyType)
18-
=> ((List<T>)base.ConvertFrom(culture, value, propertyType)).ToArray();
19+
public Type Type => typeof(T[]);
20+
21+
/// <inheritdoc/>
22+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
23+
public T[] ConvertFrom(
24+
CommandParser parser,
25+
CultureInfo culture,
26+
string value,
27+
Type propertyType)
28+
{
29+
if (string.IsNullOrWhiteSpace(value))
30+
{
31+
return Array.Empty<T>();
32+
}
33+
34+
var result = new List<T>();
35+
foreach (string pill in GetStringArray(value, culture))
36+
{
37+
T item = parser.ParseValue<T>(pill, culture);
38+
if (item != null)
39+
{
40+
result.Add(item);
41+
}
42+
}
43+
44+
return result.ToArray();
45+
}
46+
47+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
48+
private static string[] GetStringArray(string input, CultureInfo culture)
49+
{
50+
char separator = culture.TextInfo.ListSeparator[0];
51+
return input.Split(separator).Select(s => s.Trim()).ToArray();
52+
}
1953
}
2054
}

src/ImageSharp.Web/Commands/Converters/ColorConverter.cs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
using System.Collections.Generic;
66
using System.Globalization;
77
using System.Reflection;
8+
using System.Runtime.CompilerServices;
89
using System.Text.RegularExpressions;
910

1011
namespace SixLabors.ImageSharp.Web.Commands.Converters
1112
{
1213
/// <summary>
1314
/// Allows the conversion of strings into rgba32 pixel colors.
1415
/// </summary>
15-
internal class ColorConverter : CommandConverter
16+
internal class ColorConverter : ICommandConverter<Color>
1617
{
1718
/// <summary>
1819
/// The web color hexadecimal regex. Matches strings arranged
@@ -31,17 +32,21 @@ internal class ColorConverter : CommandConverter
3132
private static readonly Lazy<IDictionary<string, Color>> ColorConstantsTable = new Lazy<IDictionary<string, Color>>(InitializeColorConstantsTable);
3233

3334
/// <inheritdoc/>
34-
public override object ConvertFrom(CultureInfo culture, string value, Type propertyType)
35+
public Type Type => typeof(Color);
36+
37+
/// <inheritdoc/>
38+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
39+
public Color ConvertFrom(CommandParser parser, CultureInfo culture, string value, Type propertyType)
3540
{
3641
if (string.IsNullOrWhiteSpace(value))
3742
{
38-
return default(Color);
43+
return default;
3944
}
4045

4146
// Numeric r,g,b - r,g,b,a
4247
char separator = culture.TextInfo.ListSeparator[0];
4348

44-
if (value.Contains(separator.ToString()))
49+
if (value.IndexOf(separator) != -1)
4550
{
4651
string[] components = value.Split(separator);
4752

@@ -56,11 +61,14 @@ public override object ConvertFrom(CultureInfo culture, string value, Type prope
5661

5762
if (convert)
5863
{
59-
List<byte> rgba = CommandParser.Instance.ParseValue<List<byte>>(value);
64+
List<byte> rgba = parser.ParseValue<List<byte>>(value, culture);
6065

61-
return rgba.Count == 4
62-
? Color.FromRgba(rgba[0], rgba[1], rgba[2], rgba[3])
63-
: Color.FromRgb(rgba[0], rgba[1], rgba[2]);
66+
return rgba.Count switch
67+
{
68+
4 => Color.FromRgba(rgba[0], rgba[1], rgba[2], rgba[3]),
69+
3 => Color.FromRgb(rgba[0], rgba[1], rgba[2]),
70+
_ => default,
71+
};
6472
}
6573
}
6674

@@ -72,19 +80,16 @@ public override object ConvertFrom(CultureInfo culture, string value, Type prope
7280

7381
// Named colors
7482
IDictionary<string, Color> table = ColorConstantsTable.Value;
75-
return table.ContainsKey(value) ? table[value] : base.ConvertFrom(culture, value, propertyType);
83+
return table.ContainsKey(value) ? table[value] : default;
7684
}
7785

78-
/// <summary>
79-
/// Initializes color table mapping color constants.
80-
/// </summary>
81-
/// <returns>The <see cref="IDictionary{String, Color}"/>.</returns>
8286
private static IDictionary<string, Color> InitializeColorConstantsTable()
8387
{
8488
IDictionary<string, Color> table = new Dictionary<string, Color>(StringComparer.OrdinalIgnoreCase);
85-
foreach (FieldInfo field in TypeConstants.Color.GetFields(BindingFlags.Public | BindingFlags.Static))
89+
90+
foreach (FieldInfo field in typeof(Color).GetFields(BindingFlags.Public | BindingFlags.Static))
8691
{
87-
if (field.FieldType == TypeConstants.Color)
92+
if (field.FieldType == typeof(Color))
8893
{
8994
table[field.Name] = (Color)field.GetValue(null);
9095
}

0 commit comments

Comments
 (0)