Skip to content

Commit 71b1c22

Browse files
committed
feat: notations for file system paths
1 parent 2bfbd1d commit 71b1c22

File tree

7 files changed

+81
-13
lines changed

7 files changed

+81
-13
lines changed

src/main/kotlin/BaseStringNotation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ abstract class BaseStringNotation(private val splitAt: Regex): StringNotation {
1212
protected open fun transformPartAfterParse(index: Int, part: String) = part
1313

1414
override fun parse(sourceString: String): Word =
15-
Word(sourceString.split(splitAt).asSequence().filter(String::isNotBlank).mapIndexed(::transformPartAfterParse))
15+
Word(sourceString.split(splitAt).asSequence().mapIndexed(::transformPartAfterParse))
1616

1717
/**
1818
* Allows to transform a part before it is being printed. The default implementation does not modify the part in any way.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.joshuagleitze.stringnotation
2+
3+
/**
4+
* A notation for paths on a Unix file system. [Parsing][StringNotation.parse] will recognise all substrings that are
5+
* separated by `/` as a [part][Word.parts]. [Printing][StringNotation.print] will print the parts separated by `/`
6+
* after removing `\u0000` (ASCII: `NUL`) and `\u0057` (ASCII: slash) characters from them.
7+
*
8+
* [Printed][StringNotation.print] paths will not start with an additional `/`. To print an absolute path, include `""` (the empty string) as the first part in the printed word.
9+
*/
10+
object UnixPath: BaseStringNotation(Regex("/")) {
11+
private val invalidChars = Regex("[\u0000/]+")
12+
override fun transformPartToPrint(index: Int, part: String) = part.replace(invalidChars, "")
13+
override fun printBeforeInnerPart(index: Int, part: String) = "/"
14+
}
15+
16+
/**
17+
* A notation for paths on a Windows file system. [Parsing][StringNotation.parse] will recognise all substrings that are
18+
* separated by `\` as a [part][Word.parts]. [Printing][StringNotation.print] will print the parts separated by `\`
19+
* after removing the following characters from them:
20+
* * ASCII control characters
21+
* * `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
22+
*
23+
* To allow printing paths that start with a drive letter, the notation will not strip a `:` from the first part if it’s the last character.
24+
*/
25+
object WindowsPath: BaseStringNotation(Regex("\\\\")) {
26+
private val invalidChars = Regex("[\\p{Cntrl}<>:\"/\\\\|?*]+")
27+
override fun transformPartToPrint(index: Int, part: String): String {
28+
val replaced = part.replace(invalidChars, "")
29+
return if (index == 0 && part.endsWith(":")) {
30+
"$replaced:"
31+
} else replaced
32+
}
33+
34+
override fun printBeforeInnerPart(index: Int, part: String) = "\\"
35+
}

src/main/kotlin/JavaNotations.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import javax.lang.model.SourceVersion
1111
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
1212
* using [SourceVersion.isKeyword].
1313
*/
14-
data object JavaTypeName : StringNotation by UpperCamelCase {
14+
object JavaTypeName: StringNotation by UpperCamelCase {
1515
override fun print(word: Word) = UpperCamelCase.print(
1616
Word(word.parts.mapIndexed { index, wordPart ->
1717
if (index == 0) wordPart.keepOnlyJavaIdentifierChars()
@@ -28,7 +28,7 @@ data object JavaTypeName : StringNotation by UpperCamelCase {
2828
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
2929
* using [SourceVersion.isKeyword].
3030
*/
31-
object JavaMemberName : BaseStringNotation(camelCaseSplitRegex) {
31+
object JavaMemberName: BaseStringNotation(camelCaseSplitRegex) {
3232
override fun transformPartAfterParse(index: Int, part: String) = part.lowercase(Locale.ROOT)
3333

3434
override fun print(word: Word) = word.parts
@@ -52,7 +52,7 @@ object JavaMemberName : BaseStringNotation(camelCaseSplitRegex) {
5252
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
5353
* using [SourceVersion.isKeyword].
5454
*/
55-
object JavaPackagePart : BaseStringNotation(Regex("_|${camelCaseSplitRegex.pattern}")) {
55+
object JavaPackagePart: BaseStringNotation(Regex("_|${camelCaseSplitRegex.pattern}")) {
5656
override fun transformPartAfterParse(index: Int, part: String) = part.lowercase(Locale.ROOT)
5757

5858
override fun transformPartToPrint(index: Int, part: String) = part.lowercase(Locale.ROOT)
@@ -67,7 +67,7 @@ object JavaPackagePart : BaseStringNotation(Regex("_|${camelCaseSplitRegex.patte
6767
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
6868
* using [SourceVersion.isKeyword].
6969
*/
70-
object JavaPackageName : BaseStringNotation(Regex("\\.")) {
70+
object JavaPackageName: BaseStringNotation(Regex("\\.")) {
7171
override fun transformPartToPrint(index: Int, part: String) = part.lowercase(Locale.ROOT).makeValidJavaIdentifier()
7272

7373
override fun printBeforeInnerPart(index: Int, part: String) = "."
@@ -80,7 +80,7 @@ object JavaPackageName : BaseStringNotation(Regex("\\.")) {
8080
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected
8181
* using [SourceVersion.isKeyword].
8282
*/
83-
data object JavaConstantName : StringNotation by ScreamingSnakeCase {
83+
data object JavaConstantName: StringNotation by ScreamingSnakeCase {
8484
override fun print(word: Word) = ScreamingSnakeCase.print(word).makeValidJavaIdentifier()
8585
}
8686

src/main/kotlin/Notations.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ internal val camelCaseSplitRegex = Regex("(?<=.)(?=\\p{Lu})")
99
*
1010
* @see JavaTypeName
1111
*/
12-
object UpperCamelCase : BaseStringNotation(camelCaseSplitRegex) {
12+
object UpperCamelCase: BaseStringNotation(camelCaseSplitRegex) {
1313
override fun transformPartAfterParse(index: Int, part: String) = part.lowercase(Locale.ROOT)
1414

1515
public override fun transformPartToPrint(index: Int, part: String) = part.firstUpperThenLowerCase()
@@ -20,7 +20,7 @@ object UpperCamelCase : BaseStringNotation(camelCaseSplitRegex) {
2020
*
2121
* @see JavaMemberName
2222
*/
23-
object LowerCamelCase : BaseStringNotation(camelCaseSplitRegex) {
23+
object LowerCamelCase: BaseStringNotation(camelCaseSplitRegex) {
2424
override fun transformPartAfterParse(index: Int, part: String) = part.lowercase(Locale.ROOT)
2525

2626
override fun transformPartToPrint(index: Int, part: String) =
@@ -30,7 +30,7 @@ object LowerCamelCase : BaseStringNotation(camelCaseSplitRegex) {
3030
/**
3131
* The `SCREAMING_SNAKE_CASE` notation.
3232
*/
33-
object ScreamingSnakeCase : BaseStringNotation(Regex("_")) {
33+
object ScreamingSnakeCase: BaseStringNotation(Regex("(?<!^)_")) {
3434
override fun transformPartAfterParse(index: Int, part: String) = part.lowercase(Locale.ROOT)
3535

3636
override fun printBeforeInnerPart(index: Int, part: String) = "_"
@@ -41,7 +41,7 @@ object ScreamingSnakeCase : BaseStringNotation(Regex("_")) {
4141
/**
4242
* The `snake_case` notation.
4343
*/
44-
object SnakeCase: BaseStringNotation(Regex("_")) {
44+
object SnakeCase: BaseStringNotation(Regex("(?<!^)_")) {
4545
override fun printBeforeInnerPart(index: Int, part: String) = "_"
4646
}
4747

src/test/kotlin/BaseNotationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ data class NotationTestData(val word: Word, val string: String, var minimumJavaV
6363

6464
infix fun Word.to(string: String) = NotationTestData(this, string)
6565
infix fun String.to(word: Word) = NotationTestData(word, this)
66-
infix fun NotationTestData.ifJvmVersionIsAtLeast(minimumJavaVersion: Int) = this.apply { this.minimumJavaVersion = minimumJavaVersion }
66+
infix fun NotationTestData.ifJvmVersionIsAtLeast(minimumJavaVersion: Int) =
67+
this.apply { this.minimumJavaVersion = minimumJavaVersion }
6768

6869
val currentJavaVersion by lazy {
6970
System.getProperty("java.runtime.version")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package de.joshuagleitze.stringnotation
2+
3+
class UnixPathTest: BaseNotationTest(
4+
notation = UnixPath,
5+
unchangedWords = listOf(
6+
"/home/user/some/file" to Word("", "home", "user", "some", "file"),
7+
"a/relative/path" to Word("a", "relative", "path")
8+
),
9+
printOnlyWords = listOf(
10+
Word("", "home", "null\u0000") to "/home/null",
11+
Word("", "home", "user/some/file") to "/home/usersomefile",
12+
)
13+
)
14+
15+
class WindowsPathTest: BaseNotationTest(
16+
notation = WindowsPath,
17+
unchangedWords = listOf(
18+
"C:\\Users\\user\\some\\file" to Word("C:", "Users", "user", "some", "file"),
19+
"a\\relative\\path" to Word("a", "relative", "path")
20+
),
21+
printOnlyWords = listOf(
22+
*('\u0000'..'\u001F').map { controlChar ->
23+
Word("C:", "bad${controlChar}File") to "C:\\badFile"
24+
}.toTypedArray(),
25+
Word("C:", "bad\u007FFile") to "C:\\badFile",
26+
Word("C:", "bad<File>") to "C:\\badFile",
27+
)
28+
)

src/test/kotlin/NotationsTest.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ class LowerCamelCaseTest: BaseNotationTest(
1818

1919
class ScreamingSnakeCaseTest: BaseNotationTest(
2020
notation = ScreamingSnakeCase,
21-
unchangedWords = listOf("IM_IN_SCREAMING_SNAKE_CASE" to Word("im", "in", "screaming", "snake", "case")),
21+
unchangedWords = listOf(
22+
"IM_IN_SCREAMING_SNAKE_CASE" to Word("im", "in", "screaming", "snake", "case"),
23+
"_I_HAVE_A_PREFIX" to Word("_i", "have", "a", "prefix")
24+
),
2225
parseOnlyWords = listOf("im_iN_sNAKe_cASE_with_CAPItals" to Word("im", "in", "snake", "case", "with", "capitals")),
2326
printOnlyWords = listOf(Word("im", "iN", "sNAKe", "cASE", "with", "CAPItals") to "IM_IN_SNAKE_CASE_WITH_CAPITALS")
2427
)
@@ -27,7 +30,8 @@ class SnakeCaseTest: BaseNotationTest(
2730
notation = SnakeCase,
2831
unchangedWords = listOf(
2932
"im_in_snake_case" to Word("im", "in", "snake", "case"),
30-
"im_iN_sNAKe_cASE_with_CAPItals" to Word("im", "iN", "sNAKe", "cASE", "with", "CAPItals")
33+
"im_iN_sNAKe_cASE_with_CAPItals" to Word("im", "iN", "sNAKe", "cASE", "with", "CAPItals"),
34+
"_i_have_a_prefix" to Word("_i", "have", "a", "prefix")
3135
)
3236
)
3337

0 commit comments

Comments
 (0)