Last active
June 16, 2025 09:48
-
-
Save stephenkirk/82d841ff959234d1f743a082b98381e5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using Microsoft.Extensions.DependencyInjection; | |
| using Microsoft.Extensions.Logging; | |
| using Microsoft.Extensions.Options; | |
| using System.ComponentModel.DataAnnotations; | |
| namespace Balatro.Identity.Generation.Services | |
| { | |
| public interface IWordListProvider | |
| { | |
| IEnumerable<string> GetAdjectives(); | |
| IEnumerable<string> GetNouns(); | |
| } | |
| public interface IRandomNumberGenerator | |
| { | |
| int Next(int maxValue); | |
| } | |
| public interface IIdentifierGenerationService | |
| { | |
| Task<string> GenerateIdentifierAsync(string typePrefix, CancellationToken cancellationToken = default); | |
| } | |
| public interface IIdentifierValidationService | |
| { | |
| ValidationResult ValidateTypePrefix(string typePrefix); | |
| } | |
| public class IdentifierGenerationOptions | |
| { | |
| public const string SectionName = "IdentifierGeneration"; | |
| [Required] | |
| [MinLength(1)] | |
| public string[] Adjectives { get; set; } = Array.Empty<string>(); | |
| [Required] | |
| [MinLength(1)] | |
| public string[] Nouns { get; set; } = Array.Empty<string>(); | |
| [Range(1, 100)] | |
| public int MaxTypePrefixLength { get; set; } = 20; | |
| public bool EnableTelemetry { get; set; } = true; | |
| } | |
| public class CryptographicRandomNumberGenerator : IRandomNumberGenerator | |
| { | |
| private readonly Random _random; | |
| public CryptographicRandomNumberGenerator() | |
| { | |
| _random = new Random(Guid.NewGuid().GetHashCode()); | |
| } | |
| public int Next(int maxValue) => _random.Next(maxValue); | |
| } | |
| public class ConfigurationBasedWordListProvider : IWordListProvider | |
| { | |
| private readonly IdentifierGenerationOptions _options; | |
| public ConfigurationBasedWordListProvider(IOptions<IdentifierGenerationOptions> options) | |
| { | |
| _options = options.Value ?? throw new ArgumentNullException(nameof(options)); | |
| } | |
| public IEnumerable<string> GetAdjectives() => _options.Adjectives; | |
| public IEnumerable<string> GetNouns() => _options.Nouns; | |
| } | |
| public class IdentifierValidationService : IIdentifierValidationService | |
| { | |
| private readonly IdentifierGenerationOptions _options; | |
| private readonly ILogger<IdentifierValidationService> _logger; | |
| public IdentifierValidationService( | |
| IOptions<IdentifierGenerationOptions> options, | |
| ILogger<IdentifierValidationService> logger) | |
| { | |
| _options = options.Value; | |
| _logger = logger; | |
| } | |
| public ValidationResult ValidateTypePrefix(string typePrefix) | |
| { | |
| if (string.IsNullOrWhiteSpace(typePrefix)) | |
| { | |
| _logger.LogWarning("Type prefix validation failed: null or whitespace"); | |
| return new ValidationResult("Type prefix cannot be null or whitespace"); | |
| } | |
| if (typePrefix.Length > _options.MaxTypePrefixLength) | |
| { | |
| _logger.LogWarning("Type prefix validation failed: exceeds maximum length of {MaxLength}", | |
| _options.MaxTypePrefixLength); | |
| return new ValidationResult($"Type prefix cannot exceed {_options.MaxTypePrefixLength} characters"); | |
| } | |
| if (typePrefix.Any(c => !char.IsLetterOrDigit(c) && c != '-' && c != '_')) | |
| { | |
| _logger.LogWarning("Type prefix validation failed: contains invalid characters"); | |
| return new ValidationResult("Type prefix can only contain letters, digits, hyphens, and underscores"); | |
| } | |
| return ValidationResult.Success!; | |
| } | |
| } | |
| public class BalatroIdentifierGenerationService : IIdentifierGenerationService | |
| { | |
| private readonly IWordListProvider _wordListProvider; | |
| private readonly IRandomNumberGenerator _randomNumberGenerator; | |
| private readonly IIdentifierValidationService _validationService; | |
| private readonly ILogger<BalatroIdentifierGenerationService> _logger; | |
| private readonly IdentifierGenerationOptions _options; | |
| public BalatroIdentifierGenerationService( | |
| IWordListProvider wordListProvider, | |
| IRandomNumberGenerator randomNumberGenerator, | |
| IIdentifierValidationService validationService, | |
| ILogger<BalatroIdentifierGenerationService> logger, | |
| IOptions<IdentifierGenerationOptions> options) | |
| { | |
| _wordListProvider = wordListProvider ?? throw new ArgumentNullException(nameof(wordListProvider)); | |
| _randomNumberGenerator = randomNumberGenerator ?? throw new ArgumentNullException(nameof(randomNumberGenerator)); | |
| _validationService = validationService ?? throw new ArgumentNullException(nameof(validationService)); | |
| _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |
| _options = options.Value ?? throw new ArgumentNullException(nameof(options)); | |
| } | |
| public async Task<string> GenerateIdentifierAsync(string typePrefix, CancellationToken cancellationToken = default) | |
| { | |
| using var activity = _options.EnableTelemetry ? | |
| System.Diagnostics.Activity.Current?.Source.StartActivity("GenerateIdentifier") : null; | |
| _logger.LogDebug("Beginning identifier generation for type prefix: {TypePrefix}", typePrefix); | |
| var validationResult = _validationService.ValidateTypePrefix(typePrefix); | |
| if (validationResult != ValidationResult.Success) | |
| { | |
| _logger.LogError("Identifier generation failed validation: {ValidationError}", validationResult.ErrorMessage); | |
| throw new ArgumentException(validationResult.ErrorMessage, nameof(typePrefix)); | |
| } | |
| var adjectives = _wordListProvider.GetAdjectives().ToArray(); | |
| var nouns = _wordListProvider.GetNouns().ToArray(); | |
| if (!adjectives.Any() || !nouns.Any()) | |
| { | |
| _logger.LogError("Word lists are empty - cannot generate identifier"); | |
| throw new InvalidOperationException("Word lists cannot be empty"); | |
| } | |
| await Task.Delay(1, cancellationToken); | |
| var selectedAdjective = adjectives[_randomNumberGenerator.Next(adjectives.Length)]; | |
| var selectedNoun = nouns[_randomNumberGenerator.Next(nouns.Length)]; | |
| var generatedId = $"{typePrefix}-{selectedAdjective}-{selectedNoun}"; | |
| _logger.LogInformation("Successfully generated identifier: {GeneratedId}", generatedId); | |
| activity?.SetTag("identifier.type_prefix", typePrefix); | |
| activity?.SetTag("identifier.generated", generatedId); | |
| return generatedId; | |
| } | |
| } | |
| public static class ServiceCollectionExtensions | |
| { | |
| public static IServiceCollection AddBalatroIdentifierGeneration( | |
| this IServiceCollection services, | |
| Action<IdentifierGenerationOptions>? configureOptions = null) | |
| { | |
| services.AddOptions<IdentifierGenerationOptions>() | |
| .Configure(options => | |
| { | |
| // add more stuff here as needed | |
| options.Adjectives = new[] | |
| { | |
| "uncommon", "rare", "foil", "checkered", "orbital", "handy", "small", "big", "psychic", "verdant", | |
| "violet", "crimson", "cerulean", "foolish", "grim", "wasteful" // ... | |
| }; | |
| options.Nouns = new[] | |
| { | |
| "serpent", "pillar", "needle", "head", "hierophant", "hermit", "strength", | |
| "temperance", "aura", "wraith", "sigil", "ouija", "ectoplasm", "immolate", | |
| "ankh", "cryptid", "pluto", "uranus", "venus", "saturn", "jupiter", "earth", | |
| "neptune", "ball", "globe", "telescope", "observatory", "money", "antimatter", | |
| "trick", "illusion", "hieroglyph", "petroglyph", "retcon", "brush", "palette" | |
| }; | |
| configureOptions?.Invoke(options); | |
| }) | |
| .ValidateDataAnnotations(); | |
| services.AddSingleton<IRandomNumberGenerator, CryptographicRandomNumberGenerator>(); | |
| services.AddSingleton<IWordListProvider, ConfigurationBasedWordListProvider>(); | |
| services.AddScoped<IIdentifierValidationService, IdentifierValidationService>(); | |
| services.AddScoped<IIdentifierGenerationService, BalatroIdentifierGenerationService>(); | |
| return services; | |
| } | |
| } | |
| } | |
| // usage example: | |
| // services.AddBalatroIdentifierGeneration(); | |
| // var idService = serviceProvider.GetRequiredService<IIdentifierGenerationService>(); | |
| // var id = await idService.GenerateIdentifierAsync("user"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment