-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added support for typed properties file parsing for config, plus tests.
- Loading branch information
Showing
10 changed files
with
249 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
92 changes: 92 additions & 0 deletions
92
src/main/scala/com/escalatesoft/subcut/inject/util/PropertyFileModule.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package com.escalatesoft.subcut.inject.util | ||
|
||
import com.escalatesoft.subcut.inject.{BindingException, NewBindingModule, BindingModule, BindingKey} | ||
import NewBindingModule.newBindingModule | ||
import java.io.{FileInputStream, File} | ||
import java.util.Properties | ||
import scala.collection.JavaConverters._ | ||
import scala.language.existentials | ||
|
||
/** | ||
* Read simple value bindings from a property file for simple file-based configurations | ||
*/ | ||
case class PropertyFileModule(propFile: File, propertyParsers: Map[String, PropertyParser[_]] = PropertyMappings.Standard) extends BindingModule { | ||
if(!propFile.exists) throw new BindingException(s"No file ${propFile.getName} found") | ||
|
||
val bindings = newBindingModule(module => { | ||
val in = new FileInputStream(propFile) | ||
try { | ||
val properties = new Properties | ||
properties.load(in) | ||
|
||
val propsMap = properties.asScala | ||
|
||
for ((k, v) <- propsMap) { | ||
val (name, parser) = parserFor(k) | ||
try { | ||
module.bindings += parser.keyFor(name) -> parser.parse(v) | ||
} | ||
catch { | ||
case ex: Exception => throw new BindingException(s"""Could not parse "$v" for $k - check value is consistent with [type]: ${ex.getMessage}""") | ||
} | ||
} | ||
} finally in.close() | ||
}).bindings | ||
|
||
private def parserFor(k: String): (String, PropertyParser[_]) = { | ||
val splits = k.split('.') | ||
val typeMarker = if (splits.length > 1 && splits.last.startsWith("[")) splits.last.tail.init else "" // strip the []'s off | ||
|
||
if (typeMarker.isEmpty) (k, PropertyMappings.StringParser) else { // if no type marker, treat as String | ||
val typeParser = propertyParsers.get(typeMarker).getOrElse( | ||
throw new BindingException(s"No provided parser for type ${splits.last} in properties ${propFile.getName}")) | ||
(splits.init.mkString("."), typeParser) | ||
} | ||
} | ||
} | ||
|
||
|
||
object PropertyMappings { | ||
val StringParser = new PropertyParser[String] { | ||
def parse(prop: String): String = prop | ||
} | ||
|
||
val IntParser = new PropertyParser[Int] { | ||
def parse(prop: String): Int = prop.toInt | ||
} | ||
|
||
val LongParser = new PropertyParser[Long] { | ||
def parse(prop: String): Long = prop.toLong | ||
} | ||
|
||
val CharParser = new PropertyParser[Char] { | ||
def parse(prop: String): Char = prop.head | ||
} | ||
|
||
val DoubleParser = new PropertyParser[Double] { | ||
def parse(prop: String): Double = prop.toDouble | ||
} | ||
|
||
val FloatParser = new PropertyParser[Float] { | ||
def parse(prop: String): Float = prop.toFloat | ||
} | ||
|
||
val BooleanParser = new PropertyParser[Boolean] { | ||
def parse(prop: String): Boolean = prop.toBoolean | ||
} | ||
|
||
val Standard: Map[String, PropertyParser[_]] = Map ( | ||
"String" -> StringParser, | ||
"Int" -> IntParser, | ||
"Long" -> LongParser, | ||
"Double" -> DoubleParser, | ||
"Float" -> FloatParser, | ||
"Boolean" -> BooleanParser, | ||
"Char" -> CharParser | ||
) | ||
} | ||
|
||
abstract class PropertyParser[T : Manifest] { | ||
def parse(propString: String): T | ||
def keyFor(name: String): BindingKey[T] = BindingKey(manifest[T], Some(name)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
some.path.[Boolean] = false | ||
some.bad.[Boolean] = Truee |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
some.path.[Char] = 7 | ||
some.bad.[Char] = |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
some.path.[Double] = 2 | ||
some.bad.[Double] = two point five |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
some.path.[Int] = 7 | ||
some.bad.[Int] = seven |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
simple1 = hello | ||
simple2=well, hello there | ||
someInt.[Int] = 6 | ||
anotherInt.[Int] = 7 | ||
someLong.[Long] = 231 | ||
someFloat.[Float] = 23.21 | ||
someDouble.[Double]=25.222 | ||
someBoolean.[Boolean]=true | ||
someFalseBoolean.[Boolean] = false | ||
someChar.[Char] = a | ||
seq.of.strings.[Seq[String]] = hello, there, today | ||
some.person.[Person] = Wall, Dick, 25 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
simple1 = hello | ||
simple2=well, hello there | ||
someInt.[Int] = 6 | ||
anotherInt.[Int] = 7 | ||
someLong.[Long] = 231 | ||
someFloat.[Float] = 23.21 | ||
someDouble.[Double]=25.222 | ||
someBoolean.[Boolean]=true | ||
someFalseBoolean.[Boolean] = false | ||
someChar.[Char] = a |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
bacon = eggs | ||
salt=pepper | ||
fish = chips | ||
run= hide | ||
robot = laser eyes |
121 changes: 121 additions & 0 deletions
121
src/test/scala/com/escalatesoft/subcut/inject/util/PropertyFileModuleTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package com.escalatesoft.subcut.inject.util | ||
|
||
import org.scalatest.{SeveredStackTraces, Matchers, FunSuite} | ||
import java.io.File | ||
import com.escalatesoft.subcut.inject.BindingException | ||
|
||
/** | ||
* Test the property file module parsing | ||
*/ | ||
class PropertyFileModuleParserTest extends FunSuite with Matchers with SeveredStackTraces { | ||
|
||
test("should throw exception if passed non existent file") { | ||
val exc = intercept[BindingException] { | ||
PropertyFileModule(new File("nosuchfile.txt")) | ||
} | ||
|
||
exc.getMessage should be ("No file nosuchfile.txt found") | ||
} | ||
|
||
test("should parse a simple text file without error") { | ||
val bindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("simplestringbindings.txt").getFile)) | ||
} | ||
|
||
test("the bindings for simple text properties should be valid") { | ||
val bindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("simplestringbindings.txt").getFile)) | ||
bindings.inject[String](Some("fish")) should be ("chips") | ||
bindings.inject[String](Some("bacon")) should be ("eggs") | ||
bindings.inject[String](Some("salt")) should be ("pepper") | ||
bindings.inject[String](Some("robot")) should be ("laser eyes") | ||
bindings.inject[String](Some("run")) should be ("hide") | ||
bindings.listBindings.size should be (5) | ||
} | ||
|
||
test("should parse a typed properties binding file without error") { | ||
val bindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("propbindings.txt").getFile)) | ||
} | ||
|
||
test("the bindings for typed properties should be valid") { | ||
val bindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("propbindings.txt").getFile)) | ||
bindings.listBindings.size should be (10) | ||
bindings.inject[String](Some("simple1")) should be ("hello") | ||
bindings.inject[String](Some("simple2")) should be ("well, hello there") | ||
bindings.inject[Int](Some("someInt")) should be (6) | ||
bindings.inject[Int](Some("anotherInt")) should be (7) | ||
bindings.inject[Long](Some("someLong")) should be (231L) | ||
bindings.inject[Float](Some("someFloat")) should be (23.21F +- 0.0001F) | ||
bindings.inject[Double](Some("someDouble")) should be (25.222 +- 0.0001) | ||
bindings.inject[Boolean](Some("someBoolean")) should be (true) | ||
bindings.inject[Boolean](Some("someFalseBoolean")) should be (false) | ||
bindings.inject[Char](Some("someChar")) should be ('a') | ||
} | ||
|
||
test("the bindings should be typed to the correct type and not respond to requests for other types") { | ||
val bindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("propbindings.txt").getFile)) | ||
intercept[BindingException] { | ||
bindings.inject[Int](Some("simple1")) | ||
} | ||
intercept[BindingException] { | ||
bindings.inject[Float](Some("someDouble")) | ||
} | ||
intercept[BindingException] { | ||
bindings.inject[Boolean](Some("noSuchBoolean")) | ||
} | ||
intercept[BindingException] { | ||
bindings.inject[Char](Some("anotherInt")) | ||
} | ||
intercept[BindingException] { | ||
bindings.inject[Long](Some("someInt")) | ||
} | ||
} | ||
|
||
test("incorrect formats should throw BindingException on property module load") { | ||
assert(intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("badintprop.txt").getFile)) | ||
}.getMessage.contains ("""Could not parse "seven" for some.bad.[Int]""")) | ||
|
||
assert(intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("baddoubleprop.txt").getFile)) | ||
}.getMessage.contains ("""Could not parse "two point five" for some.bad.[Double]""")) | ||
|
||
assert(intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("badboolprop.txt").getFile)) | ||
}.getMessage.contains ("""Could not parse "Truee" for some.bad.[Boolean]""")) | ||
|
||
assert(intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("badcharprop.txt").getFile)) | ||
}.getMessage.contains ("""Could not parse "" for some.bad.[Char]""")) | ||
} | ||
|
||
val seqStringParser = new PropertyParser[Seq[String]] { | ||
def parse(propString: String): Seq[String] = propString.split(',').map(_.trim).toList | ||
} | ||
|
||
case class Person(first: String, last: String, age: Int) | ||
|
||
val personParser = new PropertyParser[Person] { | ||
def parse(propString: String): Person = { | ||
val fields = propString.split(',').map(_.trim) | ||
Person(fields(1), fields(0), fields(2).toInt) | ||
} | ||
} | ||
|
||
test("custom formats should be parsed when a custom parser is provided, but not otherwise") { | ||
intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("custompropbindings.txt").getFile)) | ||
}.getMessage should be ("No provided parser for type [Seq[String]] in properties custompropbindings.txt") | ||
|
||
val withSeqStringParser = PropertyMappings.Standard + ("Seq[String]" -> seqStringParser) | ||
|
||
intercept[BindingException] { | ||
PropertyFileModule(new File(getClass.getClassLoader.getResource("custompropbindings.txt").getFile), withSeqStringParser) | ||
}.getMessage should be ("No provided parser for type [Person] in properties custompropbindings.txt") | ||
|
||
val withBothParsers = PropertyMappings.Standard + ("Seq[String]" -> seqStringParser) + ("Person" -> personParser) | ||
|
||
val workingBindings = PropertyFileModule(new File(getClass.getClassLoader.getResource("custompropbindings.txt").getFile), withBothParsers) | ||
workingBindings.inject[Seq[String]](Some("seq.of.strings")) should be (List("hello", "there", "today")) | ||
workingBindings.inject[Person](Some("some.person")) should be (Person("Dick", "Wall", 25)) | ||
workingBindings.listBindings.size should be (12) | ||
} | ||
} |