feat(core): add StringUtils for random string generation with customizable constraints - #28
Conversation
Ziedelth
left a comment
There was a problem hiding this comment.
Review Hermes
Verdict: 4 findings — no blockers, tests pass (8/8). Code is clean, well-tested (Given/When/Then, @nested, parameterized), and core stays zero-dependency. One MAJOR (inherited from the original RandomManager pattern) and two MINORs worth addressing before merge.
Warning: MAJOR
- StringUtils.kt:17 - duplicate
'inALPHABET_SPECIAL. The string contains the single quote twice (verified programmatically: 24 chars, 23 unique).'therefore has double the selection probability versus every other special character, in both the required-special injection and the random fills. Note: the same duplicate exists in the legacyRandomManager.RANDOM_STRING_CHARACTERS(shikkanime/core) - worth fixing in both. Suggested:"_-.!~*'();:@&=+$,/?#[]%".
Suggestion: MINOR
- StringUtils.kt:26 - randomness source undocumented.
Char.random()useskotlin.random.Random.Default(non-cryptographic). Fine for identifiers (the legacy call sites generate member identifiers), but the KDoc should state the non-cryptographic nature so nobody uses it for secrets; optionally accept arandom: Random = Random.Defaultparameter forSecureRandom().asKotlinRandom()injection. - StringUtils.kt:37-48 - per-call allocations.
listOf(...).count { it }boxes 4 booleans and allocates a list per call; the alphabet is re-concatenated on every invocation. Cheap to fix with two precomputed constants (ALPHABET_ALPHANUMERIC/ALPHABET_ALL) and integer counting. Cosmetic at this scale but the constants also read better.
NIT
- StringUtils.kt:29-30 - blank line inside the parameter list between
includeSpecialandshouldHaveAtLeastOneUppercase.
Looks good
- Zero-dependency core respected (pure Kotlin stdlib).
- Tests are thorough: alphabet membership, exclusion, all-required-types, minimum-length edge (4), silent-ignore case, negative/zero length (parameterized), and the length < required-types rejection with exact messages - all 8 pass.
- Silent-ignore of
shouldHaveAtLeastOneSpecialwhenincludeSpecial=falseis documented in KDoc and covered by a dedicated test - acceptable as designed.
Uncertain points / to clarify
- Does this PR replace
RandomManager.generateRandomStringandStringUtils.generateRandomStringfrom shikkanime/core (same alphabets, same'duplicate)? If so, plan the migration and removal on the core side to avoid two diverging implementations. - The proposed
random: Randomparameter (SecureRandom injection) is not covered by any guideline - to define: useful now or YAGNI?
Reviewed by Hermes Agent (fan-out multi-models; 2 OpenRouter reviewers unavailable - credits exhausted - consolidated review from the primary reviewer + local verifications)
Ziedelth
left a comment
There was a problem hiding this comment.
Review Hermes
Verdict :
Le code est propre, bien structuré et suit scrupuleusement les conventions du framework (KDoc, typage explicite, immutabilité, tests JUnit 6 @Nested / @DisplayName / Given-When-Then, zéro dépendance externe pour le module core).
Récapitulatif des retours
core/src/main/kotlin/StringUtils.kt:17: Le caractère quote simple (') est présent en double dansALPHABET_SPECIAL(aux index 3 et 7), ce qui double sa probabilité de tirage lors de la génération aléatoire.
⚠️ Points incertains / pistes d'évolution (architecture & design)
-
Source de randomisation cryptographique vs PRNG par défaut :
generateRandomStringutilisekotlin.random.Random.Defaultviarandom()etshuffled().- Pour des identifiants non sensibles ou du mocking de test, c'est suffisant. En revanche, si la fonction doit servir à générer des tokens d'authentification, secrets ou clés de session, permettre de passer une instance
kotlin.random.Randompersonnalisée (ex.SecureRandom().asKotlinRandom()) ou documenter explicitement l'usage non cryptographique dans la KDoc serait bénéfique.
-
Allocations intermédiaires à chaque appel :
listOf(...).count { it }alloue une liste de 4 booléens à chaque exécution, etval alphabet = ...concatène les chaînes à chaque appel.- Les alphabets combinés (
ALPHABET_ALPHANUMERICetALPHABET_ALL) pourraient être pré-calculés en constantesprivate const val, et le comptage calculé sans allocation.
-
Ligne vide dans la liste des paramètres (
StringUtils.kt:30) :- Une ligne vide sépare
includeSpecialdes paramètres de contraintesshouldHaveAtLeastOne*. C'est un choix cosmétique d'aération, mais peut être nettoyé si le style projet préfère des listes de paramètres compactes.
- Une ligne vide sépare
|
Complément de review (findings additionnels du reviewer après analyse complète) En plus des 4 points postés en review inline :
Note de sévérité ajustée : la randomness non cryptographique (point 2 de la review inline) mérite d'être traitée en priorité — la forme de l'API invite à générer des secrets, et c'est le point que le reviewer a remonté en MAJOR. |
| const val ALPHABET_NUMBERS = "0123456789" | ||
|
|
||
| /** Special characters available for random string generation. */ | ||
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" |
There was a problem hiding this comment.
WARNING MAJOR - duplicate ' in ALPHABET_SPECIAL. The single quote appears twice (24 chars, 23 unique - verified programmatically), so ' has double the selection probability versus other special characters. Same duplicate exists in the legacy RandomManager.RANDOM_STRING_CHARACTERS (shikkanime/core).
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" | |
| const val ALPHABET_SPECIAL = "_-.!~*'();:@&=+$,/?#[]%" |
| length: Int, | ||
| includeSpecial: Boolean = true, | ||
|
|
||
| shouldHaveAtLeastOneUppercase: Boolean = false, |
There was a problem hiding this comment.
NIT - blank line inside the parameter list breaks the visual grouping of the requirement flags.
| shouldHaveAtLeastOneUppercase: Boolean = false, | |
| includeSpecial: Boolean = true, | |
| shouldHaveAtLeastOneUppercase: Boolean = false, |
| * | ||
| * @throws IllegalArgumentException if [length] is not positive or cannot accommodate all required character types | ||
| */ | ||
| fun generateRandomString( |
There was a problem hiding this comment.
SUGGESTION MINOR - randomness source undocumented. Char.random() uses kotlin.random.Random.Default (non-cryptographic). Fine for identifiers, but the KDoc should state it so nobody uses this for secrets; also accept a random parameter for SecureRandom().asKotlinRandom() injection.
| fun generateRandomString( | |
| fun generateRandomString( | |
| length: Int, | |
| includeSpecial: Boolean = true, | |
| shouldHaveAtLeastOneUppercase: Boolean = false, | |
| shouldHaveAtLeastOneLowercase: Boolean = false, | |
| shouldHaveAtLeastOneNumber: Boolean = false, | |
| shouldHaveAtLeastOneSpecial: Boolean = false, | |
| random: Random = Random.Default | |
| ): String { |
| ): String { | ||
| require(length > 0) { "Length must be greater than 0" } | ||
|
|
||
| val requiredCharacterTypeCount = listOf( |
There was a problem hiding this comment.
SUGGESTION MINOR - per-call allocations. listOf(...).count { it } boxes 4 booleans and allocates a list per call; the alphabet is re-concatenated on every invocation. Precomputed constants (ALPHABET_ALPHANUMERIC / ALPHABET_ALL) read better and cost nothing.
| val requiredCharacterTypeCount = listOf( | |
| var requiredCharacterTypeCount = 0 | |
| if (shouldHaveAtLeastOneUppercase) requiredCharacterTypeCount++ | |
| if (shouldHaveAtLeastOneLowercase) requiredCharacterTypeCount++ | |
| if (shouldHaveAtLeastOneNumber) requiredCharacterTypeCount++ | |
| if (includeSpecial && shouldHaveAtLeastOneSpecial) requiredCharacterTypeCount++ | |
| require(requiredCharacterTypeCount <= length) { "Length must be greater than or equal to the number of required character types" } | |
| val alphabet = if (includeSpecial) ALPHABET_ALL else ALPHABET_ALPHANUMERIC |
Ziedelth
left a comment
There was a problem hiding this comment.
Review Hermes
Verdict: 💡 COMMENT — no blocker, 3 verified inline points, open design questions in the details block.
Inline (each verified against the head SHA): duplicate apostrophe in ALPHABET_SPECIAL; contradictory includeSpecial/shouldHaveAtLeastOneSpecial silently ignored; Random.Default (ThreadLocalRandom on JVM) is not cryptographically secure — document the contract or make the randomness source injectable.
⚠️ Points incertains / à clarifier
object StringUtilsvs the transverse "no singletons" convention: the framework already ships utility objects (Validator,ControllerBinder), so the codebase pattern contradicts a strict reading — decide whether stateless utility objects are the sanctioned exception and codify it.- Explicit types on the public
const val ALPHABET_*(the root guideline asks for explicit types on public framework APIs, but existing consts in the codebase also omit them) — pick one convention. includeSpecial = trueas default is surprising for a generic generator (URL/shell-sensitive punctuation); no requirement fixes the intent — considerfalseor no default.
ALPHABET_NUMBERS/shouldHaveAtLeastOneNumberhold digits, not numbers — optional rename to*DIGITS.- Blank line separating
includeSpecialfrom theshouldHaveAtLeastOne*group — cosmetic, keep or drop. - Per-call allocations (
listOf(...).count, alphabet re-concatenation) — micro-optimization only; combined alphabet constants could be precomputed if this ever sits on a hot path.
Guideline follow-ups proposed for the feedback loop: (1) codify the randomness-source rule (Random.Default vs SecureRandom) for public framework APIs; (2) codify whether utility objects are allowed in the framework.
| const val ALPHABET_NUMBERS = "0123456789" | ||
|
|
||
| /** Special characters available for random string generation. */ | ||
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" |
There was a problem hiding this comment.
The apostrophe appears twice in ALPHABET_SPECIAL, so it is selected with twice the probability of any other special character (verified programmatically: 24 characters, 23 unique):
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" | |
| const val ALPHABET_SPECIAL = "_-.!~*'();:@&=+$,/?#[]%" |
| shouldHaveAtLeastOneNumber: Boolean = false, | ||
| shouldHaveAtLeastOneSpecial: Boolean = false | ||
| ): String { | ||
| require(length > 0) { "Length must be greater than 0" } |
There was a problem hiding this comment.
includeSpecial = false combined with shouldHaveAtLeastOneSpecial = true is silently ignored: the caller requests a postcondition that is never enforced, and the returned string can legitimately contain no special character. Failing fast on contradictory arguments prevents that class of caller mistakes (the current "should ignore" test would become a rejection test, and the KDoc @throws updated):
| require(length > 0) { "Length must be greater than 0" } | |
| require(length > 0) { "Length must be greater than 0" } | |
| require(includeSpecial || !shouldHaveAtLeastOneSpecial) { "Cannot require special characters when includeSpecial is false" } |
| /** | ||
| * Generates a random string with the requested length and character constraints. | ||
| * | ||
| * The special-character requirement is applied only when [includeSpecial] is `true`. |
There was a problem hiding this comment.
random() and shuffled() use kotlin.random.Random.Default, which on the JVM delegates to ThreadLocalRandom/java.util.Random — both documented as NOT cryptographically secure (verified on kotlin-stdlib 2.4.10: JDK8PlatformImplementations.defaultPlatformRandom() returns PlatformThreadLocalRandom). For a public framework API exposing password-style constraints, that is a predictable-output footgun if a consumer ever generates tokens or credentials with it. Either document the non-cryptographic contract (suggestion below), or add an injectable random: Random = Random.Default parameter so SecureRandom().asKotlinRandom() can be supplied:
| * The special-character requirement is applied only when [includeSpecial] is `true`. | |
| * The special-character requirement is applied only when [includeSpecial] is `true`. | |
| * | |
| * Not cryptographically secure: relies on [kotlin.random.Random.Default]. Do not use for | |
| * security-sensitive values such as tokens, secrets, or passwords. |
Ziedelth
left a comment
There was a problem hiding this comment.
Review Hermes
Verdict : 💡 COMMENT — aucun bug vérifié ; 1 point vérifié inline, quelques points de design à clarifier (collapsés).
✅ Points vérifiés conformes
corereste zéro-dépendance (aucun import de module) — ok.- KDoc sur l'API publique, anglais partout, types explicites, immutabilité — ok.
- Tests : JUnit 6,
@Nested+@DisplayName, noms en backticks, Given/When/Then,@ParameterizedTest— conformes àguidelines/TESTING.md. - CI Build & Test : PASS (confirmé en local :
./gradlew testgreen).
⚠️ Points incertains / à clarifier (design, non vérifiables comme violation de guideline)
- Source d'aléatoire (
Random.Default) —core/src/main/kotlin/StringUtils.kt:50-57s'appuie surString.random()/Iterable.random(), donckotlin.random.Random.Default, non cryptographiquement sûr. Aucune guideline n'exigeSecureRandom: si l'utilitaire peut servir à des tokens/mots de passe, prévoir soit un paramètre injectable (random: Random = Random.Default, accepteSecureRandom().asKotlinRandom()), soit une mention KDoc explicite « usage non cryptographique ». - Contradiction de paramètres silencieuse —
includeSpecial = false+shouldHaveAtLeastOneSpecial = trueignore silencieusement la contrainte demandée (documenté en KDoc et testé, mais unrequire(includeSpecial || !shouldHaveAtLeastOneSpecial)éviterait de produire une valeur plus faible que demandée). object StringUtilsvs convention « pas de singletons » — conventions.md interdit les singletons (règle écrite pour l'injection de dépendances app) ; un utilitaire stateless enobject+ const est idiomatique Kotlin. À clarifier : top-level declarations / const vsobject?- Allocations par appel —
listOf(...).count { }+ concaténation d'alphabets à chaque invocation ; précalculer des constantes combinées (ex.ALPHABET_ALPHANUMERIC) si l'utilitaire est appelé en hot path. - NITs — ligne vide entre
includeSpecialet les flagsshouldHaveAtLeastOne*(ligne 29) ; les deux nouveaux fichiers n'ont pas de newline final.
| const val ALPHABET_NUMBERS = "0123456789" | ||
|
|
||
| /** Special characters available for random string generation. */ | ||
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" |
There was a problem hiding this comment.
🔴 Vérifié en local : l'apostrophe ' apparaît 2 fois dans ALPHABET_SPECIAL (24 caractères, 23 distincts) — double probabilité de sélection par rapport à chaque autre caractère spécial, y compris pour le caractère requis injecté.
| const val ALPHABET_SPECIAL = "_-.'!~*'();:@&=+$,/?#[]%" | |
| const val ALPHABET_SPECIAL = "_-.'!~*();:@&=+$,/?#[]%" |
…zable constraints
…erator and improved validation
db2ec37 to
333140c
Compare
No description provided.