Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scalajs / sconfig Support #191

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: scala
sudo: false

scala:
- 2.12.10
- 2.12.13
- 2.13.1

jdk:
Expand Down
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -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
156 changes: 91 additions & 65 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
107 changes: 107 additions & 0 deletions project/ConfigLibraryGenerator.scala
Original file line number Diff line number Diff line change
@@ -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
}

}
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/io.circe.config/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down