Skip to main content

airframe-surface: Object Shape Inspector

airframe-surface enables reading type information, such as:

  • Constructor parameter names and their types
  • Methods defined in an object with method argument names and types.

Usage​

Maven Central Scaladoc

build.sbt

libraryDependencies += "org.wvlet.airframe" %% "airframe-surface" % "(version)"

// For Scala.js
libraryDependencies += "org.wvlet.airframe" %%% "airframe-surface" % "(version)"
  • Surface.of[X] returns parameters,
  • MethodSurface.of[X] returns the list of methods in X and their parameter names and types.
  • airframe-surface is compatible between Scala and Scala.js

Applications of Surface include:

  • Writing object serializer / deserializer without any boilerplates.
    • For example, airframe-codec Automatically generates code serialiation and deserizliation using Surface.
  • Dependency injection based on object shapes (e.g., Airframe DI)
    • airframe-di detects dependent types using Surface
  • Surface can be an indentifer of object types and type aliases.

Initial design note: Surface Design Document

Surface.of[X]​

import wvlet.airframe.surface.Surface

case class A(id:Int, name:String)

val s = Surface.of[A]
println(s.toString) // This will show A(id:Int, name:String)

// Find object parameters
s.params.mkString(", ") // Returns "id:Int, name:String"

// Object factory
s.objectFactory.map{ f =>
f.newInstance(Seq(1, "leo"))
}
// Some(A(1, "leo"))

Surface.methodsOf[X]​

Extract the public method definitions of a given class:

class A {
def hello(name:String): String = s"hello ${name}"
}

val methods = Surface.methodsOf[A]
val h = methods.head
h.name // "hello"
h.returnType // Surface.of[String]
h.args(0).surface // Surface.of[String]

Type alias​

type UserName = String

Surface.of[UserName] // Returns UserName:=String

In Scala 3, opaque type aliases are also supported:

opaque type UserName = String
Surface.of[UserName] // Returns UserName:=String

Warning: In Scala 3, due to the eager type alias expansion https://github.com/wvlet/airframe/issues/2200, the surface of a type alias can be the same surface of the referenced type. If you need to differentiate the same type with different names, use intersection types.

type UserName = String

// In Scala 3, this function can be resolved as String => Int:
val f1 = { (x: UserName) => x.toString.length }

// To resolve this as UserName => Int, explicitly specify the function type:
val f2: Funciton1[UserName, Int] = { (x: UserName) => x.toString.length }

Intersection Types​

In Scala3, you can use intersection types for labeling types.

For example, you can use String & Environment and String & Stage for differentiating the same type for different purposes:

import wvlet.airframe.surface.Surface

trait Environment
trait Stage

Surface.of[String & Environment]
Surface.of[String & Stage]

Union type A | B is also supported:

import wvlet.airframe.surface.Surface

Surface.of[String | Int]

Tagged Types​

To have different surfaces for the same type, you can use tagged type (@@):

import wvlet.airframe.surface.Surface
import wvlet.airframe.surface.tag._

class Fruit
trait Apple
trait Banana

Surface.of[Fruit @@ Apple]
Surface.of[Fruit @@ Banana]

Runtime Annotation​

Reading runtime-annotation is supported for JVM projects. Import wvlet.airframe.surface.reflect._ to use this feature.

import wvlet.airframe.surface.Surface
import wvlet.airframe.surface.reflect._
import javax.annotation.Resource

@Resource(name="my resource")
class A(@Resource(name = "param 1") a:String)

val s = Surface.of[A]
// Reading class annotation
val a = s.findAnnotationOf[Resource]
a.get.name // "my resource"

// Reading parameter annotation
val r = s.params.find(_.name == "a").findAnnotationOf[Resource]
r.get.name // "param 1"