Skip to content

Commit 0ae490b

Browse files
committed
Refactor Offset func source-gen and base64url encoding
1 parent 7b86d81 commit 0ae490b

File tree

7 files changed

+166
-131
lines changed

7 files changed

+166
-131
lines changed

samples/Jameak.CursorPagination.Sample/GeneratedFiles/Jameak.CursorPagination.SourceGenerator/Jameak.CursorPagination.SourceGenerator.PaginationGenerator/OffsetPaginationStrategy_O.g.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,14 @@ public partial class OffsetPaginationStrategy : global::Jameak.CursorPagination.
4545
global::Jameak.CursorPagination.Abstractions.Enums.PaginationDirection paginationDirection,
4646
global::Jameak.CursorPagination.Abstractions.OffsetPagination.OffsetCursor? cursor)
4747
{
48-
global::Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.ThrowIfPageSizeInvalid(pageSize, checkHasNextPage);
49-
5048
global::System.Func<global::System.Linq.IQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>, global::System.Linq.IOrderedQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>> orderFunc =
5149
queryable => PrivateHelper.ApplyOrderBy(queryable, paginationDirection);
52-
53-
global::System.Func<global::System.Linq.IQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>, global::System.Linq.IQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>> skipFunc;
54-
if(cursor != null)
55-
{
56-
var skipValue = cursor.Skip;
57-
skipFunc = queryable => global::System.Linq.Queryable.Skip(queryable, skipValue);
58-
}
59-
else
60-
{
61-
skipFunc = queryable => queryable;
62-
}
63-
64-
var toTake = pageSize;
65-
if(checkHasNextPage)
66-
{
67-
toTake = PrivateHelper.ComputeToTake(pageSize);
68-
}
69-
70-
global::System.Func<global::System.Linq.IQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>, global::System.Linq.IQueryable<global::Jameak.CursorPagination.Sample.ResponseModels.DtoTypeToPaginate>> takeFunc =
71-
queryable => global::System.Linq.Queryable.Take(queryable, toTake);
72-
73-
return (orderFunc, skipFunc, takeFunc);
50+
51+
return Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.OffsetBuildPaginationMethods(
52+
pageSize,
53+
checkHasNextPage,
54+
orderFunc,
55+
cursor);
7456
}
7557

7658
/// <inheritdoc />

src/Jameak.CursorPagination.Abstractions/Internal/InternalProcessingHelper.cs

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,48 @@ private static void HandleNextPageCheckInPlace<T>(
144144
}
145145
}
146146

147-
private static readonly char[] s_base64Padding = ['='];
148-
149147
/// <summary>
150148
/// This is an internal API that supports the library infrastructure
151149
/// and not subject to the same compatibility standards as public APIs.
152150
/// It may be changed or removed without notice in any release.
153151
/// </summary>
154152
public static string UrlSafeBase64Encode(string toEncode)
155153
{
156-
return Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode)).TrimEnd(s_base64Padding).Replace('+', '-').Replace('/', '_');
154+
// Based on https://github.com/dotnet/aspnetcore/blob/main/src/Shared/WebEncoders/WebEncoders.cs which is MIT licensed.
155+
// Reworked for .NET Standard 2.0
156+
var bytesToEncode = Encoding.UTF8.GetBytes(toEncode);
157+
var outputBuffer = new char[GetArraySizeRequiredToEncode(bytesToEncode.Length)];
158+
var numBase64Chars = Convert.ToBase64CharArray(bytesToEncode, 0, bytesToEncode.Length, outputBuffer, 0);
159+
160+
for (var i = 0; i < numBase64Chars; i++)
161+
{
162+
var ch = outputBuffer[i];
163+
switch (ch)
164+
{
165+
case '+':
166+
outputBuffer[i] = '-';
167+
break;
168+
case '/':
169+
outputBuffer[i] = '_';
170+
break;
171+
case '=':
172+
// We've reached a padding character; truncate the remainder.
173+
return CreateString(outputBuffer, i);
174+
}
175+
}
176+
177+
return CreateString(outputBuffer, numBase64Chars);
178+
179+
static string CreateString(char[] outputBuffer, int length)
180+
{
181+
return new string(outputBuffer, startIndex: 0, length: length);
182+
}
183+
184+
static int GetArraySizeRequiredToEncode(int count)
185+
{
186+
var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
187+
return checked(numWholeOrPartialInputBlocks * 4);
188+
}
157189
}
158190

