From 6284e97ca6ed3c39e10fce2e25b410e51008ba8b Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 19 Jun 2024 09:48:46 -0700 Subject: [PATCH] di (feature): Add scalafmt-friendly DI bind syntaxes (#3567) --- .../wvlet/airframe/AirframeMacros.scala | 80 +++++++++++++++++++ .../scala-2/wvlet/airframe/DesignImpl.scala | 12 +++ .../scala-3/wvlet/airframe/DesignImpl.scala | 17 ++++ .../wvlet/airframe/di/NewDISyntaxTest.scala | 54 +++++++++++++ docs/airframe-di.md | 24 ++++++ 5 files changed, 187 insertions(+) create mode 100644 airframe-di/src/test/scala/wvlet/airframe/di/NewDISyntaxTest.scala diff --git a/airframe-di-macros/src/main/scala-2/wvlet/airframe/AirframeMacros.scala b/airframe-di-macros/src/main/scala-2/wvlet/airframe/AirframeMacros.scala index 4354cfa520..0d90965983 100644 --- a/airframe-di-macros/src/main/scala-2/wvlet/airframe/AirframeMacros.scala +++ b/airframe-di-macros/src/main/scala-2/wvlet/airframe/AirframeMacros.scala @@ -253,6 +253,86 @@ private[wvlet] object AirframeMacros { h.registerTraitFactory(t) } + def designBindInstanceImpl[A: c.WeakTypeTag](c: sm.Context)(obj: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + q"""${c.prefix}.bind[${t}].toInstance(${obj})""" + } + + def designBindSingletonImpl[A: c.WeakTypeTag](c: sm.Context): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + q"""${c.prefix}.bind[${t}].toSingleton""" + } + + def designBindImplImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: sm.Context): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + q"""${c.prefix}.bind[${t}].to[${implicitly[c.WeakTypeTag[B]].tpe}]""" + } + + def designBindProvider1Impl[D1: c.WeakTypeTag, A: c.WeakTypeTag](c: sm.Context)(f: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + val d1 = implicitly[c.WeakTypeTag[D1]].tpe + q"""${c.prefix}.bind[${t}].toProvider[${d1}](${f})""" + } + + def designBindProvider2Impl[D1: c.WeakTypeTag, D2: c.WeakTypeTag, A: c.WeakTypeTag]( + c: sm.Context + )(f: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + val d1 = implicitly[c.WeakTypeTag[D1]].tpe + val d2 = implicitly[c.WeakTypeTag[D2]].tpe + q"""${c.prefix}.bind[${t}].toProvider[${d1}, ${d2}](${f})""" + } + + def designBindProvider3Impl[D1: c.WeakTypeTag, D2: c.WeakTypeTag, D3: c.WeakTypeTag, A: c.WeakTypeTag]( + c: sm.Context + )(f: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + val d1 = implicitly[c.WeakTypeTag[D1]].tpe + val d2 = implicitly[c.WeakTypeTag[D2]].tpe + val d3 = implicitly[c.WeakTypeTag[D3]].tpe + q"""${c.prefix}.bind[${t}].toProvider[${d1}, ${d2}, ${d3}](${f})""" + } + + def designBindProvider4Impl[ + D1: c.WeakTypeTag, + D2: c.WeakTypeTag, + D3: c.WeakTypeTag, + D4: c.WeakTypeTag, + A: c.WeakTypeTag + ](c: sm.Context)(f: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + val d1 = implicitly[c.WeakTypeTag[D1]].tpe + val d2 = implicitly[c.WeakTypeTag[D2]].tpe + val d3 = implicitly[c.WeakTypeTag[D3]].tpe + val d4 = implicitly[c.WeakTypeTag[D4]].tpe + q"""${c.prefix}.bind[${t}].toProvider[${d1}, ${d2}, ${d3}, ${d4}](${f})""" + } + + def designBindProvider5Impl[ + D1: c.WeakTypeTag, + D2: c.WeakTypeTag, + D3: c.WeakTypeTag, + D4: c.WeakTypeTag, + D5: c.WeakTypeTag, + A: c.WeakTypeTag + ](c: sm.Context)(f: c.Tree): c.Tree = { + import c.universe.* + val t = implicitly[c.WeakTypeTag[A]].tpe + val d1 = implicitly[c.WeakTypeTag[D1]].tpe + val d2 = implicitly[c.WeakTypeTag[D2]].tpe + val d3 = implicitly[c.WeakTypeTag[D3]].tpe + val d4 = implicitly[c.WeakTypeTag[D4]].tpe + val d5 = implicitly[c.WeakTypeTag[D5]].tpe + q"""${c.prefix}.bind[${t}].toProvider[${d1}, ${d2}, ${d3}, ${d4}, ${d5}](${f})""" + } + def designBindImpl[A: c.WeakTypeTag](c: sm.Context): c.Tree = { import c.universe.* val t = implicitly[c.WeakTypeTag[A]].tpe diff --git a/airframe-di/src/main/scala-2/wvlet/airframe/DesignImpl.scala b/airframe-di/src/main/scala-2/wvlet/airframe/DesignImpl.scala index 991fff03fc..95e7c52aab 100644 --- a/airframe-di/src/main/scala-2/wvlet/airframe/DesignImpl.scala +++ b/airframe-di/src/main/scala-2/wvlet/airframe/DesignImpl.scala @@ -13,6 +13,18 @@ private[airframe] trait DesignImpl extends LogSupport { def bind[A]: Binder[A] = macro AirframeMacros.designBindImpl[A] def remove[A]: Design = macro AirframeMacros.designRemoveImpl[A] + def bindInstance[A](obj: A): Design = macro AirframeMacros.designBindInstanceImpl[A] + def bindSingleton[A]: Design = macro AirframeMacros.designBindSingletonImpl[A] + def bindImpl[A, B <: A]: Design = macro AirframeMacros.designBindImplImpl[A, B] + def bindProvider[D1, A](f: D1 => A): Design = macro AirframeMacros.designBindProvider1Impl[D1, A] + def bindProvider[D1, D2, A](f: (D1, D2) => A): Design = macro AirframeMacros.designBindProvider2Impl[D1, D2, A] + def bindProvider[D1, D2, D3, A](f: (D1, D2, D3) => A): Design = + macro AirframeMacros.designBindProvider3Impl[D1, D2, D3, A] + def bindProvider[D1, D2, D3, D4, A](f: (D1, D2, D3, D4) => A): Design = + macro AirframeMacros.designBindProvider4Impl[D1, D2, D3, D4, A] + def bindProvider[D1, D2, D3, D4, D5, A](f: (D1, D2, D3, D4, D5) => A): Design = + macro AirframeMacros.designBindProvider5Impl[D1, D2, D3, D4, D5, A] + /** * A helper method of creating a new session and an instance of A. This method is useful when you only need to use A * as an entry point of your program. After executing the body, the sesion will be closed. diff --git a/airframe-di/src/main/scala-3/wvlet/airframe/DesignImpl.scala b/airframe-di/src/main/scala-3/wvlet/airframe/DesignImpl.scala index 9b146e8097..532892688f 100644 --- a/airframe-di/src/main/scala-3/wvlet/airframe/DesignImpl.scala +++ b/airframe-di/src/main/scala-3/wvlet/airframe/DesignImpl.scala @@ -18,6 +18,23 @@ private[airframe] trait DesignImpl extends LogSupport: val target = Surface.of[A] new Design(self.designOptions, self.binding.filterNot(_.from == target), self.hooks) + inline def bindInstance[A](obj: A): Design = + bind[A].toInstance(obj) + inline def bindSingleton[A]: Design = + bind[A].toSingleton + inline def bindImpl[A, B <: A]: Design = + bind[A].to[B] + inline def bindProvider[D1, A](f: D1 => A): Design = + bind[A].toProvider[D1](f) + inline def bindProvider[D1, D2, A](f: (D1, D2) => A): Design = + bind[A].toProvider[D1, D2](f) + inline def bindProvider[D1, D2, D3, A](f: (D1, D2, D3) => A): Design = + bind[A].toProvider[D1, D2, D3](f) + inline def bindProvider[D1, D2, D3, D4, A](f: (D1, D2, D3, D4) => A): Design = + bind[A].toProvider[D1, D2, D3, D4](f) + inline def bindProvider[D1, D2, D3, D4, D5, A](f: (D1, D2, D3, D4, D5) => A): Design = + bind[A].toProvider[D1, D2, D3, D4, D5](f) + /** * A helper method of creating a new session and an instance of A. This method is useful when you only need to use A * as an entry point of your program. After executing the body, the sesion will be closed. diff --git a/airframe-di/src/test/scala/wvlet/airframe/di/NewDISyntaxTest.scala b/airframe-di/src/test/scala/wvlet/airframe/di/NewDISyntaxTest.scala new file mode 100644 index 0000000000..65b03dfbca --- /dev/null +++ b/airframe-di/src/test/scala/wvlet/airframe/di/NewDISyntaxTest.scala @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package wvlet.airframe.di + +import wvlet.airframe.* +import wvlet.airspec.AirSpec + +object NewDISyntaxTest extends AirSpec { + trait A + case class AImpl() extends A + case class B(s: String) + case class D1(n: String) + case class D3(d1: Int, d2: Long, d3: String) + case class D4(i: Int, l: Long, d: String, b: B) + case class D5(i: Int, l: Long, d: String, b: B, c: D3) + + test("new bind syntax") { + val d = newDesign + .bindInstance[String]("hello") + .bindSingleton[B] + .bindImpl[A, AImpl] + .bindProvider[String, Int] { (s: String) => s.length } + .bindProvider { (a: A) => D1(a.toString) } + .bindProvider { (i: Int, s: String) => (i + s.length).toLong } + .bindProvider { (a: Int, b: Long, c: String) => D3(a, b, c) } + .bindProvider { (a: Int, b: Long, c: String, d: B) => D4(a, b, c, d) } + .bindProvider { (a: Int, b: Long, c: String, d: B, cc: D3) => D5(a, b, c, d, cc) } + .noLifeCycleLogging + + d.withSession { session => + session.build[String] shouldBe "hello" + session.build[B] shouldBe B("hello") + session.build[A] shouldBe AImpl() + session.build[Int] shouldBe 5 + session.build[D1] shouldBe D1("AImpl()") + + session.build[Long] shouldBe 10L + session.build[D3] shouldBe D3(5, 10, "hello") + session.build[D4] shouldBe D4(5, 10, "hello", B("hello")) + session.build[D5] shouldBe D5(5, 10, "hello", B("hello"), D3(5, 10, "hello")) + } + } +} diff --git a/docs/airframe-di.md b/docs/airframe-di.md index ee7d9bfebb..d7aec4b6ff 100644 --- a/docs/airframe-di.md +++ b/docs/airframe-di.md @@ -53,6 +53,15 @@ val d = newDesign .bind[Y].to[YImpl] ``` +Alternatively, you can use new bind syntaxes introduced in Airframe 24.6.1: + +```scala +val d = newDesign + .bindInstance[X](...) + .bindImpl[Y, YImpl] +``` + + ### Basic Usage First, create a class that has some parameters as dependencies. For example, the following code defines an App class having X, Y, and Z as its dependencies: @@ -144,6 +153,21 @@ val design: Design = If you define multiple bindings to the same type (e.g., P), the last binding will have the highest precedence. +Single version 24.6.1, Airframe DI supports the following short-hand binding syntaxes: + +```scala +val design: Design = + newDesign + .bindSingleton[A] // Bind A to a singleton instance of A + .bindInstance[B](new B(1)) // Bind B to a concrete instance of B + .bindImpl[A, AImpl] // Bind A to AImpl + .bindProvider{ (d1:D1) => P(d1) } // Bind P using a provider function + .bindProvider{ (d1: D1, d2: D2) => P(d1, d2) } // Bind P using a provider function + ... + .bindProvider{ (d1 D1, ..., d5: D5) => P(d1, ..., d5) } // Up to 5 arguments +``` +This syntax works well with code formatter tools like [scalafmt](https://scalameta.org/scalafmt/). + ### Design is Immutable