Skip to content

Commit

Permalink
Add configurable automatic name mapping from camelCase to `kebab-ca…
Browse files Browse the repository at this point in the history
…se` (#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
  • Loading branch information
lihaoyi authored Jan 26, 2024
1 parent d7b8eaf commit 8982303
Show file tree
Hide file tree
Showing 14 changed files with 668 additions and 129 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
distribution: 'temurin'
java-version: ${{ matrix.java }}
- name: Run tests
run: ./mill -i __.publishArtifacts + __.test
run: ./mill -i -k __.publishArtifacts + __.test
check-binary-compatibility:
runs-on: ubuntu-latest
steps:
Expand All @@ -38,7 +38,7 @@ jobs:
distribution: 'temurin'
java-version: 8
- name: Check Binary Compatibility
run: ./mill -i __.mimaReportBinaryIssues
run: ./mill -i -k __.mimaReportBinaryIssues

publish-sonatype:
if: github.repository == 'com-lihaoyi/mainargs' && startsWith(github.ref, 'refs/tags/')
Expand Down
2 changes: 1 addition & 1 deletion .mill-version
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
0.11.3
0.11.6

36 changes: 28 additions & 8 deletions mainargs/src/Invoker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ object Invoker {
cep: TokensReader.Class[T],
args: Seq[String],
allowPositional: Boolean,
allowRepeats: Boolean
allowRepeats: Boolean,
): Result[T] = construct(cep, args, allowPositional, allowRepeats, Util.nullNameMapper)

def construct[T](
cep: TokensReader.Class[T],
args: Seq[String],
allowPositional: Boolean,
allowRepeats: Boolean,
nameMapper: String => Option[String]
): Result[T] = {
TokenGrouping
.groupArgs(
args,
cep.main.flattenedArgSigs,
allowPositional,
allowRepeats,
cep.main.argSigs0.exists(_.reader.isLeftover)
cep.main.argSigs0.exists(_.reader.isLeftover),
nameMapper
)
.flatMap((group: TokenGrouping[Any]) => invoke(cep.companion(), cep.main, group))
}
Expand Down Expand Up @@ -82,7 +91,15 @@ object Invoker {
mains: MethodMains[B],
args: Seq[String],
allowPositional: Boolean,
allowRepeats: Boolean
allowRepeats: Boolean): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = {
runMains(mains, args, allowPositional, allowRepeats, Util.nullNameMapper)
}
def runMains[B](
mains: MethodMains[B],
args: Seq[String],
allowPositional: Boolean,
allowRepeats: Boolean,
nameMapper: String => Option[String]
): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = {
def groupArgs(main: MainData[Any, B], argsList: Seq[String]) = {
def invokeLocal(group: TokenGrouping[Any]) =
Expand All @@ -98,7 +115,8 @@ object Invoker {
main.argSigs0.exists {
case x: ArgSig => x.reader.isLeftover
case _ => false
}
},
nameMapper
)
.flatMap(invokeLocal)
)
Expand All @@ -108,14 +126,16 @@ object Invoker {
case Seq(main) => groupArgs(main, args)
case multiple =>
args.toList match {
case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name)))
case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name(nameMapper))))
case head :: tail =>
if (head.startsWith("-")) {
Left(Result.Failure.Early.SubcommandSelectionDashes(head))
} else {
multiple.find(_.name == head) match {
case None =>
Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name), head))
multiple.find{ m =>
val name = m.name(nameMapper)
name == head || (m.mainName.isEmpty && m.defaultName == head)
} match {
case None => Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name(nameMapper)), head))
case Some(main) => groupArgs(main, tail)
}
}
Expand Down
Loading

0 comments on commit 8982303

Please sign in to comment.