159191
/// <summary>
@@ -163,15 +195,83 @@ public static string UrlSafeBase64Encode(string toEncode)
163195
/// </summary>
164196
public static string UrlSafeBase64Decode(string toDecode)
165197
{
166-
// Copied from https://stackoverflow.com/a/26354677
167-
// License - CC BY-SA 3.0
168-
var incoming = toDecode.Replace('_', '/').Replace('-', '+');
169-
switch (toDecode.Length % 4)
198+
// Based on https://github.com/dotnet/aspnetcore/blob/main/src/Shared/WebEncoders/WebEncoders.cs which is MIT licensed.
199+
// Reworked for .NET Standard 2.0
200+
var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(toDecode.Length);
201+
var buffer = new char[checked(toDecode.Length + paddingCharsToAdd)];
202+
203+
var i = 0;
204+
for (var j = 0; i < toDecode.Length; i++, j++)
205+
{
206+
var ch = toDecode[j];
207+
switch (ch)
208+
{
209+
case '-':
210+
buffer[i] = '+';
211+
break;
212+
case '_':
213+
buffer[i] = '/';
214+
break;
215+
default:
216+
buffer[i] = ch;
217+
break;
218+
}
219+
}
220+
221+
for (; i < toDecode.Length + paddingCharsToAdd; i++)
222+
{
223+
buffer[i] = '=';
224+
}
225+
226+
return Encoding.UTF8.GetString(Convert.FromBase64CharArray(buffer, 0, buffer.Length));
227+
228+
static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
229+
{
230+
return (inputLength % 4) switch
231+
{
232+
2 => 2,
233+
3 => 1,
234+
_ => 0,
235+
};
236+
}
237+
}
238+
239+
/// <summary>
240+
/// This is an internal API that supports the library infrastructure
241+
/// and not subject to the same compatibility standards as public APIs.
242+
/// It may be changed or removed without notice in any release.
243+
/// </summary>
244+
public static (
245+
Func<IQueryable<T>, IOrderedQueryable<T>> applyOrderExpr,
246+
Func<IQueryable<T>, IQueryable<T>> applySkip,
247+
Func<IQueryable<T>, IQueryable<T>> applyTake)
248+
OffsetBuildPaginationMethods<T>(
249+
int pageSize,
250+
bool checkHasNextPage,
251+
Func<IQueryable<T>, IOrderedQueryable<T>> orderFunc,
252+
OffsetCursor? cursor)
253+
{
254+
ThrowIfPageSizeInvalid(pageSize, checkHasNextPage);
255+
256+
Func<IQueryable<T>, IQueryable<T>> skipFunc;
257+
if (cursor != null)
170258
{
171-
case 2: incoming += "=="; break;
172-
case 3: incoming += "="; break;
259+
var skipValue = cursor.Skip;
260+
skipFunc = queryable => queryable.Skip(skipValue);
173261
}
174-
var bytes = Convert.FromBase64String(incoming);
175-
return Encoding.ASCII.GetString(bytes);
262+
else
263+
{
264+
skipFunc = queryable => queryable;
265+
}
266+
267+
var toTake = pageSize;
268+
if (checkHasNextPage)
269+
{
270+
toTake = ComputeToTake(pageSize);
271+
}
272+
273+
IQueryable<T> TakeFunc(IQueryable<T> queryable) => queryable.Take(toTake);
274+
275+
return (orderFunc, skipFunc, TakeFunc);
176276
}
177277
}

