Skip to content

Commit

Permalink
A few bits to push...
Browse files Browse the repository at this point in the history
  • Loading branch information
IEvangelist committed Jun 12, 2024
1 parent 3544eb8 commit e4b0149
Show file tree
Hide file tree
Showing 24 changed files with 373 additions and 17 deletions.
7 changes: 4 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="6.0.2" PrivateAssets="all" IncludeAssets="runtime;build;native;contentfiles;analyzers;buildtransitive" />
<PackageVersion Include="GitHub.Actions.Core" Version="8.0.15" />
<PackageVersion Include="GitHub.Actions.Octokit" Version="8.0.15" />
<PackageVersion Include="GitHub.Actions.Core" Version="8.0.16" />
<PackageVersion Include="GitHub.Actions.Octokit" Version="8.0.16" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" IncludeAssets="runtime;build;native;contentfiles;analyzers;buildtransitive" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Build.Containers" Version="8.0.301" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="MSTest.Engine" Version="1.0.0-alpha.24271.6" />
<PackageVersion Include="MSTest.SourceGeneration" Version="1.0.0-alpha.24271.6" />
<PackageVersion Include="MSTest.TestFramework" Version="3.4.3" PrivateAssets="all" IncludeAssets="runtime;build;native;contentfiles;analyzers;buildtransitive" />
<PackageVersion Include="MSTest.Analyzers" Version="3.4.3" PrivateAssets="all" IncludeAssets="runtime;build;native;contentfiles;analyzers;buildtransitive" />
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
<PackageVersion Include="Pathological.Globbing" Version="8.0.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.200"
"version": "8.0.301"
}
}
8 changes: 7 additions & 1 deletion profanity-filter.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34004.107
Expand Down Expand Up @@ -29,12 +28,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{28623E11-D15
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EAC63754-09D3-49C9-ACDF-ECF73AA1D922}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProfanityFilter.WebApi", "src\ProfanityFilter.WebApi\ProfanityFilter.WebApi.csproj", "{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C}.Release|Any CPU.Build.0 = Release|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -56,6 +61,7 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{163A89FA-5F97-4332-B5DA-3E7BDDA97E7C} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{6FBF4FCA-D1E5-4BE3-8BA5-48EF90D8E966} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{F2750329-F469-4BD8-A0D3-7E1CE2D62E19} = {28623E11-D15F-4448-82F6-B86A7D59B1D4}
{A1F889D2-290B-42C2-A61C-877C97F9D5EB} = {EAC63754-09D3-49C9-ACDF-ECF73AA1D922}
Expand Down
1 change: 0 additions & 1 deletion src/ProfanityFilter.Action/ProfanityFilter.Action.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="GitHub.Actions.Core" />
<PackageReference Include="GitHub.Actions.Octokit" />
<PackageReference Include="Microsoft.NET.Build.Containers" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/ProfanityFilter.Services/Data/ProfaneContentReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal sealed class ProfaneContentReader
EnsureWorkingDirectory();

var builder = new GlobOptionsBuilder()
.WithPattern("Data/*.txt");
.WithPattern("**/Data/*.txt");

