-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Notations for Java packages and constants
fix: also handle Java keywords
- Loading branch information
Showing
6 changed files
with
253 additions
and
130 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package de.joshuagleitze.stringnotation | ||
|
||
import java.util.* | ||
import java.util.stream.IntStream | ||
import javax.lang.model.SourceVersion | ||
|
||
/** | ||
* A notation for Java type names. This notation is like [UpperCamelCase], but when [printing][StringNotation.print], it will drop any | ||
* character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it. | ||
* | ||
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected | ||
* using [SourceVersion.isKeyword]. | ||
*/ | ||
object JavaTypeName: StringNotation by UpperCamelCase { | ||
override fun print(word: Word) = UpperCamelCase.print(word).makeValidJavaIdentifier() | ||
|
||
override fun toString() = this::class.java.simpleName!! | ||
} | ||
|
||
/** | ||
* A notation for java member names. This notation is like [LowerCamelCase], but when [printing][StringNotation.print], it will drop any | ||
* character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it. | ||
* | ||
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected | ||
* using [SourceVersion.isKeyword]. | ||
*/ | ||
object JavaMemberName: BaseStringNotation(camelCaseSplitRegex) { | ||
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT) | ||
|
||
override fun print(word: Word) = word.parts | ||
.foldIndexed(StringBuffer()) { index, left, right -> | ||
val rightPart = | ||
if (left.contains(Regex("[a-zA-Z]"))) right.toFirstUpperOtherLowerCase() | ||
else right.toLowerCase() | ||
left.append(printBeforePart(index, rightPart)).append(rightPart) | ||
}.toString().makeValidJavaIdentifier() | ||
} | ||
|
||
/** | ||
* A notation for java package parts. When [printing][StringNotation.print], it simply concatenates all word parts and drops any character | ||
* that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it. When | ||
* [parsing][StringNotation.parse], the notation will recognise word parts both in the [LowerCamelCase] and the [SnakeCase] notation. | ||
* However, neither notation is conventional and parsing will usually yield only one word part on real-world inputs. | ||
* | ||
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected | ||
* using [SourceVersion.isKeyword]. | ||
*/ | ||
object JavaPackagePart: BaseStringNotation(Regex("_|${camelCaseSplitRegex.pattern}")) { | ||
override fun transformPartAfterParse(index: Int, part: String) = part.toLowerCase(Locale.ROOT) | ||
|
||
override fun transformPartToPrint(index: Int, part: String) = part.toLowerCase(Locale.ROOT) | ||
|
||
override fun print(word: Word) = super.print(word).makeValidJavaIdentifier() | ||
} | ||
|
||
/** | ||
* A notation for whole java packages. When [printing][StringNotation.print] parts, it will drop any character that is not allowed in a Java | ||
* identifier. If the result is a Java keyword, `_` will be appended to it. | ||
* | ||
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected | ||
* using [SourceVersion.isKeyword]. | ||
*/ | ||
object JavaPackageName: BaseStringNotation(Regex("\\.")) { | ||
override fun transformPartToPrint(index: Int, part: String) = part.toLowerCase(Locale.ROOT).makeValidJavaIdentifier() | ||
|
||
override fun printBeforeInnerPart(index: Int, part: String) = "." | ||
} | ||
|
||
/** | ||
* A notation for `static final` fields in Java. This notation is like [ScreamingSnakeCase], but when [printing][StringNotation.print], it | ||
* will drop any character that is not allowed in a Java identifier. If the result is a Java keyword, `_` will be appended to it. | ||
* | ||
* Allowed characters are determined using [Character.isJavaIdentifierStart] and [Character.isJavaIdentifierPart]. Keywords are detected | ||
* using [SourceVersion.isKeyword]. | ||
*/ | ||
object JavaConstantName: StringNotation by ScreamingSnakeCase { | ||
override fun print(word: Word) = ScreamingSnakeCase.print(word).makeValidJavaIdentifier() | ||
|
||
override fun toString() = this::class.java.simpleName!! | ||
} | ||
|
||
private fun String.makeValidJavaIdentifier() = this.keepOnlyJavaIdentifierChars().neutralizeJavaReservedKeywords().ifEmpty { "__" } | ||
|
||
private fun String.keepOnlyJavaIdentifierChars() = this.chars() | ||
.skipWhile { !Character.isJavaIdentifierStart(it) } | ||
.filter { Character.isJavaIdentifierPart(it) } | ||
.collect({ StringBuilder() }, { left, right -> left.appendCodePoint(right) }, { left, right -> left.append(right) }) | ||
.toString() | ||
|
||
private fun String.neutralizeJavaReservedKeywords() = if (SourceVersion.isKeyword(this)) this + "_" else this | ||
|
||
private inline fun IntStream.skipWhile(crossinline condition: (Int) -> Boolean): IntStream { | ||
var found = false | ||
return this.filter { | ||
if (!found) { | ||
found = !condition(it) | ||
} | ||
found | ||
} | ||
} | ||
|
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package de.joshuagleitze.stringnotation | ||
|
||
import ch.tutteli.atrium.api.fluent.en_GB.feature | ||
import ch.tutteli.atrium.api.fluent.en_GB.toBe | ||
import ch.tutteli.atrium.api.verbs.expect | ||
import org.junit.jupiter.api.TestInstance | ||
import org.junit.jupiter.params.ParameterizedTest | ||
import org.junit.jupiter.params.provider.Arguments | ||
import org.junit.jupiter.params.provider.MethodSource | ||
|
||
@TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
abstract class BaseNotationTest( | ||
private val notation: StringNotation, | ||
private val unchangedWords: List<Pair<String, Word>>, | ||
private val parseOnlyWords: List<Pair<String, Word>> = emptyList(), | ||
private val printOnlyWords: List<Pair<Word, String>> = emptyList() | ||
) { | ||
@ParameterizedTest(name = "\"{0}\" -> {1}") | ||
@MethodSource("parseWords") | ||
fun `parses words correctly`(input: String, expectedWord: Word) { | ||
expect(input.fromNotation(notation)) { | ||
feature(Word::partsList).toBe(expectedWord.partsList) | ||
} | ||
} | ||
|
||
@ParameterizedTest(name = "{1} -> \"{0}\"") | ||
@MethodSource("printWords") | ||
fun `prints words correctly`(sourceWord: Word, expectedResult: String) { | ||
expect(sourceWord) { | ||
feature(Word::toNotation, notation).toBe(expectedResult) | ||
} | ||
} | ||
|
||
@ParameterizedTest(name = "\"{0}\"") | ||
@MethodSource("unchangedWords") | ||
fun `parsing and printing a word written in this notation does not change the word`(word: String) { | ||
expect(word) { | ||
feature(String::fromNotation, notation) { | ||
feature(Word::toNotation, notation).toBe(word) | ||
} | ||
} | ||
} | ||
|
||
private fun parseWords() = asArguments(unchangedWords + parseOnlyWords) | ||
private fun printWords() = asArguments(unchangedWords.map { it.swap() } + printOnlyWords) | ||
private fun unchangedWords() = asArguments(unchangedWords) | ||
|
||
private fun asArguments(pairs: List<Pair<*, *>>) = pairs.map { | ||
Arguments.arguments( | ||
it.first, | ||
it.second | ||
) | ||
} | ||
|
||
private fun <First, Second> Pair<First, Second>.swap() = Pair(this.second, this.first) | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package de.joshuagleitze.stringnotation | ||
|
||
class JavaTypeNameTest: BaseNotationTest( | ||
notation = JavaTypeName, | ||
unchangedWords = listOf("ImInJavaTypeNotation" to Word("im", "in", "java", "type", "notation")), | ||
printOnlyWords = listOf( | ||
Word("I’m using", "Bad", "chaRacters!") to "ImusingBadCharacters", | ||
Word("1", "type", "name", "4", "you") to "TypeName4You", | ||
Word("removes", "upperCase") to "RemovesUppercase", | ||
Word("") to "__", | ||
Word("1") to "__", | ||
Word("8if") to "if_", | ||
Word("_") to "__" | ||
) | ||
) | ||
|
||
class JavaMemberNameTest: BaseNotationTest( | ||
notation = JavaMemberName, | ||
unchangedWords = listOf("imInJavaMemberNotation" to Word("im", "in", "java", "member", "notation")), | ||
printOnlyWords = listOf( | ||
Word("I’m using", "Bad", "chaRacters!") to "imusingBadCharacters", | ||
Word("1", "Member", "name", "4", "you") to "memberName4You", | ||
Word("_", "underscore", "start") to "_underscoreStart", | ||
Word("$", "dollar", "start") to "\$dollarStart", | ||
Word("a", "letter", "start") to "aLetterStart", | ||
Word("removes", "upperCase") to "removesUppercase", | ||
Word("") to "__", | ||
Word("1") to "__", | ||
Word("8if") to "if_", | ||
Word("_") to "__" | ||
) | ||
) | ||
|
||
class JavaPackagePartTest: BaseNotationTest( | ||
notation = JavaPackagePart, | ||
unchangedWords = listOf( | ||
"imapackagename" to Word("imapackagename") | ||
), | ||
parseOnlyWords = listOf( | ||
"withCamelCase" to Word("with", "camel", "case"), | ||
"with_snake_case" to Word("with", "snake", "case"), | ||
"withCamelAnd_snake_case" to Word("with", "camel", "and", "snake", "case"), | ||
"if" to Word("if") | ||
), | ||
printOnlyWords = listOf( | ||
Word("") to "__", | ||
Word("1") to "__", | ||
Word("8if") to "if_", | ||
Word("_") to "__" | ||
) | ||
) | ||
|
||
class JavaPackageNameTest: BaseNotationTest( | ||
notation = JavaPackageName, | ||
unchangedWords = listOf("i.am.a.packagename" to Word("i", "am", "a", "packagename")), | ||
parseOnlyWords = listOf( | ||
"wIth.CAPITALS" to Word("wIth", "CAPITALS"), | ||
"if.true" to Word("if", "true") | ||
), | ||
printOnlyWords = listOf( | ||
Word("if", "", "cApitAls") to "if_.__.capitals", | ||
Word("_") to "__" | ||
) | ||
|
||
) | ||
|
||
class JavaConstantNameTest: BaseNotationTest( | ||
notation = JavaConstantName, | ||
unchangedWords = listOf( | ||
"I_AM_A_CONSTANT" to Word("i", "am", "a", "constant") | ||
), | ||
parseOnlyWords = listOf( | ||
"if" to Word("if") | ||
), | ||
printOnlyWords = listOf( | ||
Word("") to "__", | ||
Word("1") to "__", | ||
Word("8if") to "IF", | ||
Word("_") to "__" | ||
) | ||
) |
Oops, something went wrong.