diff --git a/.idea/highlighting.xml b/.idea/highlighting.xml index e239317..f33b64d 100644 --- a/.idea/highlighting.xml +++ b/.idea/highlighting.xml @@ -2,6 +2,7 @@ diff --git a/src/main/scala/com/escalatesoft/subcut/inject/util/PropertyFileModule.scala b/src/main/scala/com/escalatesoft/subcut/inject/util/PropertyFileModule.scala new file mode 100644 index 0000000..13224d3 --- /dev/null +++ b/src/main/scala/com/escalatesoft/subcut/inject/util/PropertyFileModule.scala @@ -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)) +} \ No newline at end of file diff --git a/src/test/resources/badboolprop.txt b/src/test/resources/badboolprop.txt new file mode 100644 index 0000000..72cd789 --- /dev/null +++ b/src/test/resources/badboolprop.txt @@ -0,0 +1,2 @@ +some.path.[Boolean] = false +some.bad.[Boolean] = Truee \ No newline at end of file diff --git a/src/test/resources/badcharprop.txt b/src/test/resources/badcharprop.txt new file mode 100644 index 0000000..181cafb --- /dev/null +++ b/src/test/resources/badcharprop.txt @@ -0,0 +1,2 @@ +some.path.[Char] = 7 +some.bad.[Char] = \ No newline at end of file diff --git a/src/test/resources/baddoubleprop.txt b/src/test/resources/baddoubleprop.txt new file mode 100644 index 0000000..5ea3fa0 --- /dev/null +++ b/src/test/resources/baddoubleprop.txt @@ -0,0 +1,2 @@ +some.path.[Double] = 2 +some.bad.[Double] = two point five \ No newline at end of file diff --git a/src/test/resources/badintprop.txt b/src/test/resources/badintprop.txt new file mode 100644 index 0000000..ab5b035 --- /dev/null +++ b/src/test/resources/badintprop.txt @@ -0,0 +1,2 @@ +some.path.[Int] = 7 +some.bad.[Int] = seven \ No newline at end of file diff --git a/src/test/resources/custompropbindings.txt b/src/test/resources/custompropbindings.txt new file mode 100644 index 0000000..686d257 --- /dev/null +++ b/src/test/resources/custompropbindings.txt @@ -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 diff --git a/src/test/resources/propbindings.txt b/src/test/resources/propbindings.txt new file mode 100644 index 0000000..7b7a5be --- /dev/null +++ b/src/test/resources/propbindings.txt @@ -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 diff --git a/src/test/resources/simplestringbindings.txt b/src/test/resources/simplestringbindings.txt new file mode 100644 index 0000000..3ab36aa --- /dev/null +++ b/src/test/resources/simplestringbindings.txt @@ -0,0 +1,5 @@ +bacon = eggs +salt=pepper +fish = chips +run= hide +robot = laser eyes \ No newline at end of file diff --git a/src/test/scala/com/escalatesoft/subcut/inject/util/PropertyFileModuleTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/util/PropertyFileModuleTest.scala new file mode 100644 index 0000000..3464856 --- /dev/null +++ b/src/test/scala/com/escalatesoft/subcut/inject/util/PropertyFileModuleTest.scala @@ -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) + } +}