Skip to content

Commit 6d5c781

Browse files
committed
Introduce new mill.api.opt with Opt, OptGroup, Opts and OptSyntax
1 parent b6468bf commit 6d5c781

File tree

6 files changed

+320
-0
lines changed

6 files changed

+320
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package mill.api.daemon.internal
2+
3+
trait OptsApi {
4+
def toStringSeq: Seq[String]
5+
def value: Seq[OptGroupApi]
6+
}
7+
8+
trait OptGroupApi {
9+
}
10+
11+
trait OptApi {
12+
def toString(): String
13+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package mill.api.opt
2+
3+
import mill.api.daemon.internal.OptApi
4+
import mill.api.JsonFormatters.given
5+
6+
import scala.annotation.targetName
7+
import scala.language.implicitConversions
8+
9+
case class Opt private (value: Seq[Opt.OptTypes]) extends OptApi {
10+
override def toString(): String = value.mkString("")
11+
12+
def map(conv: Opt.OptTypes => Opt.OptTypes): Opt = Opt(value.map(conv)*)
13+
14+
private def startStrings: Seq[String] =
15+
value.takeWhile(_.isInstanceOf[String]).collect { case s: String => s }
16+
17+
def startsWith(prefix: String): Boolean = startStrings.mkString("").startsWith(prefix)
18+
19+
def mapStartString(rep: String => String): Opt = {
20+
val rest = value.dropWhile(_.isInstanceOf[String])
21+
Opt((rep(startStrings.mkString("")) +: rest)*)
22+
}
23+
24+
def containsPaths: Boolean = value.exists {
25+
case _: os.Path => true
26+
case _ => false
27+
}
28+
}
29+
30+
object Opt {
31+
32+
type OptTypes = (String | os.Path)
33+
34+
@targetName("applyVarArg")
35+
def apply(value: OptTypes*): Opt = {
36+
// TODO: merge sequential strings
37+
new Opt(value.filter {
38+
case s: String if s.isEmpty => false
39+
case _ => true
40+
})
41+
}
42+
43+
/**
44+
* Constructs a path from multiple path elements and a separator string.
45+
* Can be used to render classpaths.
46+
* Each path component will still be handled properly, e.g. mapped according to the current [[MappedPaths]] mapping.
47+
*/
48+
def mkPath(paths: Seq[os.Path], prefix: String = "", sep: String, suffix: String = ""): Opt = {
49+
var needSep = false
50+
Opt(
51+
(
52+
Seq(prefix) ++
53+
paths.flatMap { path =>
54+
if (needSep)
55+
Seq(sep, path)
56+
else {
57+
needSep = true
58+
Seq(path)
59+
}
60+
} ++ Seq(suffix)
61+
)*
62+
)
63+
}
64+
65+
def mkPlatformPath(paths: Seq[os.Path]): Opt = mkPath(paths, sep = java.io.File.pathSeparator)
66+
67+
given jsonReadWriter: upickle.ReadWriter[Opt] =
68+
upickle.readwriter[Seq[(Option[String], Option[os.Path])]].bimap(
69+
_.value.map {
70+
case path: os.Path => (None, Some(path))
71+
case str: String => (Some(str), None)
72+
},
73+
seq =>
74+
Opt(seq.map {
75+
case (Some(str), _) => str
76+
case (_, Some(path)) => path
77+
}*)
78+
)
79+
80+
// given stringToOpt: Conversion[String, Opt] = (value: String) => Opt(value)
81+
// given osPathToOpt: Conversion[os.Path, Opt] = (value: os.Path) => Opt(value)
82+
83+
// implicit def IterableToOpt[T](s: Iterable[T])(using f: T => Opt): Opt =
84+
// Opt(s.toSeq.flatMap(f(_).value))
85+
86+
87+
implicit def StringToOpt(s: String): Opt = Opt(s)
88+
89+
implicit def OsPathToOpt(p: os.Path): Opt = Opt(p)
90+
91+
implicit def OptToOpt(o: Opt): Opt = o
92+
93+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package mill.api.opt
2+
3+
import mill.api.daemon.internal.OptGroupApi
4+
5+
import scala.annotation.targetName
6+
import scala.language.implicitConversions
7+
8+
/**
9+
* A set of options, which are used together
10+
*/
11+
case class OptGroup private (value: Seq[Opt]) extends OptGroupApi
12+
derives upickle.ReadWriter {
13+
override def toString(): String = value.mkString("(", ", ", ")")
14+
15+
def isEmpty: Boolean = value.isEmpty
16+
17+
def containsPaths: Boolean = value.exists(_.containsPaths)
18+
19+
def head: Opt = value.head
20+
21+
def toStringSeq: Seq[String] = value.map(_.toString())
22+
23+
def concat(suffix: OptGroup): OptGroup = new OptGroup(value ++ suffix.value)
24+
25+
@`inline` final def ++(suffix: OptGroup): OptGroup = concat(suffix)
26+
27+
}
28+
29+
object OptGroup {
30+
@targetName("applyVarAar")
31+
def apply(opts: Opt*): OptGroup = new OptGroup(opts)
32+
@targetName("applyIterable")
33+
def apply[T](opts: T*)(using f: T => Opt): OptGroup = new OptGroup(opts.map(f(_)))
34+
35+
def when(cond: Boolean)(value: Opt*): OptGroup = if (cond) OptGroup(value*) else OptGroup()
36+
37+
// given optsToOptGroup: Conversion[(OptTypes, OptTypes), OptGroup] =
38+
// (tuple: (OptTypes, OptTypes)) =>
39+
// OptGroup(Opt(tuple._1), Opt(tuple._2))
40+
41+
implicit def StringToOptGroup(s: String): OptGroup = OptGroup(Seq(Opt(s)))
42+
43+
implicit def OsPathToOptGroup(p: os.Path): OptGroup = OptGroup(Seq(Opt(p)))
44+
45+
implicit def OptToOptGroup(o: Opt): OptGroup = OptGroup(Seq(o))
46+
47+
implicit def IterableToOptGroup[T](s: Iterable[T])(using f: T => OptGroup): OptGroup =
48+
OptGroup(s.toSeq.flatMap(f(_).value))
49+
50+
// implicit def ArrayToOptGroup[T](s: Array[T])(using f: T => OptGroup): OptGroup =
51+
// OptGroup(s.flatMap(f(_).value))
52+
53+
54+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package mill.api.opt
2+
3+
import scala.language.implicitConversions
4+
5+
implicit class OptSyntax(ctx: StringContext) extends AnyVal {
6+
def opt(opts: Any*): Opt = {
7+
val vals = ctx.parts.take(opts.length).zip(opts).flatMap { case (p, a) => Seq(p, a) } ++
8+
ctx.parts.drop(opts.length)
9+
10+
val elems: Seq[(String | os.Path)] = vals.flatMap {
11+
case path: os.Path => Seq(path)
12+
case s => Seq(s.toString).filter(_.nonEmpty)
13+
}
14+
Opt(elems*)
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package mill.api.opt
2+
3+
import mill.api.daemon.internal.OptsApi
4+
5+
import scala.language.implicitConversions
6+
7+
case class Opts private (override val value: OptGroup*) extends OptsApi
8+
derives upickle.ReadWriter {
9+
10+
def toStringSeq: Seq[String] = value.flatMap(_.toStringSeq)
11+
override def toString(): String = value.mkString("Opts(", ", ", ")")
12+
13+
def concat(suffix: Opts): Opts = Opts(value ++ suffix.value)
14+
@`inline` final def ++(suffix: Opts): Opts = concat(suffix)
15+
16+
def containsPaths: Boolean = value.exists(_.containsPaths)
17+
18+
def isEmpty: Boolean = value.isEmpty
19+
def filterGroup(pred: OptGroup => Boolean): Opts = Opts(value.filter(pred)*)
20+
def mapGroup(f: OptGroup => OptGroup): Opts = Opts(value.map(f)*)
21+
def flatMap(f: OptGroup => Seq[OptGroup]): Opts = Opts(value.flatMap(f)*)
22+
}
23+
24+
object Opts {
25+
def apply(value: OptGroup*): Opts = new Opts(value.filter(!_.isEmpty))
26+
// @targetName("applyUnion")
27+
// def apply(value: (Opt | OptGroup | Seq[Opt])*): Opts = Opts(value.flatMap {
28+
// case a: Opt => Seq(OptGroup(a))
29+
// case a: OptGroup => Seq(a)
30+
// case s: Iterable[Opt] => s.map(OptGroup(_))
31+
// })
32+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package mill.api.opt
2+
3+
import utest.*
4+
import mill.api.opt.*
5+
6+
class OptsTests extends TestSuite {
7+
8+
val homeDir = os.home
9+
val workDir = homeDir / "work"
10+
val outDir = workDir / "out"
11+
12+
val srcDir1 = workDir / "src1"
13+
val srcDir2 = workDir / "src2"
14+
val sources1 = Seq(srcDir1, srcDir2)
15+
16+
val srcDir3 = workDir / "src3"
17+
val srcDir4 = workDir / "src4"
18+
val sources2 = Seq(srcDir3, srcDir4)
19+
20+
val plugin1 = homeDir / ".cache" / "plugin1"
21+
22+
val opts1 = Opts(
23+
// single arg
24+
Opt("-deprecation"),
25+
// implicit single args
26+
"-verbose",
27+
// two args as group
28+
OptGroup("--release", "17"),
29+
// an option including a file via ArgParts
30+
Opt("-Xplugin=", plugin1),
31+
// an option including a file via arg string interpolator
32+
opt"-Xplugin:${plugin1}",
33+
// some files
34+
sources1,
35+
// some files as ArgGroup
36+
OptGroup(sources2*),
37+
// Mixed ArgGroup
38+
OptGroup(opt"--extra", opt"-Xplugin=${plugin1}") ++ OptGroup(sources1*)
39+
)
40+
41+
val expectedOpts1 = Opts(
42+
Opt("-deprecation"),
43+
Opt("-verbose"),
44+
OptGroup(
45+
Opt("--release"),
46+
Opt("17")
47+
),
48+
Opt("-Xplugin=", plugin1),
49+
Opt("-Xplugin:", plugin1),
50+
Opt(srcDir1),
51+
Opt(srcDir2),
52+
Opt(srcDir3),
53+
Opt(srcDir4),
54+
OptGroup(
55+
Opt("--extra"),
56+
Opt("-Xplugin=", plugin1),
57+
Opt(srcDir1),
58+
Opt(srcDir2)
59+
)
60+
)
61+
62+
val expectedSeq1 = Seq(
63+
"-deprecation",
64+
"-verbose",
65+
"--release",
66+
"17",
67+
s"-Xplugin=${plugin1.toString()}",
68+
// an option including a file via arg string interpolator
69+
s"-Xplugin:${plugin1.toString()}"
70+
) ++
71+
sources1.map(_.toString())
72+
++
73+
sources2.map(_.toString())
74+
++
75+
Seq(
76+
"--extra",
77+
s"-Xplugin=${plugin1.toString()}"
78+
) ++
79+
sources1.map(_.toString())
80+
81+
override def tests: Tests = Tests {
82+
test("structure") {
83+
assert(opts1 == expectedOpts1)
84+
}
85+
test("toStringSeq") {
86+
val str = opts1.toStringSeq
87+
assert(str == expectedSeq1)
88+
}
89+
test("jsonify") {
90+
test("without-mapping") {
91+
val json = upickle.write(opts1)
92+
assertGoldenLiteral(
93+
json,
94+
"{\"value\":[{\"value\":[[[\"-deprecation\",null]],[[\"-verbose\",null]],[[\"--release\",null]],[[\"17\",null]],[[\"-Xplugin=\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[\"-Xplugin:\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[null,\"/home/lefou/work/src1\"]],[[null,\"/home/lefou/work/src2\"]],[[null,\"/home/lefou/work/src3\"]],[[null,\"/home/lefou/work/src4\"]],[[\"--extra\",null]],[[\"-Xplugin=\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[null,\"/home/lefou/work/src1\"]],[[null,\"/home/lefou/work/src2\"]]]}]}"
95+
)
96+
assert(json.split("\\Q$HOME\\E").size == 1)
97+
val back = upickle.read[Opts](json)
98+
assert(opts1 == back)
99+
}
100+
// test("with-mapping-home") {
101+
// val json = upickle.write(opts1)
102+
// assertGoldenLiteral(
103+
// json,
104+
// "{\"value\":[{\"value\":[[[\"-deprecation\",null]],[[\"-verbose\",null]],[[\"--release\",null]],[[\"17\",null]],[[\"-Xplugin=\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[\"-Xplugin:\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[null,\"/home/lefou/work/src1\"]],[[null,\"/home/lefou/work/src2\"]],[[null,\"/home/lefou/work/src3\"]],[[null,\"/home/lefou/work/src4\"]],[[\"--extra\",null]],[[\"-Xplugin=\",null],[null,\"/home/lefou/.cache/plugin1\"]],[[null,\"/home/lefou/work/src1\"]],[[null,\"/home/lefou/work/src2\"]]]}]}"
105+
// )
106+
// assert(json.split("\\Q$HOME\\E").size == 10)
107+
// val back = upickle.read[Opts](json)
108+
// assert(opts1 == back)
109+
// }
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)