Skip to content

Commit 8982303

Browse files
authored
Add configurable automatic name mapping from camelCase to kebab-case (#101)
Fixes #16 * We maintain backwards compatibility by continuing to allow the `camelCase` names in addition to the `kebab-case` names during argument parsing. * When a an explicit `name = ???` is given to the `@main` or `@arg` annotation, that takes precedence over everything, and is not affected by the name mapping, * Name mapping is configurable by passing in `nameMapper = mainargs.Util.snakeCaseNameMapper` or `nameMapper = mainargs.Util.nullNameMapper` when you call `ParserForClass` or `ParserForMethods` * I had to add a whole bunch of annoying shims to maintain binary compatibility when threading the new `nameMapper` through all our method signatures. That would be resolved by a proposal like https://contributors.scala-lang.org/t/can-we-make-adding-a-parameter-with-a-default-value-binary-compatible/6132/3, which alas does not exist yet in the Scala implementation * The duplication in method argument lists is getting very annoying. Again, this would be solved by a proposal like https://contributors.scala-lang.org/t/unpacking-classes-into-method-argument-lists/6329, which still doesn't exist in the language * Bumping to 0.6.0 since we cannot maintain bincompat for Scala 3 and Scala 2 simultaneously * There is no way to continue to evolve the `case class`es that is compatible with both Scala 2 and Scala 3, due to differing method signature requirements. e.g. `def unapply(x: MyCaseClass): Option[Tuple]` vs `def unapply(x: MyCaseClass): MyCaseClass`. * The choice is either to break bincompat in Scala 2 or break bincompat in Scala 2, and I ended up choosing to do so in Scala 2 since those would have the larger slower-moving codebases with more of a concern for binary compatibility * Updated the docs and added coverage in the unit tests * I intend to release this as 0.5.5 once it lands
1 parent d7b8eaf commit 8982303

File tree

14 files changed

+668
-129
lines changed

14 files changed

+668
-129
lines changed

.github/workflows/actions.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
distribution: 'temurin'
2727
java-version: ${{ matrix.java }}
2828
- name: Run tests
29-
run: ./mill -i __.publishArtifacts + __.test
29+
run: ./mill -i -k __.publishArtifacts + __.test
3030
check-binary-compatibility:
3131
runs-on: ubuntu-latest
3232
steps:
@@ -38,7 +38,7 @@ jobs:
3838
distribution: 'temurin'
3939
java-version: 8
4040
- name: Check Binary Compatibility
41-
run: ./mill -i __.mimaReportBinaryIssues
41+
run: ./mill -i -k __.mimaReportBinaryIssues
4242

4343
publish-sonatype:
4444
if: github.repository == 'com-lihaoyi/mainargs' && startsWith(github.ref, 'refs/tags/')

.mill-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
0.11.3
1+
0.11.6
22

mainargs/src/Invoker.scala

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@ object Invoker {
55
cep: TokensReader.Class[T],
66
args: Seq[String],
77
allowPositional: Boolean,
8-
allowRepeats: Boolean
8+
allowRepeats: Boolean,
9+
): Result[T] = construct(cep, args, allowPositional, allowRepeats, Util.nullNameMapper)
10+
11+
def construct[T](
12+
cep: TokensReader.Class[T],
13+
args: Seq[String],
14+
allowPositional: Boolean,
15+
allowRepeats: Boolean,
16+
nameMapper: String => Option[String]
917
): Result[T] = {
1018
TokenGrouping
1119
.groupArgs(
1220
args,
1321
cep.main.flattenedArgSigs,
1422
allowPositional,
1523
allowRepeats,
16-
cep.main.argSigs0.exists(_.reader.isLeftover)
24+
cep.main.argSigs0.exists(_.reader.isLeftover),
25+
nameMapper
1726
)
1827
.flatMap((group: TokenGrouping[Any]) => invoke(cep.companion(), cep.main, group))
1928
}
@@ -82,7 +91,15 @@ object Invoker {
8291
mains: MethodMains[B],
8392
args: Seq[String],
8493
allowPositional: Boolean,
85-
allowRepeats: Boolean
94+
allowRepeats: Boolean): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = {
95+
runMains(mains, args, allowPositional, allowRepeats, Util.nullNameMapper)
96+
}
97+
def runMains[B](
98+
mains: MethodMains[B],
99+
args: Seq[String],
100+
allowPositional: Boolean,
101+
allowRepeats: Boolean,
102+
nameMapper: String => Option[String]
86103
): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = {
87104
def groupArgs(main: MainData[Any, B], argsList: Seq[String]) = {
88105
def invokeLocal(group: TokenGrouping[Any]) =
@@ -98,7 +115,8 @@ object Invoker {
98115
main.argSigs0.exists {
99116
case x: ArgSig => x.reader.isLeftover
100117
case _ => false
101-
}
118+
},
119+
nameMapper
102120
)
103121
.flatMap(invokeLocal)
104122
)
@@ -108,14 +126,16 @@ object Invoker {
108126
case Seq(main) => groupArgs(main, args)
109127
case multiple =>
110128
args.toList match {
111-
case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name)))
129+
case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name(nameMapper))))
112130
case head :: tail =>
113131
if (head.startsWith("-")) {
114132
Left(Result.Failure.Early.SubcommandSelectionDashes(head))
115133
} else {
116-
multiple.find(_.name == head) match {
117-
case None =>
118-
Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name), head))
134+
multiple.find{ m =>
135+
val name = m.name(nameMapper)
136+
name == head || (m.mainName.isEmpty && m.defaultName == head)
137+
} match {
138+
case None => Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name(nameMapper)), head))
119139
case Some(main) => groupArgs(main, tail)
120140
}
121141
}

0 commit comments

Comments
 (0)