diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6471eae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +on: + push: + branches: + - master + pull_request: + branches: + - master + + +jobs: + ci: + strategy: + matrix: + jvm: + - temurin:1.8.0-422 + - temurin:1.21 + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6 + - uses: coursier/setup-action@v1 + with: + jvm: ${{ matrix.jvm }} + apps: sbt + - name: Run tests + run: sbt +test diff --git a/.gitignore b/.gitignore index d9adc9c..f2a4aae 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ lib_managed/ src_managed/ project/boot/ project/plugins/project/ +/.bsp # Scala-IDE specific .scala_dependencies diff --git a/build.sbt b/build.sbt index 625368e..d813926 100644 --- a/build.sbt +++ b/build.sbt @@ -1,21 +1,22 @@ // shadow sbt-scalajs' crossProject and CrossType from Scala.js 0.6.x import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} -val Scala2_11 = "2.11.12" -val Scala2_12 = "2.12.10" -val Scala2_13 = "2.13.1" -val FastParse = "1.0.1" -val Shapeless = "2.3.3" -val ScalaCheck = "1.14.2" -val ScalaTest = "3.0.8" -val ScalaTestNative = "3.2.0-SNAP10" +val Scala2_12 = "2.12.19" +val Scala2_13 = "2.13.14" +val Scala3 = "3.3.3" +val FastParse = "3.1.1" +val Shapeless = "2.3.12" +val ScalaCheck = "1.18.1" +val ScalaTest = "3.2.19" + +val ScalaTestScalaCheck = s"$ScalaTest.0" val SharedSettings = Seq( name := "toml-scala", organization := "tech.sparse", scalaVersion := Scala2_13, - crossScalaVersions := Seq(Scala2_13, Scala2_12, Scala2_11), + crossScalaVersions := Seq(Scala3, Scala2_13, Scala2_12), pomExtra := https://github.com/sparsetech/toml-scala @@ -40,7 +41,7 @@ val SharedSettings = Seq( lazy val root = project.in(file(".")) .aggregate(toml.js, toml.jvm, toml.native) .settings(SharedSettings: _*) - .settings(skip in publish := true) + .settings(publish / skip := true) lazy val toml = crossProject(JSPlatform, JVMPlatform, NativePlatform) @@ -48,29 +49,15 @@ lazy val toml = .settings(SharedSettings) .settings( libraryDependencies ++= Seq( - "org.scalameta" %%% "fastparse" % FastParse, - "com.chuusai" %%% "shapeless" % Shapeless - ) - ) - .jsSettings( - libraryDependencies ++= Vector( - "org.scalacheck" %%% "scalacheck" % ScalaCheck % "test", - "org.scalatest" %%% "scalatest" % ScalaTest % "test" - ) - ) - .jvmSettings( - libraryDependencies ++= Vector( - "org.scalacheck" %% "scalacheck" % ScalaCheck % "test", - "org.scalatest" %% "scalatest" % ScalaTest % "test" - ) - ) - .nativeSettings( - scalaVersion := Scala2_11, - crossScalaVersions := Seq(Scala2_11), - // See https://github.com/scalalandio/chimney/issues/78#issuecomment-419705142 - nativeLinkStubs := true, - libraryDependencies ++= Vector( - "org.scalatest" %%% "scalatest" % ScalaTestNative % "test" + "com.lihaoyi" %%% "fastparse" % FastParse, + "org.scalacheck" %%% "scalacheck" % ScalaCheck % Test, + "org.scalatest" %%% "scalatest" % ScalaTest % Test, + "org.scalatestplus" %%% s"scalacheck-${ScalaCheck.split('.').take(2).mkString("-")}" % ScalaTestScalaCheck % Test ), - excludeFilter in Test := "*GeneratedSpec*" || "*Generators*" + libraryDependencies ++= { + if(scalaVersion.value.startsWith("3.")) + Seq.empty + else + Seq("com.chuusai" %%% "shapeless" % Shapeless) + } ) diff --git a/js/src/main/scala/toml/PlatformCodecs.scala b/js-native/src/main/scala-2/toml/PlatformCodecs.scala similarity index 100% rename from js/src/main/scala/toml/PlatformCodecs.scala rename to js-native/src/main/scala-2/toml/PlatformCodecs.scala diff --git a/js-native/src/main/scala/toml/PlatformRules.scala b/js-native/src/main/scala/toml/PlatformRules.scala new file mode 100644 index 0000000..3e54e75 --- /dev/null +++ b/js-native/src/main/scala/toml/PlatformRules.scala @@ -0,0 +1,7 @@ +package toml + +import fastparse._ + +trait PlatformRules { + def date[$: P] = P(CharIn()).map(_ => null.asInstanceOf[Value]) +} diff --git a/js/src/main/scala/toml/PlatformValue.scala b/js-native/src/main/scala/toml/PlatformValue.scala similarity index 100% rename from js/src/main/scala/toml/PlatformValue.scala rename to js-native/src/main/scala/toml/PlatformValue.scala diff --git a/js/src/main/scala/toml/PlatformRules.scala b/js/src/main/scala/toml/PlatformRules.scala deleted file mode 100644 index 6bfe4d2..0000000 --- a/js/src/main/scala/toml/PlatformRules.scala +++ /dev/null @@ -1,7 +0,0 @@ -package toml - -import scala.meta.internal.fastparse.all._ - -trait PlatformRules { - val date = StringIn().map(_ => null.asInstanceOf[Value]) -} diff --git a/jvm/src/main/scala/toml/PlatformCodecs.scala b/jvm/src/main/scala-2/toml/PlatformCodecs.scala similarity index 100% rename from jvm/src/main/scala/toml/PlatformCodecs.scala rename to jvm/src/main/scala-2/toml/PlatformCodecs.scala diff --git a/jvm/src/main/scala/toml/PlatformRules.scala b/jvm/src/main/scala/toml/PlatformRules.scala index 94bfb12..98f2f32 100644 --- a/jvm/src/main/scala/toml/PlatformRules.scala +++ b/jvm/src/main/scala/toml/PlatformRules.scala @@ -2,13 +2,14 @@ package toml import java.time._ -import scala.meta.internal.fastparse.all._ +import fastparse._ +import fastparse.NoWhitespace._ trait PlatformRules { this: Rules => private val TenPowers = List(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000) - val localTime: Parser[Value.Time] = P( + def localTime[$: P]: P[Value.Time] = P( digit.rep(2).! ~ ":" ~ digit.rep(2).! ~ ":" ~ digit.rep(2).! ~ ("." ~ digit.rep.!).? ).map { case (h, m, s, ns) => val nano = ns.map { str => @@ -19,24 +20,24 @@ trait PlatformRules { this: Rules => Value.Time(LocalTime.of(h.toInt, m.toInt, s.toInt, nano)) } - val localDate: Parser[Value.Date] = P( + def localDate[$: P]: P[Value.Date] = P( digit.rep(4).! ~ "-" ~ digit.rep(2).! ~ "-" ~ digit.rep(2).! ).map { case (y, m, d) => Value.Date(LocalDate.of(y.toInt, m.toInt, d.toInt)) } - val localDateTime: Parser[Value.DateTime] = P( + def localDateTime[$: P]: P[Value.DateTime] = P( localDate ~ "T" ~ localTime ).map { case (date, time) => Value.DateTime(LocalDateTime.of(date.value, time.value)) } - val offsetDateTime: Parser[Value.OffsetDateTime] = P( + def offsetDateTime[$: P]: P[Value.OffsetDateTime] = P( localDateTime ~ ("Z" | (("-" | "+") ~ digit.rep(2) ~ ":" ~ digit.rep(2))).! ).map { case (dateTime, offset) => Value.OffsetDateTime( OffsetDateTime.of(dateTime.value, ZoneOffset.of(offset))) } - val date = P(offsetDateTime | localDateTime | localDate | localTime) + def date[$: P] = P(offsetDateTime | localDateTime | localDate | localTime) } diff --git a/jvm/src/test/scala-2/toml/DateSpec.scala b/jvm/src/test/scala-2/toml/DateSpec.scala new file mode 100644 index 0000000..962461f --- /dev/null +++ b/jvm/src/test/scala-2/toml/DateSpec.scala @@ -0,0 +1,20 @@ +package toml + +import java.time._ + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import fastparse.Parsed._ + +class DateSpec extends AnyFunSuite with Matchers { + import TestHelpers._ + + test("Codec derivation") { + import Codecs._ + + case class Root(ld: LocalDate) + + val toml = "ld = 1979-05-27" + assert(Toml.parseAs[Root](toml) == Right(Root(LocalDate.of(1979, 5, 27)))) + } +} diff --git a/jvm/src/test/scala/toml/DateGenSpec.scala b/jvm/src/test/scala/toml/DateGenSpec.scala index 837f652..a569142 100644 --- a/jvm/src/test/scala/toml/DateGenSpec.scala +++ b/jvm/src/test/scala/toml/DateGenSpec.scala @@ -1,26 +1,27 @@ package toml import org.scalatest.prop._ -import org.scalatest.{Matchers, PropSpec} +import org.scalatest.propspec.AnyPropSpec +import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks -import scala.meta.internal.fastparse.all._ -import scala.meta.internal.fastparse.core.Parsed.{Failure, Success} +import fastparse._ +import fastparse.Parsed.{Failure, Success} -class DateGenSpec extends PropSpec with ScalaCheckPropertyChecks with Matchers { +class DateGenSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matchers { import TestHelpers._ property("parse dates following the RFC 3339 spec (`date` parser)") { import Generators.Dates._ forAll(dateFormatGen) { s => - shouldBeSuccess(Rules.offsetDateTime.parse(s)) + shouldBeSuccess(parse(s, Rules.offsetDateTime(_))) } } property("parse dates following the RFC 3339 spec") { import Generators.Dates._ forAll(dateFormatGen) { s => - shouldBeSuccess(Rules.elem.parse(s)) + shouldBeSuccess(parse(s, Rules.elem(_))) } } } diff --git a/jvm/src/test/scala/toml/DateSpec.scala b/jvm/src/test/scala/toml/DateSpec.scala deleted file mode 100644 index d307db6..0000000 --- a/jvm/src/test/scala/toml/DateSpec.scala +++ /dev/null @@ -1,77 +0,0 @@ -package toml - -import java.time._ - -import org.scalatest.{FunSuite, Matchers} -import scala.meta.internal.fastparse.core.Parsed._ - -class DateSpec extends FunSuite with Matchers { - import TestHelpers._ - - test("Parse local date") { - val toml = "ld = 1979-05-27" - val nodes = testSuccess(toml).nodes - assert(nodes(0) == Node.Pair("ld", Value.Date(LocalDate.of(1979, 5, 27)))) - } - - test("Parse local time") { - val toml = - """ - |lt1 = 07:32:00 - |lt2 = 00:32:00.999999 - |lt3 = 00:32:00.555 - """.stripMargin - - val nodes = testSuccess(toml).nodes - assert(nodes(0) == Node.Pair("lt1", Value.Time(LocalTime.of(7, 32, 0, 0)))) - assert(nodes(1) == Node.Pair("lt2", Value.Time(LocalTime.of(0, 32, 0, 999999000)))) - assert(nodes(2) == Node.Pair("lt3", Value.Time(LocalTime.of(0, 32, 0, 555000000)))) - } - - test("Parse local date time") { - val toml = - """ - |ldt1 = 1979-05-27T07:32:00 - |ldt2 = 1979-05-27T00:32:00.999999 - """.stripMargin - val nodes = testSuccess(toml).nodes - assert(nodes(0) == Node.Pair("ldt1", Value.DateTime(LocalDateTime.of( - LocalDate.of(1979, 5, 27), LocalTime.of(7, 32, 0, 0))))) - assert(nodes(1) == Node.Pair("ldt2", Value.DateTime(LocalDateTime.of( - LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0, 999999000))))) - } - - test("Parse offset date time") { - val toml = - """ - |odt1 = 1979-05-27T07:32:00Z - |odt2 = 1979-05-27T00:32:00-07:00 - |odt3 = 1979-05-27T00:32:00.999999-07:00 - """.stripMargin - val nodes = testSuccess(toml).nodes - assert(nodes(0) == Node.Pair("odt1", Value.OffsetDateTime( - OffsetDateTime.of( - LocalDateTime.of( - LocalDate.of(1979, 5, 27), LocalTime.of(7, 32, 0) - ), ZoneOffset.of("Z"))))) - assert(nodes(1) == Node.Pair("odt2", Value.OffsetDateTime( - OffsetDateTime.of( - LocalDateTime.of( - LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0) - ), ZoneOffset.of("-07:00"))))) - assert(nodes(2) == Node.Pair("odt3", Value.OffsetDateTime( - OffsetDateTime.of( - LocalDateTime.of( - LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0, 999999000) - ), ZoneOffset.of("-07:00"))))) - } - - test("Codec derivation") { - import Codecs._ - - case class Root(ld: LocalDate) - - val toml = "ld = 1979-05-27" - assert(Toml.parseAs[Root](toml) == Right(Root(LocalDate.of(1979, 5, 27)))) - } -} diff --git a/jvm/src/test/scala/toml/FileSpec.scala b/jvm/src/test/scala/toml/FileSpec.scala index dbc9258..8044945 100644 --- a/jvm/src/test/scala/toml/FileSpec.scala +++ b/jvm/src/test/scala/toml/FileSpec.scala @@ -3,10 +3,11 @@ package toml import java.time._ import java.io.File -import org.scalatest.{FunSuite, Matchers} -import scala.meta.internal.fastparse.core.Parsed._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.funsuite.AnyFunSuite +import fastparse.Parsed._ -class FileSpec extends FunSuite with Matchers { +class FileSpec extends AnyFunSuite with Matchers { import TestHelpers._ test("Parse multi-line strings") { diff --git a/native/src/main/scala/toml/PlatformCodecs.scala b/native/src/main/scala/toml/PlatformCodecs.scala deleted file mode 100644 index a07db60..0000000 --- a/native/src/main/scala/toml/PlatformCodecs.scala +++ /dev/null @@ -1,3 +0,0 @@ -package toml - -trait PlatformCodecs diff --git a/native/src/main/scala/toml/PlatformRules.scala b/native/src/main/scala/toml/PlatformRules.scala deleted file mode 100644 index 6bfe4d2..0000000 --- a/native/src/main/scala/toml/PlatformRules.scala +++ /dev/null @@ -1,7 +0,0 @@ -package toml - -import scala.meta.internal.fastparse.all._ - -trait PlatformRules { - val date = StringIn().map(_ => null.asInstanceOf[Value]) -} diff --git a/native/src/main/scala/toml/PlatformValue.scala b/native/src/main/scala/toml/PlatformValue.scala deleted file mode 100644 index 87be6e9..0000000 --- a/native/src/main/scala/toml/PlatformValue.scala +++ /dev/null @@ -1,3 +0,0 @@ -package toml - -trait PlatformValue diff --git a/project/build.properties b/project/build.properties index dca663d..be54e77 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.2.8 +sbt.version = 1.10.0 diff --git a/project/plugins.sbt b/project/plugins.sbt index 5040e26..a3b9603 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ logLevel := Level.Warn -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.1") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.6.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.29") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.9") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") diff --git a/shared/src/main/scala/toml/Codecs.scala b/shared/src/main/scala-2/toml/Codecs.scala similarity index 100% rename from shared/src/main/scala/toml/Codecs.scala rename to shared/src/main/scala-2/toml/Codecs.scala diff --git a/shared/src/main/scala-2/toml/TomlVersionSpecific.scala b/shared/src/main/scala-2/toml/TomlVersionSpecific.scala new file mode 100644 index 0000000..025166c --- /dev/null +++ b/shared/src/main/scala-2/toml/TomlVersionSpecific.scala @@ -0,0 +1,54 @@ +package toml + +import shapeless._ + +trait TomlVersionSpecific { + + def parse(toml: String, extensions: Set[Extension] = Set()): Either[Parse.Error, Value.Tbl] + + class CodecHelperGeneric[A] { + def apply[D <: HList, R <: HList](table: Value.Tbl)(implicit + generic: LabelledGeneric.Aux[A, R], + defaults: Default.AsRecord.Aux[A, D], + defaultMapper: util.RecordToMap[D], + codec: Codec[R] + ): Either[Parse.Error, A] = { + val d = defaultMapper(defaults()) + codec(table, d, 0).right.map(generic.from) + } + + def apply[D <: HList, R <: HList]( + toml: String, + extensions: Set[Extension] + )(implicit + generic: LabelledGeneric.Aux[A, R], + defaults: Default.AsRecord.Aux[A, D], + defaultMapper: util.RecordToMap[D], + codec: Codec[R] + ): Either[Parse.Error, A] = { + val d = defaultMapper(defaults()) + parse(toml, extensions).right + .flatMap(codec(_, d, 0).right.map(generic.from)) + } + + def apply[D <: HList, R <: HList](toml: String)(implicit + generic: LabelledGeneric.Aux[A, R], + defaults: Default.AsRecord.Aux[A, D], + defaultMapper: util.RecordToMap[D], + codec: Codec[R] + ): Either[Parse.Error, A] = apply(toml, Set()) + } + + class CodecHelperValue[A] { + def apply(value: Value)(implicit codec: Codec[A]): Either[Parse.Error, A] = + codec(value, Map(), 0) + + def apply(toml: String, extensions: Set[Extension] = Set())(implicit + codec: Codec[A] + ): Either[Parse.Error, A] = + parse(toml, extensions).right.flatMap(codec(_, Map(), 0)) + } + + def parseAs[T]: CodecHelperGeneric[T] = new CodecHelperGeneric[T] + def parseAsValue[T]: CodecHelperValue[T] = new CodecHelperValue[T] +} diff --git a/shared/src/main/scala/toml/util/RecordToMap.scala b/shared/src/main/scala-2/toml/util/RecordToMap.scala similarity index 100% rename from shared/src/main/scala/toml/util/RecordToMap.scala rename to shared/src/main/scala-2/toml/util/RecordToMap.scala diff --git a/shared/src/main/scala-3/toml/TomlVersionSpecific.scala b/shared/src/main/scala-3/toml/TomlVersionSpecific.scala new file mode 100644 index 0000000..abb796f --- /dev/null +++ b/shared/src/main/scala-3/toml/TomlVersionSpecific.scala @@ -0,0 +1,3 @@ +package toml + +trait TomlVersionSpecific diff --git a/shared/src/main/scala/toml/Constants.scala b/shared/src/main/scala/toml/Constants.scala index 9291ffb..8aa01d9 100644 --- a/shared/src/main/scala/toml/Constants.scala +++ b/shared/src/main/scala/toml/Constants.scala @@ -1,20 +1,20 @@ package toml object Constants { - val SingleQuote = '\'' - val DoubleQuote = '"' + final val SingleQuote = '\'' + final val DoubleQuote = '"' - val MultiLineSingleQuote = "'''" - val MultiLineDoubleQuote = "\"\"\"" + final val MultiLineSingleQuote = "'''" + final val MultiLineDoubleQuote = "\"\"\"" - val NumbersRange = '0' to '9' - val LettersRange = ('a' to 'z') ++ ('A' to 'Z') + final val NumbersRange = "0-9" + final val LettersRange = "a-zA-Z" - val Dashes = Set('-', '_') + final val Dashes = "\\-_" - val Lf = "\n" - val CrLf = "\r\n" + final val Lf = "\n" + final val CrLf = "\r\n" - val WhitespaceChars = Set(' ', '\t') - val EscapeChars = Set('\b', '\t', '\n', '\f', '\r', '"', '\\') + final val WhitespaceChars = " \t" + final val EscapeChars = "\b\t\n\f\r\"\\" } diff --git a/shared/src/main/scala/toml/Rules.scala b/shared/src/main/scala/toml/Rules.scala index eedae83..4cdec32 100644 --- a/shared/src/main/scala/toml/Rules.scala +++ b/shared/src/main/scala/toml/Rules.scala @@ -1,6 +1,7 @@ package toml -import scala.meta.internal.fastparse.all._ +import fastparse._ +import NoWhitespace._ private[toml] case class NamedFunction[T, V](f: T => V, name: String) extends (T => V) @@ -22,54 +23,55 @@ class Rules(extensions: Set[Extension]) extends PlatformRules { val UntilNewline = NamedFunction(!CrLf.contains(_: Char), "UntilNewline") - val newLine = P(StringIn(CrLf, Lf)) - val charsChunk = P(CharsWhile(UntilNewline)) - val comment = P("#" ~ charsChunk.? ~ &(newLine | End)) - val whitespace = P(CharIn(WhitespaceChars.toList)) + def newLine[$: P] = P(StringIn(CrLf, Lf)) + def charsChunk[$: P] = P(CharsWhile(UntilNewline)) + def comment[$: P] = P("#" ~ charsChunk.? ~ &(newLine | End)) + def whitespace[$: P] = P(CharIn(WhitespaceChars)) - val skip = P(NoCut(NoTrace((whitespace | comment | newLine).rep))) - val skipWs = P(NoCut(NoTrace(whitespace.rep))) + def skip[$: P] = P(NoCut(NoTrace((whitespace | comment | newLine).rep))) + def skipWs[$: P] = P(NoCut(NoTrace(whitespace.rep))) - val letter = P(CharIn(LettersRange)) - val digit = P(CharIn(NumbersRange)) - val digits = P(digit.rep(1)) - val dash = P(CharIn(Dashes.toList)) + def letter[$: P] = P(CharIn(LettersRange)) + def digit[$: P] = P(CharIn(NumbersRange)) + def digits[$: P] = P(digit.rep(1)) + def dash[$: P] = P(CharIn(Dashes)) val StringChars = NamedFunction(!"\"\\".contains(_: Char), "StringChars") - val strChars = P(CharsWhile(StringChars)) + def strChars[$: P] = P(CharsWhile(StringChars)) - val hexDigit = P(CharIn('0' to '9', 'a' to 'f', 'A' to 'F')) - val unicodeEsc = P("u" ~ hexDigit.rep(4)) - val unicodeEscLong = P("U" ~ hexDigit.rep(8)) - val escape = P("\\" ~ ( - CharIn("\"/\\bfnrt") | unicodeEsc | unicodeEscLong + + def hexDigit[$: P] = P(CharIn("0-9", "a-f", "A-F")) + def unicodeEsc[$: P] = P("u" ~ hexDigit.rep(4)) + def unicodeEscLong[$: P] = P("U" ~ hexDigit.rep(8)) + def escape[$: P] = P("\\" ~ ( + CharIn("\"/\\\\bfnrt") | unicodeEsc | unicodeEscLong )) - val basicStr: Parser[Value.Str] = - P(DoubleQuote.toString ~/ (strChars | escape).rep.! ~ DoubleQuote.toString) + def basicStr[$: P]: P[Value.Str] = + P(DoubleQuote.toString ~/ (escape | strChars).rep.! ~ DoubleQuote.toString) .map(str => Value.Str(Unescape.unescapeJavaString(str))) - val literalStr: Parser[Value.Str] = + def literalStr[$: P]: P[Value.Str] = P( SingleQuote.toString ~/ (!SingleQuote.toString ~ AnyChar).rep.! ~ SingleQuote.toString - ).map(Value.Str) - val multiLineBasicStr: Parser[Value.Str] = + ).map(Value.Str.apply) + def multiLineBasicStr[$: P]: P[Value.Str] = P( MultiLineDoubleQuote ~/ newLine.? ~ (!MultiLineDoubleQuote ~ AnyChar).rep.! ~ MultiLineDoubleQuote ).map(str => Value.Str(Unescape.unescapeJavaString(str))) - val multiLineLiteralStr: Parser[Value.Str] = + def multiLineLiteralStr[$: P]: P[Value.Str] = P( MultiLineSingleQuote ~/ newLine.? ~ (!MultiLineSingleQuote ~ AnyChar).rep.! ~ MultiLineSingleQuote - ).map(Value.Str) + ).map(Value.Str.apply) - val string: Parser[Value.Str] = P( + def string[$: P]: P[Value.Str] = P( multiLineBasicStr | multiLineLiteralStr | basicStr | @@ -77,13 +79,13 @@ class Rules(extensions: Set[Extension]) extends PlatformRules { def rmUnderscore(s: String) = s.replace("_", "") - val sign = P(CharIn("+-")) - val integral = P(digits.rep(min = 1, sep = "_")) - val fractional = P("." ~ integral) - val exponent = P(CharIn("eE") ~ sign.? ~ integral) - val integer: Parser[Value.Num] = + def sign[$: P] = P(CharIn("+\\-")) + def integral[$: P] = P(digits.rep(min = 1, sep = "_")) + def fractional[$: P] = P("." ~ integral) + def exponent[$: P] = P(CharIn("eE") ~ sign.? ~ integral) + def integer[$: P]: P[Value.Num] = P(sign.? ~ integral).!.map(s => Value.Num(rmUnderscore(s).toLong)) - val double: Parser[Value.Real] = + def double[$: P]: P[Value.Real] = P( sign.?.! ~ ( @@ -99,46 +101,46 @@ class Rules(extensions: Set[Extension]) extends PlatformRules { if (sign == "-") Value.Real(-value) else Value.Real(value) } - val `true` = P("true") .map(_ => Value.Bool(true)) - val `false` = P("false").map(_ => Value.Bool(false)) - val boolean = P(`true` | `false`) + def `true`[$: P] = P("true") .map(_ => Value.Bool(true)) + def `false`[$: P] = P("false").map(_ => Value.Bool(false)) + def boolean[$: P] = P(`true` | `false`) - val bareKey = P((letter | digit | dash).rep(min = 1)).! - val validKey: Parser[String] = + def bareKey[$: P] = P((letter | digit | dash).rep(1)).! + def validKey[$: P]: P[String] = P(NoCut(basicStr.map(_.value)) | NoCut(literalStr.map(_.value)) | bareKey) - val pair: Parser[(String, Value)] = + def pair[$: P]: P[(String, Value)] = P(validKey ~ skipWs ~ "=" ~ skipWs ~ elem) - val array: Parser[Value.Arr] = + def array[$: P]: P[Value.Arr] = P("[" ~ skip ~ elem.rep(sep = skip ~ "," ~ skip) ~ ",".? ~ skip ~ "]") .map(l => Value.Arr(l.toList)) - val inlineTable: Parser[Value.Tbl] = + def inlineTable[$: P]: P[Value.Tbl] = (if (extensions.contains(MultiLineInlineTables)) P("{" ~ skip ~ pair.rep(sep = skip ~ "," ~ skip) ~ ",".? ~ skip ~ "}") else P("{" ~ skipWs ~ pair.rep(sep = skipWs ~ "," ~ skipWs) ~ skipWs ~ "}") ).map(p => Value.Tbl(p.toMap)) - val tableIds: Parser[Seq[String]] = + def tableIds[$: P]: P[Seq[String]] = P(validKey.rep(min = 1, sep = skipWs ~ "." ~ skipWs).map(_.toSeq)) - val tableDef: Parser[Seq[String]] = + def tableDef[$: P]: P[Seq[String]] = P("[" ~ skipWs ~ tableIds ~ skipWs ~ "]") - val tableArrayDef: Parser[Seq[String]] = + def tableArrayDef[$: P]: P[Seq[String]] = P("[[" ~ skipWs ~ tableIds ~ skipWs ~ "]]") - val pairNode: Parser[Node.Pair] = pair.map { case (k, v) => Node.Pair(k, v) } - val table: Parser[Node.NamedTable] = + def pairNode[$: P]: P[Node.Pair] = pair.map { case (k, v) => Node.Pair(k, v) } + def table[$: P]: P[Node.NamedTable] = P(tableDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) => Node.NamedTable(a.toList, b.toList) } - val tableArray: Parser[Node.NamedArray] = + def tableArray[$: P]: P[Node.NamedArray] = P(tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) => Node.NamedArray(a.toList, b.toList) } - lazy val elem: Parser[Value] = + def elem[$: P]: P[Value] = P(date | string | boolean | double | integer | array | inlineTable) - val node: Parser[Node] = P(pairNode | table | tableArray) - val root: Parser[Root] = P(skip ~ node.rep(sep = skip) ~ skip ~ End) + def node[$: P]: P[Node] = P(pairNode | table | tableArray) + def root[$: P]: P[Root] = P(skip ~ node.rep(sep = skip) ~ skip ~ End) .map(nodes => Root(nodes.toList)) } diff --git a/shared/src/main/scala/toml/Toml.scala b/shared/src/main/scala/toml/Toml.scala index 751ca62..ba7f7f0 100644 --- a/shared/src/main/scala/toml/Toml.scala +++ b/shared/src/main/scala/toml/Toml.scala @@ -1,62 +1,13 @@ package toml -import shapeless._ +import fastparse.Parsed -import scala.meta.internal.fastparse.core.Parsed._ - -object Toml { +object Toml extends TomlVersionSpecific { def parse(toml: String, extensions: Set[Extension] = Set()): Either[Parse.Error, Value.Tbl] = - new Rules(extensions).root.parse(toml) match { - case Success(v, _) => Embed.root(v) - case f: Failure[_, _] => Left(List() -> f.msg) + fastparse.parse(toml, new Rules(extensions).root(_)) match { + case Parsed.Success(v, _) => Embed.root(v) + case f: Parsed.Failure => Left(List() -> f.msg) } def generate(root: Root): String = Generate.generate(root) - - class CodecHelperGeneric[A] { - def apply[D <: HList, R <: HList](table: Value.Tbl)(implicit - generic : LabelledGeneric.Aux[A, R], - defaults : Default.AsRecord.Aux[A, D], - defaultMapper: util.RecordToMap[D], - codec : Codec[R] - ): Either[Parse.Error, A] = { - val d = defaultMapper(defaults()) - codec(table, d, 0).right.map(generic.from) - } - - def apply[D <: HList, R <: HList]( - toml : String, - extensions: Set[Extension] - )(implicit - generic : LabelledGeneric.Aux[A, R], - defaults : Default.AsRecord.Aux[A, D], - defaultMapper: util.RecordToMap[D], - codec : Codec[R] - ): Either[Parse.Error, A] = { - val d = defaultMapper(defaults()) - parse(toml, extensions) - .right - .flatMap(codec(_, d, 0).right.map(generic.from)) - } - - def apply[D <: HList, R <: HList](toml: String)( - implicit - generic : LabelledGeneric.Aux[A, R], - defaults : Default.AsRecord.Aux[A, D], - defaultMapper: util.RecordToMap[D], - codec : Codec[R] - ): Either[Parse.Error, A] = apply(toml, Set()) - } - - class CodecHelperValue[A] { - def apply(value: Value)(implicit codec: Codec[A]): Either[Parse.Error, A] = - codec(value, Map(), 0) - - def apply(toml: String, extensions: Set[Extension] = Set()) - (implicit codec: Codec[A]): Either[Parse.Error, A] = - parse(toml, extensions).right.flatMap(codec(_, Map(), 0)) - } - - def parseAs [T]: CodecHelperGeneric[T] = new CodecHelperGeneric[T] - def parseAsValue[T]: CodecHelperValue [T] = new CodecHelperValue[T] } diff --git a/shared/src/main/scala/toml/Unescape.scala b/shared/src/main/scala/toml/Unescape.scala index 5da17de..804937b 100644 --- a/shared/src/main/scala/toml/Unescape.scala +++ b/shared/src/main/scala/toml/Unescape.scala @@ -31,11 +31,11 @@ object Unescape { case Some('r') => "\r" case Some('\"') => "\"" case Some('\\') => "\\" - case Some('u') if i + 4 >= str.length => // U+XXXX + case Some('u') if i + 4 <= str.length => // U+XXXX val dec = str.slice(i, i + 4) i += 4 decodeUnicodeShort(dec) - case Some('U') if i + 8 >= str.length => // U+XXXXXXXX + case Some('U') if i + 8 <= str.length => // U+XXXXXXXX val dec = str.slice(i, i + 8) i += 8 decodeUnicodeShort(dec) diff --git a/shared/src/test/scala/toml/CodecSpec.scala b/shared/src/test/scala-2/toml/CodecSpec.scala similarity index 98% rename from shared/src/test/scala/toml/CodecSpec.scala rename to shared/src/test/scala-2/toml/CodecSpec.scala index 1d2fe94..af0c2ff 100644 --- a/shared/src/test/scala/toml/CodecSpec.scala +++ b/shared/src/test/scala-2/toml/CodecSpec.scala @@ -1,10 +1,10 @@ package toml -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite import Codecs._ -class CodecSpec extends FunSuite { +class CodecSpec extends AnyFunSuite { test("Booleans") { case class BoolPair(a: Boolean, b: Boolean) @@ -20,7 +20,7 @@ class CodecSpec extends FunSuite { } test("Strings") { - val elem = Rules.elem.parse("\"test\"").get.value + val elem = fastparse.parse("\"test\"", Rules.elem(_)).get.value val result = new Toml.CodecHelperValue[String].apply(elem) assert(result == Right("test")) } @@ -53,7 +53,7 @@ class CodecSpec extends FunSuite { } test("Lists") { - val elem = Rules.elem.parse("""["test", "a"]""").get.value + val elem = fastparse.parse("""["test", "a"]""", Rules.elem(_)).get.value val result = new Toml.CodecHelperValue[List[String]].apply(elem) assert(result == Right(List("test", "a"))) } diff --git a/shared/src/test/scala/toml/EmbedSpec.scala b/shared/src/test/scala/toml/EmbedSpec.scala index 54b93dd..e5db6fa 100644 --- a/shared/src/test/scala/toml/EmbedSpec.scala +++ b/shared/src/test/scala/toml/EmbedSpec.scala @@ -1,13 +1,13 @@ package toml -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite import Value._ -class EmbedSpec extends FunSuite { +class EmbedSpec extends AnyFunSuite { test("One pair") { val pair = """a = 1""" - val node = Rules.root.parse(pair).get.value + val node = fastparse.parse(pair, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map("a" -> Num(1))))) } @@ -16,7 +16,7 @@ class EmbedSpec extends FunSuite { """b = 2 |a = 1 """.stripMargin - val node2 = Rules.root.parse(pairs).get.value + val node2 = fastparse.parse(pairs, Rules.root(_)).get.value assert(Embed.root(node2) == Right(Tbl(Map( "a" -> Num(1), "b" -> Num(2))))) } @@ -27,7 +27,7 @@ class EmbedSpec extends FunSuite { |[table] |a = 1 """.stripMargin - val node = Rules.root.parse(table).get.value + val node = fastparse.parse(table, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map("table" -> Tbl(Map("a" -> Num(1))))))) } @@ -39,7 +39,7 @@ class EmbedSpec extends FunSuite { |[table] |b = 2 """.stripMargin - val node = Rules.root.parse(table).get.value + val node = fastparse.parse(table, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map("a" -> Num(1), "table" -> Tbl(Map("b" -> Num(2))))))) } @@ -50,7 +50,7 @@ class EmbedSpec extends FunSuite { |[table.table2] |value = 42 """.stripMargin - val node = Rules.root.parse(table).get.value + val node = fastparse.parse(table, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map("table" -> Tbl(Map("table2" -> @@ -65,7 +65,7 @@ class EmbedSpec extends FunSuite { |[table.table3] |value = 42 """.stripMargin - val node = Rules.root.parse(table).get.value + val node = fastparse.parse(table, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "table" -> Tbl(Map( @@ -80,7 +80,7 @@ class EmbedSpec extends FunSuite { |[table.table3] |value = 42 """.stripMargin - val node = Rules.root.parse(table).get.value + val node = fastparse.parse(table, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "table" -> Tbl(Map( @@ -96,7 +96,7 @@ class EmbedSpec extends FunSuite { | { x = 7, y = 8, z = 9 }, | { x = 2, y = 4, z = 8 } ] """.stripMargin - val node = Rules.root.parse(tableList).get.value + val node = fastparse.parse(tableList, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "points" -> Arr(List( @@ -119,7 +119,7 @@ class EmbedSpec extends FunSuite { |colour = "grey" """.stripMargin - val node = Rules.root.parse(array).get.value + val node = fastparse.parse(array, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "products" -> Arr(List( Tbl(Map("name" -> Str("Hammer"), "sku" -> Num(738594937), "colour" -> Str("blue"))), @@ -142,7 +142,7 @@ class EmbedSpec extends FunSuite { |colour = "grey" """.stripMargin - val node = Rules.root.parse(array).get.value + val node = fastparse.parse(array, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "products" -> Arr(List( Tbl(Map("name" -> Str("Hammer"), "sku" -> Num(738594937))), @@ -174,7 +174,7 @@ class EmbedSpec extends FunSuite { | name = "plantain" """.stripMargin - val node = Rules.root.parse(array).get.value + val node = fastparse.parse(array, Rules.root(_)).get.value assert(Embed.root(node) == Right(Tbl(Map( "fruit" -> Arr(List( Tbl(Map( diff --git a/shared/src/test/scala/toml/GeneratedSpec.scala b/shared/src/test/scala/toml/GeneratedSpec.scala index 2d62ccf..1bcc3a3 100644 --- a/shared/src/test/scala/toml/GeneratedSpec.scala +++ b/shared/src/test/scala/toml/GeneratedSpec.scala @@ -1,93 +1,97 @@ package toml import org.scalatest.prop._ -import org.scalatest.{Matchers, PropSpec} -import scala.meta.internal.fastparse.all._ -import scala.meta.internal.fastparse.core.Parsed.{Failure, Success} +import org.scalatest.propspec.AnyPropSpec +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import fastparse._ +import fastparse.Parsed.{Failure, Success} import scala.util.Try -class GeneratedSpec extends PropSpec with PropertyChecks with Matchers { +class GeneratedSpec extends AnyPropSpec with ScalaCheckPropertyChecks with Matchers { import TestHelpers._ property("Parse arrays") { import Generators.Arrays._ - forAll(arrayGen) { s: String => - shouldBeSuccess(Rules.elem.parse(s)) + forAll(arrayGen) { (s: String) => + shouldBeSuccess(parse(s, Rules.elem(_))) } } property("Parse integers") { import Generators.Numbers._ - forAll(validLongGen) { s: String => + forAll(validLongGen) { (s: String) => val expected = Success(Value.Num(Rules.rmUnderscore(s).toLong), s.length) - Rules.elem.parse(s) shouldBe expected + parse(s, Rules.elem(_)) shouldBe expected } } ignore("Parse doubles") { import Generators.Numbers._ - forAll(validDoubleGen) { s: String => + forAll(validDoubleGen) { (s: String) => val expected = Success(Value.Real(Rules.rmUnderscore(s).toDouble), s.length) - Rules.elem.parse(s) shouldBe expected + parse(s, Rules.elem(_)) shouldBe expected } } property("Parse booleans") { import Generators.Booleans._ - forAll(validBoolGen) { s: String => + forAll(validBoolGen) { (s: String) => val expected = Success(toBool(s), s.length) - Rules.elem.parse(s) shouldBe expected + parse(s, Rules.elem(_)) shouldBe expected } } property("Detect if booleans are not lowercase") { import Generators.Booleans._ - forAll(invalidBoolGen) { s: String => - shouldBeFailure(Rules.elem.parse(s)) + forAll(invalidBoolGen) { (s: String) => + shouldBeFailure(parse(s, Rules.elem(_))) } } property("Detect if any string is unbalanced (missing quote)") { import Generators.Strings._ - forAll(invalidStrGen) { s: String => - shouldBeFailure(Rules.elem.parse(s)) + forAll(invalidStrGen) { (s: String) => + shouldBeFailure(parse(s, Rules.elem(_))) } } property("Parse pairs (key and value)") { import Generators.Tables._ - forAll(pairGen) { s: String => - shouldBeSuccess[(String, Value)](Rules.pair.parse(s)) + forAll(pairGen) { (s: String) => + shouldBeSuccess[(String, Value)](parse(s, Rules.pair(_))) } } property("Parse pairs (with `root` parser)") { import Generators.Tables._ - forAll(pairGen) { s: String => - shouldBeSuccess(Rules.root.parse(s)) + forAll(pairGen) { (s: String) => + shouldBeSuccess(parse(s, Rules.root(_))) } } property("Parse table definitions") { import Generators.Tables._ - forAll(tableDefGen) { s: String => - shouldBeSuccess[Seq[String]](Rules.tableDef.parse(s)) + forAll(tableDefGen) { (s: String) => + shouldBeSuccess[Seq[String]](parse(s, Rules.tableDef(_))) } } property("Parse tables") { + import NoWhitespace._ import Generators.Tables._ - forAll(tableGen) { s: String => - shouldBeSuccess[Node.NamedTable]((Rules.skip ~ Rules.table).parse(s)) + def p[$: P] = P(Rules.skip ~ Rules.table) + forAll(tableGen) { (s: String) => + shouldBeSuccess[Node.NamedTable](parse(s, p(_))) } } property("Parse tables (with `root` parser)") { import Generators.Tables._ - forAll(tableGen) { s: String => - shouldBeSuccess(Rules.root.parse(s)) + forAll(tableGen) { (s: String) => + shouldBeSuccess(parse(s, Rules.root(_))) } } } diff --git a/shared/src/test/scala/toml/GenerationSpec.scala b/shared/src/test/scala/toml/GenerationSpec.scala index 492ad63..39c8eaa 100644 --- a/shared/src/test/scala/toml/GenerationSpec.scala +++ b/shared/src/test/scala/toml/GenerationSpec.scala @@ -3,9 +3,9 @@ package toml import Node._ import Value._ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite -class GenerationSpec extends FunSuite { +class GenerationSpec extends AnyFunSuite { def check(root: Root, expected: String): Unit = { val generated = Toml.generate(root) val parsed = Toml.parse(generated) @@ -140,7 +140,7 @@ class GenerationSpec extends FunSuite { "-Yno-adapted-args", "-Ywarn-numeric-widen", "-feature" - ).map(Str))))) + ).map(Str.apply))))) val table = """ diff --git a/shared/src/test/scala/toml/Generators.scala b/shared/src/test/scala/toml/Generators.scala index 0996dcf..3beb434 100644 --- a/shared/src/test/scala/toml/Generators.scala +++ b/shared/src/test/scala/toml/Generators.scala @@ -10,8 +10,8 @@ object Generators { def enquoteStr(s: String, q: Char): String = q + s + q def quotedStrGen(quote: Char): Gen[String] = for { - s <- Gen.alphaStr if s.nonEmpty - } yield enquoteStr(s.filter(_ != quote), quote) + s <- Gen.alphaStr if s.nonEmpty && !s.contains(quote) + } yield enquoteStr(s, quote) def doubleQuoteStrGen: Gen[String] = quotedStrGen(DoubleQuote) def singleQuoteStrGen: Gen[String] = quotedStrGen(SingleQuote) diff --git a/shared/src/test/scala/toml/ParseSpec.scala b/shared/src/test/scala/toml/ParseSpec.scala index 92fdbc5..33133dc 100644 --- a/shared/src/test/scala/toml/ParseSpec.scala +++ b/shared/src/test/scala/toml/ParseSpec.scala @@ -3,9 +3,9 @@ package toml import Value._ import Node._ -import org.scalatest.FunSuite +import org.scalatest.funsuite.AnyFunSuite -class ParseSpec extends FunSuite { +class ParseSpec extends AnyFunSuite { test("Parse strings") { val toml = """ diff --git a/shared/src/test/scala/toml/RulesSpec.scala b/shared/src/test/scala/toml/RulesSpec.scala index 0a117cb..a3354e3 100644 --- a/shared/src/test/scala/toml/RulesSpec.scala +++ b/shared/src/test/scala/toml/RulesSpec.scala @@ -1,9 +1,10 @@ package toml -import org.scalatest.{FunSuite, Matchers} -import scala.meta.internal.fastparse.core.Parsed._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.funsuite.AnyFunSuite +import fastparse.Parsed._ -class RulesSpec extends FunSuite with Matchers { +class RulesSpec extends AnyFunSuite with Matchers { import TestHelpers._ val smallTest = diff --git a/shared/src/test/scala/toml/TestHelpers.scala b/shared/src/test/scala/toml/TestHelpers.scala index a35ca2e..2c5335a 100644 --- a/shared/src/test/scala/toml/TestHelpers.scala +++ b/shared/src/test/scala/toml/TestHelpers.scala @@ -1,32 +1,32 @@ package toml -import scala.meta.internal.fastparse.all._ -import scala.meta.internal.fastparse.core.Parsed._ +import fastparse._ +import fastparse.Parsed._ -import org.scalatest.Matchers +import org.scalatest.matchers.should.Matchers object TestHelpers { import Matchers._ def testSuccess(example: String, rules: Rules = Rules): Root = - rules.root.parse(example) match { + parse(example, rules.root(_), verboseFailures = true) match { case Success(v, _) => v - case f: Failure[_, _] => fail(s"Failed to parse `$example`: ${f.msg}") + case f: Failure => fail(s"Failed to parse `$example`: ${f.longMsg}") } def testFailure(example: String, rules: Rules = Rules): Unit = - rules.root.parse(example) match { + parse(example,rules.root(_)) match { case Success(_, _) => fail(s"Did not fail: $example") - case _: Failure[_, _] => + case _: Failure => } def shouldBeSuccess[T](r: Parsed[T]): Unit = r match { - case s: Success[T, _, _] => - case f: Failure[_, _] => fail(s"$r is not a Success: $f") + case s: Success[T] => + case f: Failure => fail(s"$r is not a Success: $f") } def shouldBeFailure[T](r: Parsed[T]): Unit = r match { - case s: Success[T, _, _] => fail(s"$r is not a Failure.") - case f: Failure[_, _] => + case s: Success[T] => fail(s"$r is not a Failure.") + case f: Failure => } } diff --git a/version.sbt b/version.sbt index 60b60a5..4d8ac69 100644 --- a/version.sbt +++ b/version.sbt @@ -1,6 +1,11 @@ -version in ThisBuild := { - import sys.process._ - val version = Seq("git", "describe", "--tags").!!.trim.tail - println("[info] Setting version to: " + version) - version +import scala.sys.process._ +import scala.util.Try + +ThisBuild / version := { + val current = (ThisBuild / version).value + val v = + Try(Seq("git", "describe", "--tags").!!.trim.tail) + .getOrElse(current) + println(s"[info] Setting version to: $v") + v }