diff --git a/README.md b/README.md index 03d5d48..9ccec07 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ +SubCut Extended README +============= + +SUBCUT 2.5 WILL ADD SUPPORT FOR PROPERTY INJECTION SO I'M GOING TO STOP ANY SUPPORT FOR THIS PROJECT WHEN IT WILL BE RELEASED + +This project is an extension of SubCut (https://github.com/dickwall/subcut) I'm keeping active while waiting for the extension +code to be merged in the original project. + +I added some feature to implement injection similarly to the @value annotation in spring. For example: + +```(scala) +class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val stringProperty = injectProperty[String]("property1") + val intProperty = injectProperty[Int]("property2") + val longProperty = injectProperty[Long]("property3") + val floatProperty = injectProperty[Float]("property4") +} +``` + +Allowing to inject in this way: + +```(scala) +{ + implicit val bindingModule = newBindingModuleWithConfig(PropertiesConfigPropertySource.fromPath("src/test/resources/test.properties")) + + val configReaderInstance = new ToInject +} +``` + +It supports injection of basic types and allows generic types injection if a custom conversion function is provided. + SubCut README ============= diff --git a/build.sbt b/build.sbt index 75dfd3b..5b0147d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,10 +1,10 @@ -name := "subcut" +name := "subcut_ext" -organization := "com.escalatesoft.subcut" +organization := "com.pragmasoft" -version := "2.0" +version := "2.1" -crossScalaVersions := Seq("2.10.0", "2.9.2", "2.9.1", "2.9.0-1", "2.9.0") +crossScalaVersions := Seq("2.10.0", "2.11.1") scalaVersion := "2.10.1" @@ -12,7 +12,7 @@ scalacOptions += "-deprecation" libraryDependencies += "junit" % "junit" % "4.5" % "test" -libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1" % "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "2.1.3" % "test" libraryDependencies <<= (scalaVersion, libraryDependencies) { (ver, deps) => deps :+ "org.scala-lang" % "scala-compiler" % ver @@ -53,4 +53,14 @@ pomExtra := ( Dick Wall http://about.me/dickwall - ) + + galarragas + Stefano Galarraga + + + + + conjars.org + http://conjars.org/repo/ + + ) diff --git a/pom.xml b/pom.xml index fc971b1..165e8ef 100644 --- a/pom.xml +++ b/pom.xml @@ -4,9 +4,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.escalatesoft.subcut - subcut - 2.0 + com.pragmasoft + subcut_ext + 2.1 2.10.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index cda13ac..3bf6a38 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ -resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" +resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" -addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0") +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") resolvers += Classpaths.typesafeResolver -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.0.0") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") diff --git a/src/main/scala/com/escalatesoft/subcut/inject/BindingModule.scala b/src/main/scala/com/escalatesoft/subcut/inject/BindingModule.scala index 75face0..707f6d7 100644 --- a/src/main/scala/com/escalatesoft/subcut/inject/BindingModule.scala +++ b/src/main/scala/com/escalatesoft/subcut/inject/BindingModule.scala @@ -7,23 +7,39 @@ package com.escalatesoft.subcut.inject * Time: 11:39 AM */ import scala.collection._ +import com.escalatesoft.subcut.inject.config._ +import com.escalatesoft.subcut.inject.config.Undefined +import scala.Some +import com.escalatesoft.subcut.inject.config.Defined /** * The binding key, used to uniquely identify the desired injection using the class and an optional name. */ private[inject] case class BindingKey[A](m: Manifest[A], name: Option[String]) + +trait ConfigPropertySourceProvider { + def configPropertySource: ConfigPropertySource +} + +trait WithoutConfigPropertySource extends ConfigPropertySourceProvider { + def configPropertySource: ConfigPropertySource = new ConfigPropertySource { + def getOptional(propertyName: String): ConfigProperty = throw new IllegalStateException("No ConfigPropertySource provided") + } +} + + /** * The main BindingModule trait. * Not intended to be used directly, instead extend NewBindingModule with a function to create the bindings * (recommended - the result will be immutable) or a MutableBindingModule (not recommended unless you know what * you are doing and take on the thread safety responsibility yourself). */ -trait BindingModule { outer => +trait BindingModule extends ConfigPropertySourceProvider { outer : ConfigPropertySourceProvider => /** Abstract binding map definition */ def bindings: immutable.Map[BindingKey[_], Any] - + /** * Merge this module with another. The resulting module will include all bindings from both modules, with this * module winning if there are common bindings (binding override). If you prefer symbolic operators, @@ -44,6 +60,8 @@ trait BindingModule { outer => case notLmip => key -> notLmip }}).toMap + + def configPropertySource: ConfigPropertySource = outer.configPropertySource } } @@ -66,6 +84,8 @@ trait BindingModule { outer => def modifyBindings[A](fn: MutableBindingModule => A): A = { val mutableBindings = new MutableBindingModule { bindings = outer.bindings + + def configPropertySource: ConfigPropertySource = outer.configPropertySource } fn(mutableBindings) } @@ -135,8 +155,33 @@ trait BindingModule { outer => for ((k, v) <- bindings) yield { k.toString + " -> " + v.toString } } + /** + * Retrieves a mandatory binding for class T for the giving property in the ConfigPropertySource owned by this module. + * If there is no ConfigPropertySource the binding will fail (use NewBindingModule.newBindingModuleWithConfig to create + * a config with ConfigPropertySource). + * + * @param propertyName The key to use to retrieve the property from the ConfigPropertySource + * @return The property value converted to the required type by the implicit property converter + */ + def injectPropertyMandatory[T <: Any](propertyName: String)(implicit m: scala.reflect.Manifest[T], propertyConverter: ConfigProperty => T): T = + propertyConverter( configPropertySource.get(propertyName) ) + + /** + * Retrieves an optional binding for class T for the giving property in the ConfigPropertySource owned by this module. + * If there is no ConfigPropertySource the binding will fail (use NewBindingModule.newBindingModuleWithConfig to create + * a config with ConfigPropertySource). + * + * @param propertyName The key to use to retrieve the property from the ConfigPropertySource + * @return An optional property value converted to the required type by the implicit property converter + */ + def injectPropertyOptional[T <: Any](propertyName: String)(implicit m: scala.reflect.Manifest[T], propertyConverter: ConfigProperty => T): Option[T] = + configPropertySource.getOptional(propertyName) match { + case property@Defined(_, _) => Some(propertyConverter(property)) + case Undefined(_) => None + } } + /** * A class to create a new, immutable, binding module. In order to work, the constructor of this class * takes a function to evaluate, and passes this on to a bindings method which can be used to resolve @@ -159,9 +204,21 @@ trait BindingModule { outer => * you want. The module will be frozen after creation of the bindings, but is mutable for the * time you are defining it with the DSL. */ -class NewBindingModule(fn: MutableBindingModule => Unit) extends BindingModule { +class NewBindingModule(fn: MutableBindingModule => Unit) extends BindingModule with WithoutConfigPropertySource { + lazy val bindings = { + val module = new Object with MutableBindingModule with WithoutConfigPropertySource + fn(module) + module.freeze().fixed.bindings + } +} + +class NewBindingModuleWithConfig(fn: MutableBindingModule => Unit)(implicit configProvider: ConfigPropertySource) extends BindingModule { + override val configPropertySource = configProvider + lazy val bindings = { - val module = new Object with MutableBindingModule + val module = new Object with MutableBindingModule { + def configPropertySource: ConfigPropertySource = configProvider + } fn(module) module.freeze().fixed.bindings } @@ -182,9 +239,19 @@ class NewBindingModule(fn: MutableBindingModule => Unit) extends BindingModule { * bind [QueryService] toSingle { new SlowInitQueryService } * } * + * + * Use the newBindingModuleWithConfig method if you need to inject from a ConfigPropertySource. That needs a + * ConfigPropertySource instance to be implicitly available. A version without any binding is available if you + * are just injecting properties using the injectProperty method */ object NewBindingModule { def newBindingModule(fn: MutableBindingModule => Unit): BindingModule = new NewBindingModule(fn) + + def newBindingModuleWithConfig(fn: MutableBindingModule => Unit)(implicit configProvider: ConfigPropertySource): BindingModule = + new NewBindingModuleWithConfig(fn)(configProvider) + + def newBindingModuleWithConfig(implicit configProvider: ConfigPropertySource): BindingModule = + new NewBindingModuleWithConfig( { mutableBindingModule: MutableBindingModule => } )(configProvider) } /** @@ -239,6 +306,7 @@ trait MutableBindingModule extends BindingModule { outer => def fixed: BindingModule = { new BindingModule { override val bindings = outer._bindings + def configPropertySource: ConfigPropertySource = outer.configPropertySource } } @@ -576,6 +644,22 @@ trait MutableBindingModule extends BindingModule { outer => name = None } + /** + * Bind to a new instance of the provided class for each injection. The value for this instance is read from + * the ConfigPropertySource available in the BindingModule and converted using an implicitly available converter from + * ConfigPropertySource (essentially a wrapper for String) to the target type. + * Converters for the basic types are already available, you need to provide a new one for special types. + * + * @param name + * @param source + * @param converter + * @tparam I + * @return + */ + def toProperty[I <: T](name: String)(implicit source: ConfigPropertySource, converter: ConfigProperty => I) { + toSingle( converter(source.get(name)) ) + } + /** * Part of the fluent interface in the DSL, identified by provides a name to attach to the binding key so that, * in combination with the trait type being bound, a unique key is formed. This form takes a string name, but @@ -622,3 +706,4 @@ trait MutableBindingModule extends BindingModule { outer => // and a parameterized bind method to kick it all off def bind[T <: Any](implicit m: scala.reflect.Manifest[T]) = new Bind[T]() } + diff --git a/src/main/scala/com/escalatesoft/subcut/inject/Injectable.scala b/src/main/scala/com/escalatesoft/subcut/inject/Injectable.scala index 6322670..d86aff5 100644 --- a/src/main/scala/com/escalatesoft/subcut/inject/Injectable.scala +++ b/src/main/scala/com/escalatesoft/subcut/inject/Injectable.scala @@ -1,5 +1,7 @@ package com.escalatesoft.subcut.inject +import com.escalatesoft.subcut.inject.config.{ConfigProperty, BasicPropertyConversions} + /** * The trait that provides dependency injection features for a class or object. To use this trait, * Mix it in to the class or object definition, and then define the abstract bindingModule which holds @@ -8,7 +10,7 @@ package com.escalatesoft.subcut.inject * or, perhaps most flexibly, an implicit constructor parameter in a curried parameter list. This last option * can provide flexible and mostly invisible bindings all the way down an object instance creation chain. */ -trait Injectable { +trait Injectable extends BasicPropertyConversions{ implicit def bindingModule: BindingModule /** @@ -167,6 +169,33 @@ trait Injectable { def injectOptional[T <: Any](name: String)(implicit m: scala.reflect.Manifest[T]): Option[T] = bindingModule.injectOptional[T](Some(name)) + /** + * Inject a required instance for the given trait as read from the ConfigPropertySource provided by the BindingModule + * and converted using an implicit converter from ConfigProperty (a wrapper of String properties) to the required target + * trait. + * + * If the module contains no configuration the injection will fail. The same will happen if the property cannot be + * found. + * + * Instances of ConfigPropertySource trait can be developed to allow the injection to retrieve from different sources. + * At the moment the only implementation available allows the use of the Java Properties class. + * + * @param propertyName The key in the ConfigPropertySource to load the property to be Inject + * @return An instance converted to the relevant type + */ + def injectProperty[T <: Any](propertyName: String)(implicit m: scala.reflect.Manifest[T], propertyConverter: ConfigProperty => T): T = + bindingModule.injectPropertyMandatory[T](propertyName) + + /** + * Inject an optional for the given trait as read from the ConfigPropertySource provided by the BindingModule + * and converted using an implicit converter from ConfigProperty (a wrapper of String properties) to the required target + * trait.. If there is no matching binding, this method will return None. + * + * @param propertyName The name of the property to be read + * @return An option with instance converted to the relevant type if found or None if not found + */ + def injectOptionalProperty[T <: Any](propertyName: String)(implicit m: scala.reflect.Manifest[T], propertyConverter: ConfigProperty => T): Option[T] = + bindingModule.injectPropertyOptional[T](propertyName) } /** diff --git a/src/main/scala/com/escalatesoft/subcut/inject/config/BasicPropertyConversions.scala b/src/main/scala/com/escalatesoft/subcut/inject/config/BasicPropertyConversions.scala new file mode 100644 index 0000000..b505c61 --- /dev/null +++ b/src/main/scala/com/escalatesoft/subcut/inject/config/BasicPropertyConversions.scala @@ -0,0 +1,27 @@ +package com.escalatesoft.subcut.inject.config + +object BasicPropertyConversions { + + implicit def _toString(input: ConfigProperty) : String = input.value + implicit def _toInt(input: ConfigProperty) : Int = augmentString(input.value).toInt + implicit def _toLong(input: ConfigProperty) : Long = augmentString(input.value).toLong + implicit def _toDouble(input: ConfigProperty) : Double = augmentString(input.value).toDouble + implicit def _toFloat(input: ConfigProperty) : Float = augmentString(input.value).toFloat + implicit def _toByte(input: ConfigProperty) : Byte = augmentString(input.value).toByte + implicit def _toShort(input: ConfigProperty) : Short = augmentString(input.value).toShort +} + +import BasicPropertyConversions._ +trait BasicPropertyConversions { + + implicit def toString(input: ConfigProperty) : String = _toString(input) + implicit def toInt(input: ConfigProperty) : Int = _toInt(input) + implicit def toLong(input: ConfigProperty) : Long = _toLong(input) + implicit def toDouble(input: ConfigProperty) : Double = _toDouble(input) + implicit def toFloat(input: ConfigProperty) : Float = _toFloat(input) + implicit def toByte(input: ConfigProperty) : Byte = _toByte(input) + implicit def toShort(input: ConfigProperty) : Short = _toShort(input) + +} + + diff --git a/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigProperty.scala b/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigProperty.scala new file mode 100644 index 0000000..15be26b --- /dev/null +++ b/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigProperty.scala @@ -0,0 +1,18 @@ +package com.escalatesoft.subcut.inject.config + +sealed trait ConfigProperty { + def name: String + def value: String + + def valueAs[T](implicit converter: ConfigProperty => T) : Option[T] +} + +case class Defined(name: String, value: String) extends ConfigProperty { + def valueAs[T](implicit converter: ConfigProperty => T) : Option[T] = Some( converter(this) ) +} + +case class Undefined(name: String) extends ConfigProperty { + def value = throw new IllegalArgumentException(s"Undefined value for property $name") + + def valueAs[T](implicit converter: ConfigProperty => T) : Option[T] = None +} diff --git a/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigPropertySource.scala b/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigPropertySource.scala new file mode 100644 index 0000000..3bc3cc2 --- /dev/null +++ b/src/main/scala/com/escalatesoft/subcut/inject/config/ConfigPropertySource.scala @@ -0,0 +1,10 @@ +package com.escalatesoft.subcut.inject.config + +trait ConfigPropertySource { + def get(propertyName: String) : ConfigProperty = getOptional(propertyName) match { + case property@Defined(_, value) => property + case Undefined(_) => throw new IllegalArgumentException(s"Missing property $propertyName in config") + } + + def getOptional(propertyName: String) : ConfigProperty +} diff --git a/src/main/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySource.scala b/src/main/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySource.scala new file mode 100644 index 0000000..20c2478 --- /dev/null +++ b/src/main/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySource.scala @@ -0,0 +1,39 @@ +package com.escalatesoft.subcut.inject.config + +import java.util.Properties +import java.io.{FileInputStream, File} + +class PropertiesConfigPropertySource(properties: Properties) extends ConfigPropertySource { + def getOptional(propertyName: String) : ConfigProperty = { + val prop = properties.getProperty(propertyName) + + if(prop != null) + Defined(propertyName, prop) + else + Undefined(propertyName) + } +} + +class PropertiesConfigMapSource(properties: Map[String, String]) extends ConfigPropertySource { + def getOptional(propertyName: String) : ConfigProperty = properties.get(propertyName) match { + case Some(prop) => Defined(propertyName, prop) + case None => Undefined(propertyName) + } +} + +object PropertiesConfigPropertySource { + def apply(properties: Properties) = new PropertiesConfigPropertySource(properties) + def apply(properties: Map[String, String]) = new PropertiesConfigMapSource(properties) + + def fromPath(path: String) = { + val properties = new Properties() + properties.load(new FileInputStream(new File(path))) + new PropertiesConfigPropertySource(properties) + } + + def fromClasspathResource(path: String) = { + val properties = new Properties() + properties.load(getClass.getClassLoader.getResourceAsStream(path)) + new PropertiesConfigPropertySource(properties) + } +} diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..17d42fd --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,4 @@ +property1=value1 +property2=2 +property3=3 +property4=4 \ No newline at end of file diff --git a/src/test/scala/com/escalatesoft/subcut/inject/ConfigPropertyBindingTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/ConfigPropertyBindingTest.scala new file mode 100644 index 0000000..0ddb53d --- /dev/null +++ b/src/test/scala/com/escalatesoft/subcut/inject/ConfigPropertyBindingTest.scala @@ -0,0 +1,207 @@ +package com.escalatesoft.subcut.inject + +import org.scalatest.FunSuite +import org.scalatest.matchers.ShouldMatchers + +import NewBindingModule._ +import com.escalatesoft.subcut.inject.config._ +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import java.util.Date +import java.text.SimpleDateFormat +import scala.concurrent.duration._ + +@RunWith(classOf[JUnitRunner]) +class ConfigPropertyBindingTest extends FunSuite with ShouldMatchers { + + + test("Should inject from config") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = injectProperty[String]("property1") + val property2 = injectProperty[String]("property2") + + def properties: List[Any] = List(property1, property2) + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "value1", "property2" -> "value2") + } + implicit val bindingModule = newBindingModuleWithConfig + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", "value2")) + } + + test("Should convert basic types") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = injectProperty[String]("property1") + val property2 = injectProperty[Int]("property2") + val property3 = injectProperty[Long]("property3") + val property4 = injectProperty[Float]("property4") + + def properties: List[Any] = List(property1, property2, property3, property4) + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "value1", "property2" -> "2", "property3" -> "3", "property4" -> "4.0") + } + implicit val bindingModule = newBindingModuleWithConfig { bindingModule => } + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } + + test("Should bind properties from config source") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = inject[String]("property1") + val property2 = inject[Int]("property2") + val property3 = inject[Long]("property3") + val property4 = inject[Float]("property4") + + def properties: List[Any] = List(property1, property2, property3, property4) + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "value1", "property2" -> "2", "property3" -> "3", "property4" -> "4.0") + } + + implicit val bindingModule = newBindingModuleWithConfig { bindingModule => + import bindingModule._ + import BasicPropertyConversions._ + + bind [String] idBy 'property1 toProperty "property1" + bind [Int] idBy 'property2 toProperty "property2" + bind [Long] idBy 'property3 toProperty "property3" + bind [Float] idBy 'property4 toProperty "property4" + } + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } + + test("Should optionally bind properties from config source") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = injectOptionalProperty[Int]("property1") getOrElse(-1) + val property2 = injectOptionalProperty[Int]("property2") getOrElse(-1) + + def properties: List[Any] = List(property1, property2) + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "100") + } + + implicit val bindingModule = newBindingModuleWithConfig + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List(100, -1)) + } + + test("Should bind custom type if a converter is provided") { + implicit def toDate(prop: ConfigProperty): Date = new SimpleDateFormat("yyyy-mm-DD").parse(prop.value) + + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = inject[Date]("property1") + val property2 = injectProperty[Date]("property2") + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "2014-01-15", "property2" -> "2014-01-15") + } + + implicit val bindingModule = newBindingModuleWithConfig { bindingModule => + import bindingModule._ + + bind [Date] idBy 'property1 toProperty "property1" + } + + val configReaderInstance = new ToInject + + configReaderInstance.property1 should equal (new SimpleDateFormat("yyyy-mm-DD").parse("2014-01-15")) + configReaderInstance.property2 should equal (new SimpleDateFormat("yyyy-mm-DD").parse("2014-01-15")) + } + + test("Operators on binding modules are bringing around the config properties provider - modifyBindings") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = inject[String]("property1") + val property2 = inject[Int]("property2") + val property3 = inject[Long]("property3") + val property4 = inject[Float]("property4") + + def properties: List[Any] = List(property1, property2, property3, property4) + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "property1" -> "value1", "property2" -> "2", "property3" -> "3", "property4" -> "4.0") + } + + implicit val bindingModule = newBindingModuleWithConfig { bindingModule => + import bindingModule._ + import BasicPropertyConversions._ + + bind [String] idBy 'property1 toProperty "property1" + bind [Int] idBy 'property2 toProperty "property2" + bind [Long] idBy 'property3 toProperty "property3" + bind [Float] idBy 'property4 toProperty "property4" + } + + bindingModule modifyBindings { implicit bindingModule => + import bindingModule._ + + bind [String] idBy 'property1 toSingle "value1-MODIFIED" + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1-MODIFIED", 2, 3l, 4.0)) + } + } + + test("You can write more esoteric conversion using property name") { + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val timeout1 = injectProperty[Duration]("timeout.millis") + val timeout2 = inject[Duration]("timeout2") + } + + implicit def toDuration(prop: ConfigProperty): Duration = { + val amount: Long = prop.value.toLong + val timeQualifier: String = prop.name.split('.').last + + timeQualifier match { + case "seconds" | "secs" => amount.seconds + case "hours" => amount.hours + case _ => amount.milliseconds + } + } + + implicit val propertyProvider = PropertiesConfigPropertySource { + Map( "timeout.millis" -> "100", "timeout.seconds" -> "20") + } + + implicit val bindingModule = newBindingModuleWithConfig { bindingModule => + import bindingModule._ + + bind [Duration] idBy "timeout2" toProperty "timeout.seconds" + } + + val configReaderInstance = new ToInject + + configReaderInstance.timeout1 should equal (100.milliseconds) + configReaderInstance.timeout2 should equal (20.seconds) + } + + test("you can use config source to directly access properties too") { + import BasicPropertyConversions._ + + val config = PropertiesConfigPropertySource { + Map( "intProp" -> "3", "doubleProp" -> "4.0") + } + + config.getOptional("intProp").valueAs[Int] should equal (Some(3)) + config.getOptional("doubleProp").valueAs[Double] should equal (Some(4.0)) + config.getOptional("otherProp").valueAs[Double] should equal (None) + } +} diff --git a/src/test/scala/com/escalatesoft/subcut/inject/FrozenModuleTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/FrozenModuleTest.scala index 7aaabe0..5ab10c6 100644 --- a/src/test/scala/com/escalatesoft/subcut/inject/FrozenModuleTest.scala +++ b/src/test/scala/com/escalatesoft/subcut/inject/FrozenModuleTest.scala @@ -82,7 +82,7 @@ class MultFunc extends MathFunc { override def doIt(x: Int, y: Int): Int = x * y } -object MathModule extends MutableBindingModule +object MathModule extends MutableBindingModule with WithoutConfigPropertySource trait MathInjector extends BoundToModule { override val bindingModule = MathModule diff --git a/src/test/scala/com/escalatesoft/subcut/inject/ModuleCompositionAndMergingTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/ModuleCompositionAndMergingTest.scala index 11f8cb6..a0a7e3e 100644 --- a/src/test/scala/com/escalatesoft/subcut/inject/ModuleCompositionAndMergingTest.scala +++ b/src/test/scala/com/escalatesoft/subcut/inject/ModuleCompositionAndMergingTest.scala @@ -60,7 +60,7 @@ object StandardConfiguration extends NewBindingModule({ module => bind [String] idBy 'version toSingle "1.3" }) -class FromProperties extends BindingModule { +class FromProperties extends BindingModule with WithoutConfigPropertySource { val bindings = { val newMod = new NewBindingModule({ module => import module._ diff --git a/src/test/scala/com/escalatesoft/subcut/inject/PlainScalaInjectInBindingTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/PlainScalaInjectInBindingTest.scala index e8bb99a..d43eb64 100644 --- a/src/test/scala/com/escalatesoft/subcut/inject/PlainScalaInjectInBindingTest.scala +++ b/src/test/scala/com/escalatesoft/subcut/inject/PlainScalaInjectInBindingTest.scala @@ -60,7 +60,7 @@ class PlainScalaInjectInBindingTest extends FunSuite with ShouldMatchers { } //all implementations unaware of framework, inject called during module definition - object EchoModule extends MutableBindingModule with Injectable { + object EchoModule extends MutableBindingModule with WithoutConfigPropertySource with Injectable { val bindingModule = this diff --git a/src/test/scala/com/escalatesoft/subcut/inject/PushBindingsTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/PushBindingsTest.scala index 6f2b236..a3186d8 100644 --- a/src/test/scala/com/escalatesoft/subcut/inject/PushBindingsTest.scala +++ b/src/test/scala/com/escalatesoft/subcut/inject/PushBindingsTest.scala @@ -87,15 +87,15 @@ class PushBindingsTest extends FunSuite with ShouldMatchers with SeveredStackTra } -object PushBindingsTestModule1 extends MutableBindingModule { +object PushBindingsTestModule1 extends MutableBindingModule with WithoutConfigPropertySource { bind [BoundTrait1] toSingle new T1Impl1 } -object PushBindingsTestModule2 extends MutableBindingModule { +object PushBindingsTestModule2 extends MutableBindingModule with WithoutConfigPropertySource { bind [BoundTrait2] toSingle new T2Impl1 } -object PushBindingsTestModule extends MutableBindingModule { +object PushBindingsTestModule extends MutableBindingModule with WithoutConfigPropertySource { withBindingModules(PushBindingsTestModule1, PushBindingsTestModule2) } diff --git a/src/test/scala/com/escalatesoft/subcut/inject/TrialBindingModule.scala b/src/test/scala/com/escalatesoft/subcut/inject/TrialBindingModule.scala index c9dcbdc..c99ea22 100644 --- a/src/test/scala/com/escalatesoft/subcut/inject/TrialBindingModule.scala +++ b/src/test/scala/com/escalatesoft/subcut/inject/TrialBindingModule.scala @@ -68,7 +68,7 @@ class InjectionTest extends FunSuite with ShouldMatchers with SeveredStackTraces } } -object TrialBindingModule extends MutableBindingModule { +object TrialBindingModule extends MutableBindingModule with WithoutConfigPropertySource { bind [TestTrait] toProvider { new TestImpl } bind [TestTrait] identifiedBy 'testConfig toProvider (new AlternativeImpl) bind [TestTrait] identifiedBy "something else" toProvider { new AlternativeImpl } diff --git a/src/test/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySourceTest.scala b/src/test/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySourceTest.scala new file mode 100644 index 0000000..9684dea --- /dev/null +++ b/src/test/scala/com/escalatesoft/subcut/inject/config/PropertiesConfigPropertySourceTest.scala @@ -0,0 +1,66 @@ +package com.escalatesoft.subcut.inject.config + +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.FunSuite +import com.escalatesoft.subcut.inject.{Injectable, BindingModule} +import com.escalatesoft.subcut.inject.NewBindingModule._ +import java.util.Properties +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class PropertiesConfigPropertySourceTest extends FunSuite with ShouldMatchers { + + class ToInject(implicit val bindingModule: BindingModule) extends Injectable { + val property1 = injectProperty[String]("property1") + val property2 = injectProperty[Int]("property2") + val property3 = injectProperty[Long]("property3") + val property4 = injectProperty[Float]("property4") + + def properties: List[Any] = List(property1, property2, property3, property4) + } + + test("should load from properties") { + val properties = new Properties() + properties.put("property1", "value1") + properties.put("property2", "2") + properties.put("property3", "3") + properties.put("property4", "4") + implicit val propertySource = PropertiesConfigPropertySource(properties) + + implicit val bindingModule = newBindingModuleWithConfig + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } + + test("should load from classpath") { + implicit val propertySource = PropertiesConfigPropertySource.fromClasspathResource("test.properties") + + implicit val bindingModule = newBindingModuleWithConfig + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } + + test("should load from path") { + implicit val propertySource = PropertiesConfigPropertySource.fromPath("src/test/resources/test.properties") + + implicit val bindingModule = newBindingModuleWithConfig + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } + + test("should work without implicit too") { + + implicit val bindingModule = newBindingModuleWithConfig(PropertiesConfigPropertySource.fromPath("src/test/resources/test.properties")) + + val configReaderInstance = new ToInject + + configReaderInstance.properties should equal (List("value1", 2, 3l, 4.0)) + } +}