src/Jameak.CursorPagination.SourceGenerator/OffsetPaginationClassBuilder.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,32 +64,14 @@ internal class OffsetPaginationClassBuilder
6464
global::{{typeof(PaginationDirection).FullName}} paginationDirection,
6565
global::{{typeof(OffsetCursor).FullName}}? cursor)
6666
{
67-
global::{{typeof(InternalProcessingHelper).FullName}}.{{nameof(InternalProcessingHelper.ThrowIfPageSizeInvalid)}}(pageSize, checkHasNextPage);
68-
6967
global::System.Func<global::System.Linq.IQueryable<{{TargetClassTypePlaceholder}}>, global::System.Linq.IOrderedQueryable<{{TargetClassTypePlaceholder}}>> orderFunc =
7068
queryable => {{PrivateHelperClassName}}.ApplyOrderBy(queryable, paginationDirection);
71-
72-
global::System.Func<global::System.Linq.IQueryable<{{TargetClassTypePlaceholder}}>, global::System.Linq.IQueryable<{{TargetClassTypePlaceholder}}>> skipFunc;
73-
if(cursor != null)
74-
{
75-
var skipValue = cursor.{{nameof(OffsetCursor.Skip)}};
76-
skipFunc = queryable => global::System.Linq.Queryable.Skip(queryable, skipValue);
77-
}
78-
else
79-
{
80-
skipFunc = queryable => queryable;
81-
}
82-
83-
var toTake = pageSize;
84-
if(checkHasNextPage)
85-
{
86-
toTake = {{PrivateHelperClassName}}.ComputeToTake(pageSize);
87-
}
88-
89-
global::System.Func<global::System.Linq.IQueryable<{{TargetClassTypePlaceholder}}>, global::System.Linq.IQueryable<{{TargetClassTypePlaceholder}}>> takeFunc =
90-
queryable => global::System.Linq.Queryable.Take(queryable, toTake);
91-
92-
return (orderFunc, skipFunc, takeFunc);
69+
70+
return {{typeof(InternalProcessingHelper).FullName}}.{{nameof(InternalProcessingHelper.OffsetBuildPaginationMethods)}}(
71+
pageSize,
72+
checkHasNextPage,
73+
orderFunc,
74+
cursor);
9375
}
9476
9577
/// <inheritdoc />

test/Jameak.CursorPagination.SourceGenerator.Tests/TestHelper.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections;
22
using System.Collections.Immutable;
33
using System.Diagnostics.CodeAnalysis;
4+
using System.Globalization;
45
using System.Reflection;
56
using System.Runtime.CompilerServices;
67
using Basic.Reference.Assemblies;
@@ -39,6 +40,9 @@ public static async Task Verify(IEnumerable<string> sourceText, [CallerFilePath]
3940
MetadataReference.CreateFromFile(typeof(IKeySetCursor).Assembly.Location)
4041
];
4142

43+
// Compilation errors are localized, so to ensure snapshot reproducibility we force a consistent culture.
44+
using var cultureScope = new ChangeCultureScope("en-US");
45+
4246
var compileOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
4347
.WithNullableContextOptions(NullableContextOptions.Enable)
4448
.WithSpecificDiagnosticOptions(s_analyzers.SelectMany(e => e.SupportedDiagnostics).Select(diag => new KeyValuePair<string, ReportDiagnostic>(diag.Id, GetReportDiagnostic(diag))));
@@ -206,4 +210,25 @@ void Visit(object? node)
206210
}
207211
}
208212
}
213+
214+
private class ChangeCultureScope : IDisposable
215+
{
216+
private readonly CultureInfo _originalCurrentCulture;
217+
private readonly CultureInfo _originalCurrentUiCulture;
218+
219+
public ChangeCultureScope(string cultureName)
220+
{
221+
_originalCurrentCulture = CultureInfo.CurrentCulture;
222+
_originalCurrentUiCulture = CultureInfo.CurrentUICulture;
223+
var culture = CultureInfo.GetCultureInfo(cultureName);
224+
CultureInfo.CurrentCulture = culture;
225+
CultureInfo.CurrentUICulture = culture;
226+
}
227+
228+
public void Dispose()
229+
{
230+
CultureInfo.CurrentCulture = _originalCurrentCulture;
231+
CultureInfo.CurrentUICulture = _originalCurrentUiCulture;
232+
}
233+
}
209234
}

test/Jameak.CursorPagination.SourceGenerator.Tests/__snapshots__/OffsetSpecificTests.VerifyOffsetGeneration_AllAscending#TestOffsetPaginationStrategy_O.g.verified.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,14 @@ public partial class TestOffsetPaginationStrategy : global::Jameak.CursorPaginat
4646
global::Jameak.CursorPagination.Abstractions.Enums.PaginationDirection paginationDirection,
4747
global::Jameak.CursorPagination.Abstractions.OffsetPagination.OffsetCursor? cursor)
4848
{
49-
global::Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.ThrowIfPageSizeInvalid(pageSize, checkHasNextPage);
50-
5149
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IOrderedQueryable<global::TestNamespace.TestInput>> orderFunc =
5250
queryable => PrivateHelper.ApplyOrderBy(queryable, paginationDirection);
53-
54-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> skipFunc;
55-
if(cursor != null)
56-
{
57-
var skipValue = cursor.Skip;
58-
skipFunc = queryable => global::System.Linq.Queryable.Skip(queryable, skipValue);
59-
}
60-
else
61-
{
62-
skipFunc = queryable => queryable;
63-
}
64-
65-
var toTake = pageSize;
66-
if(checkHasNextPage)
67-
{
68-
toTake = PrivateHelper.ComputeToTake(pageSize);
69-
}
70-
71-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> takeFunc =
72-
queryable => global::System.Linq.Queryable.Take(queryable, toTake);
73-
74-
return (orderFunc, skipFunc, takeFunc);
51+
52+
return Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.OffsetBuildPaginationMethods(
53+
pageSize,
54+
checkHasNextPage,
55+
orderFunc,
56+
cursor);
7557
}
7658

