Skip to content

Commit

Permalink
Added support for typed properties file parsing for config, plus tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
dickwall committed Feb 13, 2014
1 parent 751bfd6 commit 8a11755
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 0 deletions.
1 change: 1 addition & 0 deletions .idea/highlighting.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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))
}
2 changes: 2 additions & 0 deletions src/test/resources/badboolprop.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
some.path.[Boolean] = false
some.bad.[Boolean] = Truee
2 changes: 2 additions & 0 deletions src/test/resources/badcharprop.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
some.path.[Char] = 7
some.bad.[Char] =
2 changes: 2 additions & 0 deletions src/test/resources/baddoubleprop.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
some.path.[Double] = 2
some.bad.[Double] = two point five
2 changes: 2 additions & 0 deletions src/test/resources/badintprop.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
some.path.[Int] = 7
some.bad.[Int] = seven
12 changes: 12 additions & 0 deletions src/test/resources/custompropbindings.txt
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
10 changes: 10 additions & 0 deletions src/test/resources/propbindings.txt
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
5 changes: 5 additions & 0 deletions src/test/resources/simplestringbindings.txt
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
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)
}
}

0 comments on commit 8a11755

Please sign in to comment.