diff --git a/.travis.yml b/.travis.yml index 448aa9f..f2d34ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: scala sudo: false scala: - - 2.12.10 + - 2.12.13 - 2.13.1 jdk: diff --git a/README.md b/README.md index 192ebb8..8c96e19 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,29 @@ Small library for translating between [HOCON], [Java properties], and JSON documents and circe's JSON AST. -At a high-level it can be used as a [circe] powered front-end for the [Typesafe -config] library to enable boilerplate free loading of settings into Scala types. -More generally it provides parsers and printers for interoperating with -[Typesafe config]'s JSON AST. +At a high-level it can be used as a [circe] powered front-end for either the [Typesafe +config] or [sconfig] libraries to enable boilerplate free loading of settings into Scala types. +More generally it provides parsers and printers for interoperating with a [Typesafe config] +compatible JSON AST. [HOCON]: https://github.com/lightbend/config/blob/master/HOCON.md [Java properties]: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html ## Usage -To use this library configure your sbt project with the following line: +To use this library configure your sbt project with one of the following lines: +#### With Typesafe Config ```sbt -libraryDependencies += "io.circe" %% "circe-config" % "0.7.0" +libraryDependencies += "io.circe" %% "circe-config" % "0.9.0" +``` +#### With sconfig +```sbt +libraryDependencies += "io.circe" %% "circe-sconfig" % "0.9.0" +``` +#### With Scala.js +```sbt +libraryDependencies += "io.circe" %%% "circe-sconfig" % "0.9.0" ``` ## Documentation @@ -30,7 +39,9 @@ libraryDependencies += "io.circe" %% "circe-config" % "0.7.0" The following examples use `io.circe:circe-generic` as a dependency to automatically derive decoders. They load the configuration found in -[application.conf]. +[application.conf] using `circe-config` via [Typesafe Config]. To use +`circe-sconfig` via [sconfig], replace any references to`io.circe.config` +below with `io.circe.sconfig`. ```scala scala> import com.typesafe.config.{ ConfigFactory, ConfigMemorySize } @@ -124,5 +135,6 @@ limitations under the License. [Typesafe config]: https://github.com/lightbend/config [CI]: https://github.com/circe/circe-config/actions [CI Status]: https://img.shields.io/github/workflow/status/circe/circe-config/Continuous%20Integration.svg + [sconfig]: https://github.com/ekrich/sconfig [Latest Version Badge]: https://img.shields.io/maven-central/v/io.circe/circe-config_2.12.svg [Latest Version]: https://maven-badges.herokuapp.com/maven-central/io.circe/circe-config_2.12 diff --git a/build.sbt b/build.sbt index 54b6f17..86157e7 100644 --- a/build.sbt +++ b/build.sbt @@ -1,9 +1,10 @@ name := "circe-config" description := "Yet another Typesafe Config decoder" -homepage := Some(url("https://github.com/circe/circe-config")) -licenses += "Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html") apiURL := Some(url("https://circe.github.io/circe-config/")) +ThisBuild / homepage := Some(url("https://github.com/circe/circe-config")) +ThisBuild / licenses += "Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html") + ThisBuild / organization := "io.circe" ThisBuild / crossScalaVersions := List("2.12.14", "2.13.6") ThisBuild / scalaVersion := crossScalaVersions.value.last @@ -33,7 +34,7 @@ ThisBuild / githubWorkflowBuild := Seq( ) mimaPreviousArtifacts := { - val versions = Set("0.3.0", "0.4.0", "0.4.1", "0.5.0", "0.6.0") + val versions = Set("0.3.0", "0.4.0", "0.4.1", "0.5.0", "0.6.0", "0.7.0", "0.8.0") val versionFilter: String => Boolean = CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, 12)) => _ => true case Some((2, 13)) => _ => false @@ -87,19 +88,20 @@ val Versions = new { val scalaCheck = "1.15.4" val scalaTest = "3.2.10" val scalaTestPlus = "3.2.10.0" + val sconfig = "1.4.5" } libraryDependencies ++= Seq( "com.typesafe" % "config" % Versions.config, - "io.circe" %% "circe-core" % Versions.circe, - "io.circe" %% "circe-parser" % Versions.circe, - "io.circe" %% "circe-generic" % Versions.circe % Test, - "io.circe" %% "circe-testing" % Versions.circe % Test, - "org.typelevel" %% "cats-effect" % Versions.catsEffect % Test, - "org.typelevel" %% "discipline-core" % Versions.discipline % Test, - "org.scalacheck" %% "scalacheck" % Versions.scalaCheck % Test, - "org.scalatest" %% "scalatest" % Versions.scalaTest % Test, - "org.scalatestplus" %% "scalacheck-1-15" % Versions.scalaTestPlus % Test + "io.circe" %%% "circe-core" % Versions.circe, + "io.circe" %%% "circe-parser" % Versions.circe, + "io.circe" %%% "circe-generic" % Versions.circe % Test, + "io.circe" %%% "circe-testing" % Versions.circe % Test, + "org.typelevel" %%% "cats-effect" % Versions.catsEffect % Test, + "org.typelevel" %%% "discipline-core" % Versions.discipline % Test, + "org.scalacheck" %%% "scalacheck" % Versions.scalaCheck % Test, + "org.scalatest" %%% "scalatest" % Versions.scalaTest % Test, + "org.scalatestplus" %%% "scalacheck-1-15" % Versions.scalaTestPlus % Test ) enablePlugins(GhpagesPlugin, SiteScaladocPlugin) @@ -108,63 +110,87 @@ ghpagesNoJekyll := true SiteScaladoc / siteSubdirName := "" doctestTestFramework := DoctestTestFramework.ScalaTest doctestMarkdownEnabled := true -Compile / doc / scalacOptions := Seq( - "-groups", - "-implicits", - "-doc-source-url", - scmInfo.value.get.browseUrl + "/tree/master€{FILE_PATH}.scala", - "-sourcepath", - (LocalRootProject / baseDirectory).value.getAbsolutePath -) -scalacOptions ++= Seq( - "-deprecation", - "-encoding", - "UTF-8", - "-feature", - "-language:postfixOps", - "-language:higherKinds", - "-unchecked", - "-Ywarn-dead-code", - "-Ywarn-numeric-widen", - "-Ywarn-unused:imports" +inThisBuild( + Seq( + scalacOptions ++= Seq( + "-deprecation", + "-encoding", + "UTF-8", + "-feature", + "-language:postfixOps", + "-language:higherKinds", + "-unchecked", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Ywarn-unused:imports" + ), + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 12)) => + Seq( + "-Xfatal-warnings", + "-Yno-adapted-args", + "-Xfuture" + ) + case _ => + Nil + } + }, + Compile / doc / scalacOptions := Seq( + "-groups", + "-implicits", + "-doc-source-url", + scmInfo.value.get.browseUrl + "/tree/master€{FILE_PATH}.scala", + "-sourcepath", + (LocalRootProject / baseDirectory).value.getAbsolutePath + ), + publishMavenStyle := true, + Test / publishArtifact := false, + publishTo := Some(if (isSnapshot.value) Opts.resolver.sonatypeSnapshots else Opts.resolver.sonatypeStaging), + pomIncludeRepository := (_ => false), + scmInfo := Some( + ScmInfo(url("https://github.com/circe/circe-config"), "scm:git:git@github.com:circe/circe-config.git") + ), + developers := List( + Developer("jonas", "Jonas Fonseca", "jonas.fonseca@gmail.com", url("https://github.com/jonas")) + ) + ) ) -scalacOptions ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 12)) => - Seq( - "-Xfatal-warnings", - "-Yno-adapted-args", - "-Xfuture" - ) - case _ => - Nil - } -} - Compile / console / scalacOptions --= Seq("-Ywarn-unused-import", "-Ywarn-unused:imports") Test / console / scalacOptions := (Compile / console / scalacOptions).value -publishMavenStyle := true -Test / publishArtifact := false -pomIncludeRepository := { _ => - false -} -publishTo := Some { - if (isSnapshot.value) - Opts.resolver.sonatypeSnapshots - else - Opts.resolver.sonatypeStaging -} - -scmInfo := Some( - ScmInfo( - url("https://github.com/circe/circe-config"), - "scm:git:git@github.com:circe/circe-config.git" - ) -) +lazy val `circe-config` = + project in file(".") + +lazy val `circe-sconfig` = + crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .in(file(".sconfig")) + .enablePlugins(ConfigLibraryGenerator) + .settings( + description := "Yet another Typesafe Config AST decoder", + configLibrary := ConfigLibrary( + targetPackage = "io.circe.sconfig", + targetShortPackage = "sconfig", + targetName = "circe-sconfig", + libraryPackage = "org.ekrich.config", + libraryDocUrl = "[[https://github.com/ekrich/sconfig SConfig]]" + ), + mimaPreviousArtifacts := { + val unavailable = Set("0.3.0", "0.4.0", "0.4.1", "0.5.0", "0.6.0", "0.7.0", "0.8.0") + (LocalRootProject / mimaPreviousArtifacts).value.collect { + case prev if !unavailable.contains(prev.revision) => prev.organization %%% "circe-sconfig" % prev.revision + } + }, + libraryDependencies ++= (LocalRootProject / libraryDependencies).value, + libraryDependencies += "org.ekrich" %%% "sconfig" % Versions.sconfig, + libraryDependencies -= "com.typesafe" % "config" % Versions.config + ) + .jvmSettings( + doctestTestFramework := (LocalRootProject / doctestTestFramework).value, + doctestMarkdownEnabled := (LocalRootProject / doctestMarkdownEnabled).value + ) -developers := List( - Developer("jonas", "Jonas Fonseca", "jonas.fonseca@gmail.com", url("https://github.com/jonas")) -) +aggregateProjects(`circe-sconfig`.jvm, `circe-sconfig`.js) diff --git a/project/ConfigLibraryGenerator.scala b/project/ConfigLibraryGenerator.scala new file mode 100644 index 0000000..4c83387 --- /dev/null +++ b/project/ConfigLibraryGenerator.scala @@ -0,0 +1,107 @@ +import sbt._ +import sbt.Keys._ +import sbt.io.Path.rebase + +object ConfigLibraryGenerator extends AutoPlugin { + + override def projectSettings: Seq[Setting[_]] = + inConfig(Compile)( + Seq( + sourceGenerators += sourceGeneratorTask, + resourceGenerators += resourceGeneratorTask + ) + ) ++ inConfig(Test)( + Seq( + sourceGenerators += sourceGeneratorTask, + resourceGenerators += resourceGeneratorTask + ) + ) + + object autoImport { + + val configLibrary = + settingKey[ConfigLibrary]("Defines metadata of Typesafe Config API Compatible library to build against.") + + final case class ConfigLibrary( + targetPackage: String, + targetShortPackage: String, + targetName: String, + libraryPackage: String, + libraryDocUrl: String + ) + + } + + def sourceGeneratorTask: Def.Initialize[Task[Seq[File]]] = Def.task { + val library = autoImport.configLibrary.value + val inCache = Difference.inputs( + streams.value.cacheStoreFactory.make(s"${library.targetShortPackage}-in"), + FileInfo.lastModified + ) + val outCache = + Difference.inputs(streams.value.cacheStoreFactory.make(s"${library.targetShortPackage}-out"), FileInfo.exists) + val mappings = (LocalRootProject / Keys.unmanagedSources).value + .pair(rebase((LocalRootProject / sourceDirectories).value, sourceManaged.value)) + .toMap + + streams.value.log.debug( + s"Mappings used for ${library.targetName} sources generation:${System.lineSeparator}${mappings.mkString(System.lineSeparator)}" + ) + + inCache(mappings.keySet) { inReport => + outCache { outReport => + if (outReport.checked.nonEmpty && inReport.modified.isEmpty && outReport.modified.isEmpty) + outReport.checked + else + inReport.checked.map { f => + val out = IO + .read(f) + .replaceAll("io[.]circe[.]config", library.targetPackage) + .replaceFirst("package config", "package " + library.targetShortPackage) + .replaceFirst("package object config", "package object " + library.targetShortPackage) + .replaceAll("com[.]typesafe[.]config", library.libraryPackage) + .replaceAll("import config[.]", s"import ${library.targetShortPackage}.") + .replaceAll("""private\[config] """, s"private[${library.targetShortPackage}] ") + .replaceAll("circe-config", library.targetName) + .replace("[[https://github.com/lightbend/config Typesafe config]]", library.libraryDocUrl) + val target = mappings(f) + + IO.delete(target) + IO.write(target, out.getBytes) + target + } + } + }.toSeq + } + + def resourceGeneratorTask: Def.Initialize[Task[Seq[File]]] = Def.task { + val library = autoImport.configLibrary.value + val inCache = Difference.inputs( + streams.value.cacheStoreFactory.make(s"${library.targetShortPackage}-in"), + FileInfo.lastModified + ) + val outCache = + Difference.inputs(streams.value.cacheStoreFactory.make(s"${library.targetShortPackage}-out"), FileInfo.exists) + val mappings = (LocalRootProject / unmanagedResources).value + .pair(rebase((LocalRootProject / unmanagedResourceDirectories).value, resourceManaged.value)) + .toMap + + streams.value.log.debug( + s"Mappings used for ${library.targetName} resource generation:${System.lineSeparator}${mappings.mkString(System.lineSeparator)}" + ) + + inCache(mappings.keySet) { inReport => + outCache { outReport => + if (outReport.checked.nonEmpty && inReport.modified.isEmpty && outReport.modified.isEmpty) + outReport.checked + else + inReport.checked.map { r => + val target = mappings(r) + IO.copy(Seq(r -> target), io.CopyOptions.apply().withOverwrite(true)) + target + } + } + }.toSeq + } + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 40c243c..fcfab83 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,5 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.13.0") addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.9.9") diff --git a/src/main/scala/io.circe.config/package.scala b/src/main/scala/io.circe.config/package.scala index f5bb932..998740c 100644 --- a/src/main/scala/io.circe.config/package.scala +++ b/src/main/scala/io.circe.config/package.scala @@ -65,11 +65,11 @@ package object config { private[config] def jsonToConfigValue(json: Json): ConfigValue = json.fold( ConfigValueFactory.fromAnyRef(null), - boolean => ConfigValueFactory.fromAnyRef(boolean), + boolean => ConfigValueFactory.fromAnyRef(Predef.boolean2Boolean(boolean)), number => number.toLong match { - case Some(long) => ConfigValueFactory.fromAnyRef(long) - case None => ConfigValueFactory.fromAnyRef(number.toDouble) + case Some(long) => ConfigValueFactory.fromAnyRef(Predef.long2Long(long)) + case None => ConfigValueFactory.fromAnyRef(Predef.double2Double(number.toDouble)) }, str => ConfigValueFactory.fromAnyRef(str), arr => ConfigValueFactory.fromIterable(arr.map(jsonToConfigValue).asJava),