Skip to content

Commit

Permalink
Add .NET Standard 2.0 and .NET 6 targets (#8)
Browse files Browse the repository at this point in the history
Resolves #7
  • Loading branch information
RichardD2 authored Aug 18, 2023
1 parent 8a5aa6f commit e6d0818
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: "7.x" # TODO: Specify multiple .NET versions once we add support for other target frameworks
dotnet-version: 7.0.x

- name: Generate the package
# NOTE: As for the `${GITHUB_REF_NAME#v}` bit below, see https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables:~:text=branch%2D1.-,GITHUB_REF_NAME,-The%20short%20ref. We could've also done https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable but since we're using this value in a single place, we don't need to make it an environment variable.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: windows-latest # NOTE: Has to be Windows because the old .NET Framework 4.x (which is one of the target frameworks of the test project) is Windows-only

steps:
- name: Checkout the repository
Expand All @@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: "7.x" # TODO: Specify multiple .NET versions once we add support for other target frameworks
dotnet-version: 7.0.x # NOTE: The `windows-latest` container already comes pre-installed with the .NET versions that we need, but we do need to use `setup-dotnet` (and pass it one of the versions — e.g. 7.0.x), so that the relevant binaries (e.g. `dotnet`) are actually added to PATH so we can use them. See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#preinstalled-software and https://github.com/actions/setup-dotnet

# TODO: Some .NET codebases with GitHub Actions workflows seem to run `dotnet restore`, `dotnet build` and `dotnet test` as separate steps. Why?
- name: Run the tests
Expand Down
46 changes: 46 additions & 0 deletions src/Sqids/PolyfillExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#if NETSTANDARD2_0

using System.Runtime.InteropServices;

namespace Sqids;

internal static class PolyfillExtensions
{
public static StringBuilder Append(this StringBuilder builder, ReadOnlySpan<char> value)
{
if (builder is null) throw new ArgumentNullException(nameof(builder));

if (!value.IsEmpty)
{
unsafe
{
fixed (char* p = &MemoryMarshal.GetReference(value))
{
builder.Append(p, value.Length);
}
}
}

return builder;
}

public static StringBuilder Insert(this StringBuilder builder, int index, ReadOnlySpan<char> value)
{
if (builder is null) throw new ArgumentNullException(nameof(builder));

if (!value.IsEmpty)
{
// NOTE: Unfortunately, not much we can do here; the StringBuilder.Insert(int, char*, int) method is private.
char[] temp = new char[value.Length];
value.CopyTo(temp);
builder.Insert(index, temp);
}

return builder;
}

public static bool Contains(this Span<char> source, char toFind) =>
source.IndexOf(toFind) != -1;
}

#endif
17 changes: 15 additions & 2 deletions src/Sqids/Sqids.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>latest</LangVersion>
<TargetFrameworks>netstandard2.0;net6.0;net7.0</TargetFrameworks>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
Expand All @@ -25,9 +25,22 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\icon.png" Pack="true" PackagePath="" />
<None Include="..\..\nuget-readme.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageReference Include="PolySharp" Version="1.13.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>

</Project>
30 changes: 22 additions & 8 deletions src/Sqids/SqidsEncoder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Sqids;
namespace Sqids;

/// <summary>
/// The Sqids encoder/decoder. This is the main class.
Expand Down Expand Up @@ -179,7 +179,7 @@ private string Encode(ReadOnlySpan<int> numbers, bool partitioned = false)
}
}

if (IsBlockedId(result))
if (IsBlockedId(result.AsSpan()))
{
Span<int> newNumbers = numbers.Length * sizeof(int) > MaxStackallocSize
? new int[numbers.Length]
Expand Down Expand Up @@ -256,7 +256,7 @@ public int[] Decode(ReadOnlySpan<char> id)

var separatorIndex = id.IndexOf(separator);
var chunk = separatorIndex == -1 ? id : id[..separatorIndex]; // NOTE: The first part of `id` (every thing to the left of the separator) represents the number that we ought to decode.
id = separatorIndex == -1 ? string.Empty : id[(separatorIndex + 1)..]; // NOTE: Everything to the right of the separator will be `id` for the next iteration
id = separatorIndex == -1 ? default : id[(separatorIndex + 1)..]; // NOTE: Everything to the right of the separator will be `id` for the next iteration

if (chunk.IsEmpty)
continue;
Expand All @@ -277,6 +277,20 @@ public int[] Decode(ReadOnlySpan<char> id)
return result.ToArray(); // TODO: A way to return an array without creating a new array from the list like this?
}

// NOTE: Implicit `string` => `Span<char>` conversion was introduced in .NET Standard 2.1 (see https://learn.microsoft.com/en-us/dotnet/api/system.string.op_implicit), which means without this overload, calling `Decode` with a string on versions older than .NET Standard 2.1 would require calling `.AsSpan()` on the string, which is cringe.
#if NETSTANDARD2_0
/// <summary>
/// Decodes an ID into numbers.
/// </summary>
/// <param name="id">The encoded ID.</param>
/// <returns>
/// An array of integers containing the decoded number(s) (it would contain only one element
/// if the ID represents a single number); or an empty array if the input ID is null,
/// empty, or includes characters not found in the alphabet.
/// </returns>
public int[] Decode(string id) => Decode(id.AsSpan());
#endif

private bool IsBlockedId(ReadOnlySpan<char> id)
{
foreach (string word in _blockList)
Expand All @@ -285,15 +299,15 @@ private bool IsBlockedId(ReadOnlySpan<char> id)
continue;

if ((id.Length <= 3 || word.Length <= 3) &&
id.Equals(word, StringComparison.OrdinalIgnoreCase))
id.Equals(word.AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;

if (word.Any(char.IsDigit) &&
(id.StartsWith(word, StringComparison.OrdinalIgnoreCase) ||
id.EndsWith(word, StringComparison.OrdinalIgnoreCase)))
(id.StartsWith(word.AsSpan(), StringComparison.OrdinalIgnoreCase) ||
id.EndsWith(word.AsSpan(), StringComparison.OrdinalIgnoreCase)))
return true;

if (id.Contains(word, StringComparison.OrdinalIgnoreCase))
if (id.Contains(word.AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;
}

Expand Down Expand Up @@ -321,7 +335,7 @@ private static ReadOnlySpan<char> ToId(int num, ReadOnlySpan<char> alphabet)
result = result / alphabet.Length;
} while (result > 0);

return id.ToString(); // TODO: possibly avoid creating a string
return id.ToString().AsSpan(); // TODO: possibly avoid creating a string
}

private static int ToNumber(ReadOnlySpan<char> id, ReadOnlySpan<char> alphabet)
Expand Down
43 changes: 26 additions & 17 deletions test/Sqids.Tests/Sqids.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<!-- NOTE: `net472` is used for testing `netstandard2.0` -->
<TargetFrameworks>net472;net6.0;net7.0</TargetFrameworks>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Sqids\Sqids.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sqids\Sqids.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="PolySharp" Version="1.13.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>

0 comments on commit e6d0818

Please sign in to comment.