return builder.Build();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ internal sealed class DefaultProfaneContentFilterService(IMemoryCache cache) : I
{
private const string ProfaneListKey = nameof(ProfaneListKey);

/// <summary>
/// Reads all profane words from their respective sources asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation that
/// returns a readonly dictionary of all profane words.</returns>
private async Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync()
/// <inheritdoc />
public async Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync()
{
return await cache.GetOrCreateAsync(ProfaneListKey, async entry =>
{
Expand Down Expand Up @@ -62,7 +58,7 @@ await Parallel.ForEachAsync(fileNames,
}

/// <inheritdoc />
async ValueTask<FilterResult> IProfaneContentFilterService.FilterProfanityAsync(
public async ValueTask<FilterResult> FilterProfanityAsync(
string content,
FilterParameters parameters)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public record class ProfaneSourceFilter(
/// Gets the string representation of the <see cref="ProfaneWords"/> set
/// represented as a regular expression pattern.
/// </summary>
[JsonIgnore]
public string RegexPattern { get; } =
$"\\b({string.Join('|', ProfaneWords)})\\b";
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public enum ReplacementStrategy
MiddleAsterisk,

/// <summary>
/// Represents the first letter then asterisk replacement strategy, which replaces the
/// Represents the first letter then asterisk replacement strategy, which replaces
/// everything after the first letter of the profanity with asterisk.
/// </summary>
FirstLetterThenAsterisk,
Expand Down
1 change: 1 addition & 0 deletions src/ProfanityFilter.Services/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
global using System.Diagnostics.CodeAnalysis;
global using System.Text;
global using System.Text.RegularExpressions;
global using System.Text.Json.Serialization;

global using Microsoft.Extensions.Caching.Memory;
global using Microsoft.Extensions.DependencyInjection;
Expand Down
7 changes: 7 additions & 0 deletions src/ProfanityFilter.Services/IProfaneContentFilterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ public interface IProfaneContentFilterService
ValueTask<FilterResult> FilterProfanityAsync(
string content,
FilterParameters parameters);

/// <summary>
/// Reads all profane words from their respective sources asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation that
/// returns a readonly dictionary of all profane words.</returns>
Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Endpoints;

internal static class ProfanityFilterEndpointExtensions
{
internal static WebApplication MapProfanityFilterEndpoints(this WebApplication app)
{
var profanity = app.MapGroup("profanity");

profanity.MapPost("filter", OnApplyFilterAsync)
.WithOpenApi()
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.WithSummary("""
Use this endpoint to attempt applying a profanity-filter. The response is returned as Markdown.
""")
.WithHttpLogging(HttpLoggingFields.All);

profanity.MapGet("strategies", OnGetStrategies)
.WithOpenApi()
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Returns an array of the possible replacement strategies available. See https://github.com/IEvangelist/profanity-filter?tab=readme-ov-file#-replacement-strategies
""")
.WithHttpLogging(HttpLoggingFields.All);

var data = profanity.MapGroup("data");

data.MapGet("names", OnGetDataNamesAsync)
.WithOpenApi()
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Returns an array of the data names.
""")
.WithHttpLogging(HttpLoggingFields.All);

data.MapGet("{name}", OnGetDataByNameAsync)
.WithOpenApi()
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Returns an array of the profane words for a given data name.
""")
.WithHttpLogging(HttpLoggingFields.All);

return app;
}

private static async Task<IResult> OnApplyFilterAsync(
[FromBody] ProfanityFilterRequest request,
[FromServices] IProfaneContentFilterService filterService)
{
if (request is null || string.IsNullOrWhiteSpace(request.Text))
{
return Results.BadRequest($"""
You need to provide a valid request.
""");
}

var parameters = new FilterParameters(
request.Strategy, FilterTarget.Body);

var filterResult =
await filterService.FilterProfanityAsync(request.Text, parameters);

var response = new ProfanityFilterResponse(
ContainsProfanity: filterResult.IsFiltered,
InputText: filterResult.Input,
FilteredText: filterResult.FinalOutput,
ReplacementStrategy: request.Strategy,
FiltrationSteps: [.. filterResult.Steps?.Where(static s => s.IsFiltered)],
Matches: [.. filterResult.Matches]
);

return TypedResults.Json(
response,
SourceGenerationContext.Default.ProfanityFilterResponse);
}

private static IResult OnGetStrategies() =>
TypedResults.Json([
.. Enum.GetValues<ReplacementStrategy>()
],
SourceGenerationContext.Default.StrategyResponseArray
);

private static async Task<IResult> OnGetDataNamesAsync(
[FromServices] IProfaneContentFilterService filterService)
{
var map = await filterService.ReadAllProfaneWordsAsync();

return TypedResults.Json([
.. map.Keys.Select(static key => Path.GetFileNameWithoutExtension(key))
],
SourceGenerationContext.Default.StringArray);
}

private static async Task<IResult> OnGetDataByNameAsync(
[FromRoute] string name,
[FromServices] IProfaneContentFilterService filterService)
{
var map = await filterService.ReadAllProfaneWordsAsync();

foreach (var (key, value) in map)
{
if (Path.GetFileNameWithoutExtension(key) == name)
{
return TypedResults.Json([
.. value.ProfaneWords
],
SourceGenerationContext.Default.StringArray);
}
}

return TypedResults.NotFound();
}
}
19 changes: 19 additions & 0 deletions src/ProfanityFilter.WebApi/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

global using System.Diagnostics.CodeAnalysis;

global using System.Text.Json;
global using System.Text.Json.Serialization;

global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.HttpLogging;

global using ProfanityFilter.Services;
global using ProfanityFilter.Services.Extensions;
global using ProfanityFilter.Services.Filters;
global using ProfanityFilter.Services.Results;

global using ProfanityFilter.WebApi.Endpoints;
global using ProfanityFilter.WebApi.Models;
global using ProfanityFilter.WebApi.Serialization;
13 changes: 13 additions & 0 deletions src/ProfanityFilter.WebApi/Models/ProfanityFilterRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Models;

/// <summary>
/// A representation of a profanity-filter request.
/// </summary>
/// <param name="Text">The text to evaluate for profanity.</param>
/// <param name="ReplacementStrategy">The desired replacement strategy to use. Defaults to <c>*</c>.</param>
public sealed record class ProfanityFilterRequest(
string Text,
ReplacementStrategy Strategy = ReplacementStrategy.Asterisk);
27 changes: 27 additions & 0 deletions src/ProfanityFilter.WebApi/Models/ProfanityFilterResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Models;

/// <summary>
/// A representation of a profanity-filter response object.
/// </summary>
/// <param name="ContainsProfanity">A boolean value indicating whether or
/// not the the <see cref="InputText"/> contains profanity.</param>
/// <param name="InputText">The original input text.</param>
/// <param name="FilteredText">The final output text, after filters have been applied.</param>
/// <param name="ReplacementStrategy">The replacement strategy used.</param>
/// <param name="FiltrationSteps">An array of steps representing the
/// filtration process, step-by-step.</param>
public sealed record class ProfanityFilterResponse(
[property:MemberNotNullWhen(
true,
nameof(ProfanityFilterResponse.FilteredText),
nameof(ProfanityFilterResponse.FiltrationSteps),
nameof(ProfanityFilterResponse.Matches))]
bool ContainsProfanity,
string InputText,
string? FilteredText,
ReplacementStrategy ReplacementStrategy,
ProfanityFilterStep[]? FiltrationSteps = default,
string[]? Matches = default);
20 changes: 20 additions & 0 deletions src/ProfanityFilter.WebApi/Models/ProfanityFilterStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Models;

/// <summary>
/// A representation of a profanity-filter step, detailing
/// each step applied to a filter operation.
/// </summary>
/// <param name="Input">The input before the step ran.</param>
/// <param name="Output">The output after the step ran.</param>
/// <param name="ProfaneSourceData">The profane source data for the step. For example, <i>Data/SwearWordList.txt</i>.</param>
public sealed record class ProfanityFilterStep(
string Input,
string Output,
string ProfaneSourceData)
{
public static implicit operator ProfanityFilterStep(FilterStep step) =>
new(step.Input, step.Output!, step.ProfaneSourceData);
}
43 changes: 43 additions & 0 deletions src/ProfanityFilter.WebApi/Models/StrategyResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Models;

/// <summary>
/// A representation of a strategy response object.
/// </summary>
/// <param name="StrategyName">The name of the strategy.</param>
/// <param name="StrategyValue">The <c>int</c> value of the strategy.</param>
/// <param name="Description">The description of the strategy.</param>
public sealed record class StrategyResponse(
string StrategyName,
int StrategyValue,
string Description)
{
public static implicit operator StrategyResponse(ReplacementStrategy strategy)
{
var description = strategy switch
{
ReplacementStrategy.Asterisk => "Replaces the profanity word with asterisk.",
ReplacementStrategy.Emoji => "Replaces the profanity word with an emoji.",
ReplacementStrategy.AngerEmoji => "Replaces the profanity word with the one of the anger emoji.",
ReplacementStrategy.MiddleSwearEmoji => "Represents a replacement strategy where the middle of the swear word is replaced with an emoji.",
ReplacementStrategy.RandomAsterisk => "Represents a replacement strategy where the profanity is replaced with a random number of asterisk.",
ReplacementStrategy.MiddleAsterisk => "Represents the middle asterisk replacement strategy, which replaces the characters in the middle of the profanity with asterisk.",
ReplacementStrategy.FirstLetterThenAsterisk => "Represents the first letter then asterisk replacement strategy, which replaces everything after the first letter of the profanity with asterisk.",
ReplacementStrategy.VowelAsterisk => "Represents a replacement strategy where vowels in a profane wordare replaced with asterisk.",
ReplacementStrategy.Bleep => "Represents a replacement strategy where the profanity is replaced with athe word \"bleep\".",
ReplacementStrategy.RedactedRectangle => "Represents a replacement strategy where the profane word has each letterreplaced with the rectangle symbol <c>█</c>.",
ReplacementStrategy.StrikeThrough => "Represents a replacement strategy where the profane word is <c>~~struck through~~</c>.",
ReplacementStrategy.Underscores => "Represents a replacement strategy where the profane word is replaced by underscores.",
ReplacementStrategy.Grawlix => "Represents a replacement strategy where the profane word is replaced by grawlix, for example <c>\"#$@!\"</c>.",
ReplacementStrategy.BoldGrawlix => "Represents a replacement strategy where the profane word is replaced by bold grawlix, for example <c>\"#$@!\"</c>.",
_ => "",
};

return new StrategyResponse(
StrategyName: Enum.GetName(strategy) ?? strategy.ToString(),
StrategyValue: (int)strategy,
Description: description);
}
}
Loading

0 comments on commit e4b0149

Please sign in to comment.