Skip to content

Commit

Permalink
Refactor to support File based methods on Scala Native (#384)
Browse files Browse the repository at this point in the history
* ConfigParseOptions, shared to all, FileUtils, JVM to JVM Native

* Refactor ConfigFactoryDocumentTest to Shared and JVM - JS and Native tests from 318 to 346

* Rename ConfigFactoryCommon to ConfigFactoryShared

* Initial refactor for ConfigDocumentFactory

* Format to pass CI

* Remove ConfigDocumentFactory java.io.File methods from shared and put in JvmNative

* Add test for native - not sure this is correct

* File reading now works on Native

* Share validation tests

* Share JVM/Native ConfigFactoryDocumentTest

* Update README for release

* Remove commented out code

* Remove commented out validation serialization test that was moved to JVM only
  • Loading branch information
ekrich authored Nov 5, 2024
1 parent 6ff9be1 commit ea96ab2
Show file tree
Hide file tree
Showing 44 changed files with 1,371 additions and 330 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Complete setup documentation and the current `scalafix` version can be found in
## Versions
Release [1.8.0](https://github.com/ekrich/sconfig/releases/tag/v1.8.0) - (2024-11-05)<br/>
Release [1.7.0](https://github.com/ekrich/sconfig/releases/tag/v1.7.0) - (2023-04-16)<br/>
Release [1.6.0](https://github.com/ekrich/sconfig/releases/tag/v1.6.0) - (2023-12-28)<br/>
Release [1.5.1](https://github.com/ekrich/sconfig/releases/tag/v1.5.1) - (2023-09-15)<br/>
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ lazy val sconfig = crossProject(JVMPlatform, NativePlatform, JSPlatform)
.nativeConfigure(_.enablePlugins(ScalaNativeJUnitPlugin))
.nativeSettings(
crossScalaVersions := versions,
nativeConfig ~= (
Test / nativeConfig ~= (
_.withEmbedResources(true)
),
logLevel := Level.Info, // Info or Debug
Expand Down
22 changes: 15 additions & 7 deletions docs/SCALA-NATIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ val config = ConfigFactory.parseReader(
val specialChars = config.getString("fromProps.specialChars")
```

### How to read a HOCON configuation file into a String for Scala Native
In Scala Native `java.io.FileReader` is available so you can create a
`FileReader` from a `File`.

In order to read the configuration file into a `String` you need to know the relative
### How to read a HOCON configuation file using Scala Native

In order to read a configuration file you need to know the relative
path from where the executable was started or use an absolute path. If the
Scala Native executable is `run` from `sbt` it will have the current working directory
equal to the directory at the base of your project where `sbt` was started. If curious
Expand All @@ -88,16 +91,21 @@ println(s"Working Dir: $dir")
```

Continuing the same thought process you can use the following code to read the file
into a `String` from a simple `sbt` project where the `src` directory is at the top
from a simple `sbt` project where the `src` directory is at the top
level of your project and you are using the `run` command. If you package your
application or run the application executable directly, then making the path relative
to the binary with the code above could be your best option. Another option is to use
the `"user.home"` or the `"user.dir"` property to configure the file path.
the `"user.home"` or the `"user.dir"` property to configure the file path. Note: When the
executable is added to the path, the current working directory is where you start the
executable from so keep that in mind

```scala
import java.nio.file.{Files, Paths}
val bytes = Files.readAllBytes(Paths.get("src/main/resources/myapp.conf"))
val configStr = new String(bytes)
import java.io.File
val file = new File("src/main/resources/myapp.conf")
// ConfigDocument
val configDocument = ConfigDocumentFactory.parseFile(file)
// or Config
val config = ConfigFactory.parseFile(file)
```

Using this code with the code above gives you a working solution to use `sconfig`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package org.ekrich.config
/**
* [[ConfigFactory]] methods for Scala.js platform
*/
abstract class PlatformConfigFactory extends ConfigFactoryCommon {}
abstract class PlatformConfigFactory extends ConfigFactoryShared {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.ekrich.config.parser

/**
* [[ConfigDocumentFactory]] methods for Scala.js platform
*/
abstract class PlatformConfigDocumentFactory
extends ConfigDocumentFactoryShared {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package org.ekrich.config
/**
* [[ConfigFactory]] methods common to JVM and Native
*/
abstract class ConfigFactoryJvmNative extends ConfigFactoryCommon {}
abstract class ConfigFactoryJvmNative extends ConfigFactoryShared {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.ekrich.config.parser

import java.io.File

import org.ekrich.config.ConfigParseOptions
import org.ekrich.config.impl.Parseable

/**
* [[ConfigDocumentFactory]] methods common to JVM and Native
*/
abstract class ConfigDocumentFactoryJvmNative
extends ConfigDocumentFactoryShared {

/**
* Parses a file into a ConfigDocument instance.
*
* @param file
* the file to parse
* @param options
* parse options to control how the file is interpreted
* @return
* the parsed configuration
* @throws org.ekrich.config.ConfigException
* on IO or parse errors
*/
def parseFile(file: File, options: ConfigParseOptions): ConfigDocument =
Parseable.newFile(file, options).parseConfigDocument()

/**
* Parses a file into a ConfigDocument instance as with
* [[#parseFile(file:java\.io\.File,options:org\.ekrich\.config\.ConfigParseOptions)* parseFile(File, ConfigParseOptions)]]
* but always uses the default parse options.
*
* @param file
* the file to parse
* @return
* the parsed configuration
* @throws org.ekrich.config.ConfigException
* on IO or parse errors
*/
def parseFile(file: File): ConfigDocument =
parseFile(file, ConfigParseOptions.defaults)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.ekrich.config.impl

import java.io.{BufferedReader, FileReader}

import org.ekrich.config.parser._
import org.junit.Assert._
import org.junit.Test

import FileUtils._

class ConfigDocumentFactoryTest extends TestUtils {

@Test
def configDocumentFileParse: Unit = {
val configDocument =
ConfigDocumentFactory.parseFile(resourceFile("/test03.conf"))
val fileReader = new BufferedReader(
new FileReader(resourceFile("/test03.conf"))
)
var line = fileReader.readLine()
val sb = new StringBuilder()
while (line != null) {
sb.append(line)
sb.append("\n")
line = fileReader.readLine()
}
fileReader.close()
val fileText = sb.toString()
assertEquals(fileText, defaultLineEndingsToUnix(configDocument.render))
}

private def defaultLineEndingsToUnix(s: String): String =
s.replaceAll(System.lineSeparator(), "\n")

@Test
def configDocumentReaderParse: Unit = {
val configDocument = ConfigDocumentFactory.parseReader(
new FileReader(resourceFile("/test03.conf"))
)
val configDocumentFile =
ConfigDocumentFactory.parseFile(resourceFile("/test03.conf"))
assertEquals(configDocumentFile.render, configDocument.render)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object FileUtils {
else ""

val resourceDir = {
val f = new File("src/test/resources")
val f = TestPath.file()
if (!f.exists()) {
val here = new File(".").getAbsolutePath
throw new Exception(
Expand All @@ -39,6 +39,9 @@ object FileUtils {
f
}

// TODO: can't test if file exists because some tests require the absense
// of the file. The problem is that later on in the framework, not test
// is done so silent failures can occur
def resourceFile(filename: String): File =
new File(resourceDir, filename)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright (C) 2011 Typesafe Inc. <http://typesafe.com>
*/
package org.ekrich.config.impl

import org.junit._

import org.ekrich.config.ConfigFactory
import org.ekrich.config.ConfigParseOptions
import org.ekrich.config.ConfigException
import FileUtils._

class ValidationFileTest extends TestUtils {
// TODO: There is a problem upstream where an exception is not thrown
// when the file does not exist. Added a check in FileUtils for native only
// jvm would need it.
@Test
def validation(): Unit = {
val reference = ConfigFactory.parseFile(
resourceFile("validate-reference.conf"),
ConfigParseOptions.defaults
)
val conf = ConfigFactory.parseFile(
resourceFile("validate-invalid.conf"),
ConfigParseOptions.defaults
)
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference)
}

val expecteds = Seq(
Missing("willBeMissing", 1, "number"),
WrongType("int3", 7, "number", "object"),
WrongType("float2", 9, "number", "boolean"),
WrongType("float3", 10, "number", "list"),
WrongType("bool1", 11, "boolean", "number"),
WrongType("bool3", 13, "boolean", "object"),
Missing("object1.a", 17, "string"),
WrongType("object2", 18, "object", "list"),
WrongType("object3", 19, "object", "number"),
WrongElementType("array3", 22, "boolean", "object"),
WrongElementType("array4", 23, "object", "number"),
WrongType("array5", 24, "list", "number"),
WrongType("a.b.c.d.e.f.g", 28, "boolean", "number"),
Missing("a.b.c.d.e.f.j", 28, "boolean"),
WrongType("a.b.c.d.e.f.i", 30, "boolean", "list")
)

checkValidationException(e, expecteds)
}

@Test
def validationWithRoot(): Unit = {
val objectWithB = parseObject("""{ b : c }""")
val reference = ConfigFactory
.parseFile(
resourceFile("validate-reference.conf"),
ConfigParseOptions.defaults
)
.withFallback(objectWithB)
val conf = ConfigFactory.parseFile(
resourceFile("validate-invalid.conf"),
ConfigParseOptions.defaults
)
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference, "a", "b")
}

val expecteds = Seq(
Missing("b", 1, "string"),
WrongType("a.b.c.d.e.f.g", 28, "boolean", "number"),
Missing("a.b.c.d.e.f.j", 28, "boolean"),
WrongType("a.b.c.d.e.f.i", 30, "boolean", "list")
)

checkValidationException(e, expecteds)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.ekrich.config.parser

/**
* [[ConfigDocumentFactory]] methods for Scala JVM platform
*/
abstract class PlatformConfigDocumentFactory
extends ConfigDocumentFactoryJvmNative {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.ekrich.config.impl

import java.io.File

// See: https://github.com/scala-native/scala-native/issues/4077
object TestPath {
def file(): File = new File("src/test/resources")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (C) 2011 Typesafe Inc. <http://typesafe.com>
*/
package org.ekrich.config.impl

import org.junit._

import org.ekrich.config.ConfigException

class ValidationSerializableTest extends TestUtils {
@Test
def validationFailedSerializable(): Unit = {
// Reusing a previous test case to generate an error
val reference = parseConfig("""{ a : [{},{},{}] }""")
val conf = parseConfig("""{ a : 42 }""")
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference)
}
val expecteds = Seq(WrongType("a", 1, "list", "number"))

val actual = checkSerializableNoMeaningfulEquals(e)
checkValidationException(actual, expecteds)
}
}
Loading

0 comments on commit ea96ab2

Please sign in to comment.