diff --git a/build.sbt b/build.sbt index 606ce21c..04dddd7b 100644 --- a/build.sbt +++ b/build.sbt @@ -21,7 +21,8 @@ inThisBuild( url("https://geirsson.com") ), scalaVersion := ScalaVersions.head, - crossScalaVersions := ScalaVersions + crossScalaVersions := ScalaVersions, + resolvers += Resolver.sonatypeRepo("snapshots") ) ) @@ -83,7 +84,8 @@ lazy val website = project .dependsOn( docs, json, - typesafe + typesafe, + sconfig ) lazy val core = crossProject(JVMPlatform, JSPlatform) @@ -127,6 +129,18 @@ lazy val typesafe = project ) .dependsOn(coreJVM % "test->test;compile->compile") +lazy val sconfigLib = "org.ekrich" %% "sconfig" % "0.7.0" + +lazy val sconfig = project + .in(file("metaconfig-sconfig")) + .settings( + testSettings, + moduleName := "metaconfig-sconfig", + description := "Integration for HOCON using ekrich/sconfig.", + libraryDependencies += sconfigLib + ) + .dependsOn(coreJVM % "test->test;compile->compile") + lazy val hocon = crossProject(JVMPlatform, JSPlatform) .in(file("metaconfig-hocon")) .settings( diff --git a/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/SConfig2Class.scala b/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/SConfig2Class.scala new file mode 100644 index 00000000..413bde07 --- /dev/null +++ b/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/SConfig2Class.scala @@ -0,0 +1,82 @@ +package metaconfig +package sconfig + +import org.ekrich.config._ +import scala.collection.JavaConverters._ +import scala.collection.mutable +import metaconfig.internal.ConfGet + +object SConfig2Class { + def gimmeConfFromString(string: String): Configured[Conf] = + gimmeSafeConf(() => ConfigFactory.parseString(string)) + def gimmeConfFromFile(file: java.io.File): Configured[Conf] = { + if (!file.exists()) + Configured.NotOk(ConfError.fileDoesNotExist(file.getAbsolutePath)) + else if (file.isDirectory) + Configured.NotOk( + ConfError.message(s"File ${file.getAbsolutePath} is a directory") + ) + else gimmeSafeConf(() => ConfigFactory.parseFile(file)) + } + def gimmeConf(config: Config): Configured[Conf] = + gimmeSafeConf(() => config) + + private def gimmeSafeConf(config: () => Config): Configured[Conf] = { + val cache = mutable.Map.empty[Input, Array[Int]] + def loop(value: ConfigValue): Conf = { + val conf = value match { + case obj: ConfigObject => + Conf.Obj(obj.asScala.mapValues(loop).toList) + case lst: ConfigList => + Conf.Lst(lst.listIterator().asScala.map(loop).toList) + case _ => + value.unwrapped match { + case x: String => Conf.Str(x) + case x: java.lang.Integer => Conf.Num(BigDecimal(x)) + case x: java.lang.Long => Conf.Num(BigDecimal(x)) + case x: java.lang.Double => Conf.Num(BigDecimal(x)) + case x: java.lang.Boolean => Conf.Bool(x) + case null => Conf.Null() + case x => + throw new IllegalArgumentException( + s"Unexpected config value $value with unwrapped value $x" + ) + } + } + getPositionOpt(value.origin, cache).fold(conf)(conf.withPos) + } + try { + Configured.Ok(loop(config().resolve().root)) + } catch { + case e: ConfigException.Parse => + Configured.NotOk( + ConfError.parseError(getPosition(e.origin, cache), e.getMessage) + ) + } + } + + private def getPosition( + originOrNull: ConfigOrigin, + cache: mutable.Map[Input, Array[Int]] + ): Position = + getPositionOpt(originOrNull, cache).getOrElse(Position.None) + + private def getPositionOpt( + originOrNull: ConfigOrigin, + cache: mutable.Map[Input, Array[Int]] + ): Option[Position] = + for { + origin <- Option(originOrNull) + url <- Option(origin.url) + linePlus1 <- Option(origin.lineNumber) + line = linePlus1 - 1 + input = Input.File(new java.io.File(url.toURI)) + offsetByLine = cache.getOrElseUpdate( + input, + ConfGet.getOffsetByLine(input.chars) + ) + if line < offsetByLine.length + start = offsetByLine(line) + } yield Position.Range(input, start, start) + +} diff --git a/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/package.scala b/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/package.scala new file mode 100644 index 00000000..103b538e --- /dev/null +++ b/metaconfig-sconfig/src/main/scala/metaconfig/sconfig/package.scala @@ -0,0 +1,12 @@ +package metaconfig + +package object sconfig { + implicit val sConfigMetaconfigParser = new MetaconfigParser { + override def fromInput(input: Input): Configured[Conf] = input match { + case Input.File(path, _) => + SConfig2Class.gimmeConfFromFile(path.toFile) + case els => + SConfig2Class.gimmeConfFromString(new String(els.chars)) + } + } +} diff --git a/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/HoconPrinterProps.scala b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/HoconPrinterProps.scala new file mode 100644 index 00000000..5517b361 --- /dev/null +++ b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/HoconPrinterProps.scala @@ -0,0 +1,56 @@ +package metaconfig.sconfig + +import metaconfig.Conf +import metaconfig.ConfOps +import metaconfig.ConfShow +import org.scalacheck.Properties +import org.scalameta.logger +import org.scalatest.FunSuite +import scala.meta.testkit.DiffAssertions +import metaconfig.Generators.argConfShow +import org.scalacheck.Prop.forAll + +object HoconPrinterProps { + def checkRoundtrip(conf: String): Boolean = { + val a = Conf.parseString(conf).get + val hocon = Conf.printHocon(a) + val b = Conf.parseString(hocon).get + val isEqual = a == b + if (!isEqual) { + pprint.log(a) + pprint.log(b) + logger.elem(conf, hocon, Conf.patch(a, b)) + } + a == b + } + +} + +class HoconPrinterProps extends Properties("HoconPrinter") { + property("roundtrip") = forAll { conf: ConfShow => + HoconPrinterProps.checkRoundtrip(conf.str) + } +} + +class HoconPrinterRoundtripSuite extends FunSuite with DiffAssertions { + def ignore(conf: String): Unit = super.ignore(conf) {} + def checkRoundtrip(conf: String): Unit = + test(conf.take(100)) { + assert(HoconPrinterProps.checkRoundtrip(conf)) + } + + ignore( + """ + |a.a = "d" + |a.bc = 9 + """.stripMargin + ) + + checkRoundtrip( + """ + |aa.bb = true + |aa.d = 3 + |aa.aa = "cb" + """.stripMargin + ) +} diff --git a/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/PatchProps.scala b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/PatchProps.scala new file mode 100644 index 00000000..48685b60 --- /dev/null +++ b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/PatchProps.scala @@ -0,0 +1,57 @@ +package metaconfig.sconfig + +import metaconfig.Conf +import metaconfig.ConfOps +import metaconfig.ConfShow +import org.scalacheck.Prop.forAll +import org.scalacheck.Properties +import org.scalameta.logger +import org.scalatest.FunSuite +import scala.meta.testkit.DiffAssertions +import metaconfig.Generators.argConfShow + +object PatchProps { + // asserts that applying + def checkPatch(a: String, b: String): Boolean = { + val original = Conf.parseString(a).get + val revised = Conf.parseString(b).get + val patch = Conf.patch(original, revised) + val expected = Conf.applyPatch(original, revised) + val obtained = Conf.applyPatch(original, patch) + if (obtained != expected) { + logger.elem( + obtained, + expected, + patch.toString, + Conf.patch(obtained, expected) + ) + } + obtained == expected + } +} + +class PatchProps extends Properties("Patch") { + + property("roundtrip") = forAll { (a: ConfShow, b: ConfShow) => + PatchProps.checkPatch(a.str, b.str) + } + +} +class PatchPropsSuite extends FunSuite with DiffAssertions { + def check(a: String, b: String): Unit = { + test(a) { assert(PatchProps.checkPatch(a, b)) } + } + + check( + """ + |ad.da = true + |cc.bd = "dd" + """.stripMargin, + """ + | + |ad.a.dc = false + |ad = "ad" + """.stripMargin + ) + +} diff --git a/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/SConfig2ClassTest.scala b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/SConfig2ClassTest.scala new file mode 100644 index 00000000..e892451e --- /dev/null +++ b/metaconfig-sconfig/src/test/scala/metaconfig/sconfig/SConfig2ClassTest.scala @@ -0,0 +1,54 @@ +package metaconfig.sconfig + +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import metaconfig.Conf +import org.scalatest.FunSuite + +class SConfig2ClassTest extends FunSuite { + test("basic") { + val file = File.createTempFile("prefix", ".conf") + Files.write( + Paths.get(file.toURI), + """|a.b = 2 + |a = [ + | 1, + | "2" + |] + |a += true""".stripMargin.getBytes() + ) + val obtained = SConfig2Class.gimmeConfFromFile(file).get + val expected = Conf.Obj( + "a" -> Conf.Lst( + Conf.Num(1), + Conf.Str("2"), + Conf.Bool(true) + ) + ) + assert(obtained == expected) + } + + test("file not found") { + val f = File.createTempFile("doesnotexist", "conf") + f.delete() + assert(SConfig2Class.gimmeConfFromFile(f).isNotOk) + } + + test("null") { + val obtained = + SConfig2Class + .gimmeConfFromString( + """|keywords = [ + | null + |]""".stripMargin + ) + .get + val expected = Conf.Obj( + "keywords" -> Conf.Lst( + Conf.Null() + ) + ) + assert(obtained == expected) + } +} diff --git a/metaconfig-typesafe-config/src/main/scala/metaconfig/typesafeconfig/TypesafeConfig2Class.scala b/metaconfig-typesafe-config/src/main/scala/metaconfig/typesafeconfig/TypesafeConfig2Class.scala index 822a2ecf..767eee19 100644 --- a/metaconfig-typesafe-config/src/main/scala/metaconfig/typesafeconfig/TypesafeConfig2Class.scala +++ b/metaconfig-typesafe-config/src/main/scala/metaconfig/typesafeconfig/TypesafeConfig2Class.scala @@ -30,7 +30,7 @@ object TypesafeConfig2Class { case lst: ConfigList => Conf.Lst(lst.listIterator().asScala.map(loop).toList) case _ => - value.unwrapped() match { + value.unwrapped match { case x: String => Conf.Str(x) case x: java.lang.Integer => Conf.Num(BigDecimal(x)) case x: java.lang.Long => Conf.Num(BigDecimal(x)) @@ -43,14 +43,14 @@ object TypesafeConfig2Class { ) } } - getPositionOpt(value.origin(), cache).fold(conf)(conf.withPos) + getPositionOpt(value.origin, cache).fold(conf)(conf.withPos) } try { - Configured.Ok(loop(config().resolve().root())) + Configured.Ok(loop(config().resolve().root)) } catch { case e: ConfigException.Parse => Configured.NotOk( - ConfError.parseError(getPosition(e.origin(), cache), e.getMessage) + ConfError.parseError(getPosition(e.origin, cache), e.getMessage) ) } } @@ -67,8 +67,8 @@ object TypesafeConfig2Class { ): Option[Position] = for { origin <- Option(originOrNull) - url <- Option(origin.url()) - linePlus1 <- Option(origin.lineNumber()) + url <- Option(origin.url) + linePlus1 <- Option(origin.lineNumber) line = linePlus1 - 1 input = Input.File(new java.io.File(url.toURI)) offsetByLine = cache.getOrElseUpdate(