7759
/// <inheritdoc />

test/Jameak.CursorPagination.SourceGenerator.Tests/__snapshots__/OffsetSpecificTests.VerifyOffsetGeneration_MixedOrdering#TestOffsetPaginationStrategy_O.g.verified.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,14 @@ public partial class TestOffsetPaginationStrategy : global::Jameak.CursorPaginat
4646
global::Jameak.CursorPagination.Abstractions.Enums.PaginationDirection paginationDirection,
4747
global::Jameak.CursorPagination.Abstractions.OffsetPagination.OffsetCursor? cursor)
4848
{
49-
global::Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.ThrowIfPageSizeInvalid(pageSize, checkHasNextPage);
50-
5149
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IOrderedQueryable<global::TestNamespace.TestInput>> orderFunc =
5250
queryable => PrivateHelper.ApplyOrderBy(queryable, paginationDirection);
53-
54-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> skipFunc;
55-
if(cursor != null)
56-
{
57-
var skipValue = cursor.Skip;
58-
skipFunc = queryable => global::System.Linq.Queryable.Skip(queryable, skipValue);
59-
}
60-
else
61-
{
62-
skipFunc = queryable => queryable;
63-
}
64-
65-
var toTake = pageSize;
66-
if(checkHasNextPage)
67-
{
68-
toTake = PrivateHelper.ComputeToTake(pageSize);
69-
}
70-
71-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> takeFunc =
72-
queryable => global::System.Linq.Queryable.Take(queryable, toTake);
73-
74-
return (orderFunc, skipFunc, takeFunc);
51+
52+
return Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.OffsetBuildPaginationMethods(
53+
pageSize,
54+
checkHasNextPage,
55+
orderFunc,
56+
cursor);
7557
}
7658

7759
/// <inheritdoc />

test/Jameak.CursorPagination.SourceGenerator.Tests/__snapshots__/OffsetSpecificTests.VerifyOffsetGeneration_MixedOrderingAndFlippedOrderInteger#TestOffsetPaginationStrategy_O.g.verified.cs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,14 @@ public partial class TestOffsetPaginationStrategy : global::Jameak.CursorPaginat
4646
global::Jameak.CursorPagination.Abstractions.Enums.PaginationDirection paginationDirection,
4747
global::Jameak.CursorPagination.Abstractions.OffsetPagination.OffsetCursor? cursor)
4848
{
49-
global::Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.ThrowIfPageSizeInvalid(pageSize, checkHasNextPage);
50-
5149
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IOrderedQueryable<global::TestNamespace.TestInput>> orderFunc =
5250
queryable => PrivateHelper.ApplyOrderBy(queryable, paginationDirection);
53-
54-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> skipFunc;
55-
if(cursor != null)
56-
{
57-
var skipValue = cursor.Skip;
58-
skipFunc = queryable => global::System.Linq.Queryable.Skip(queryable, skipValue);
59-
}
60-
else
61-
{
62-
skipFunc = queryable => queryable;
63-
}
64-
65-
var toTake = pageSize;
66-
if(checkHasNextPage)
67-
{
68-
toTake = PrivateHelper.ComputeToTake(pageSize);
69-
}
70-
71-
global::System.Func<global::System.Linq.IQueryable<global::TestNamespace.TestInput>, global::System.Linq.IQueryable<global::TestNamespace.TestInput>> takeFunc =
72-
queryable => global::System.Linq.Queryable.Take(queryable, toTake);
73-
74-
return (orderFunc, skipFunc, takeFunc);
51+
52+
return Jameak.CursorPagination.Abstractions.Internal.InternalProcessingHelper.OffsetBuildPaginationMethods(
53+
pageSize,
54+
checkHasNextPage,
55+
orderFunc,
56+
cursor);
7557
}
7658

7759
/// <inheritdoc />

0 commit comments

Comments
 (0)