CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-softwaremill-macwire--macros

Zero-cost, compile-time, type-safe dependency injection library providing macros for automatic instance creation in Scala applications

Pending
Overview
Eval results
Files

dynamic-instance-access.mddocs/

Dynamic Instance Access

Dynamic instance access provides runtime lookup capabilities for instances using the wiredInModule function. This creates a Wired instance that maps types to factory functions based on the public members of an object, enabling dynamic dependency injection patterns.

Platform Availability: This functionality is only fully implemented in Scala 2. The Scala 3 version exists but is not yet implemented (returns ???).

Core Functions

wiredInModule

Creates a Wired instance that provides dynamic access to instances based on the public members of the given object.

def wiredInModule(in: AnyRef): Wired  // Scala 2 only

Parameters:

  • in: AnyRef - Object whose public members will be used to create instance mappings

Returns: Wired instance containing type-to-factory mappings

Behavior:

  • Analyzes public nullary methods (methods with no parameters) of the input object
  • Creates a mapping from each method's return type to a factory function that calls the method
  • Excludes methods that don't return AnyRef subtypes
  • Returns a Wired instance from the util module for dynamic instance lookup

Wired Type

The Wired type provides dynamic instance access capabilities:

// From com.softwaremill.macwire.util module
class Wired(protected val instanceFactoryMap: InstanceFactoryMap) extends InstanceLookup with DynamicInstantiate

trait InstanceLookup {
  def lookup[T](cls: Class[T]): List[T]
  def lookupSingleOrThrow[T](cls: Class[T]): T
}

trait DynamicInstantiate {
  def wireClassInstanceByName(className: String): Any
  def wireClassInstance[T](cls: Class[T]): T
}

// Note: InstanceFactoryMap is private to macwire
private[macwire] type InstanceFactoryMap = Map[Class[_], () => AnyRef]

Usage Examples

Basic Module Wiring

import com.softwaremill.macwire._

class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)

object UserModule {
  lazy val databaseAccess = new DatabaseAccess()
  lazy val securityFilter = new SecurityFilter()  
  lazy val userFinder = new UserFinder(databaseAccess, securityFilter)
}

// Create dynamic access to UserModule members
val wired = wiredInModule(UserModule)

// Dynamic lookup by type
val db: List[DatabaseAccess] = wired.lookup(classOf[DatabaseAccess])
val security: SecurityFilter = wired.lookupSingleOrThrow(classOf[SecurityFilter])

Service Registry Pattern

trait EmailService
trait NotificationService  
trait LoggingService

class SmtpEmailService extends EmailService
class PushNotificationService extends NotificationService
class FileLoggingService extends LoggingService

object ServiceRegistry {
  def emailService: EmailService = new SmtpEmailService()
  def notificationService: NotificationService = new PushNotificationService()
  def loggingService: LoggingService = new FileLoggingService()
}

val serviceRegistry = wiredInModule(ServiceRegistry)

// Dynamic service lookup
val emailSvc = serviceRegistry.lookupSingleOrThrow(classOf[EmailService])
val notificationSvc = serviceRegistry.lookup(classOf[NotificationService])

Configuration-Based Wiring

class AppConfig(environment: String) {
  def isDevelopment: Boolean = environment == "dev"
  
  def databaseUrl: String = if (isDevelopment) "jdbc:h2:mem:test" else "jdbc:postgresql://prod-db/app"
  def cacheSize: Int = if (isDevelopment) 100 else 10000
  def logger: Logger = if (isDevelopment) new ConsoleLogger() else new FileLogger("app.log")
}

val config = new AppConfig("dev")
val wiredConfig = wiredInModule(config)

// Access configuration values dynamically
val dbUrl: String = wiredConfig.lookupSingleOrThrow(classOf[String])  // Note: may be ambiguous
val logger: Logger = wiredConfig.lookupSingleOrThrow(classOf[Logger])

Plugin System Implementation

trait Plugin {
  def name: String
  def version: String
}

class DatabasePlugin extends Plugin {
  def name = "database"
  def version = "1.0"
}

class CachePlugin extends Plugin {
  def name = "cache" 
  def version = "2.0"
}

object PluginManager {
  def databasePlugin: Plugin = new DatabasePlugin()
  def cachePlugin: Plugin = new CachePlugin()
}

val pluginRegistry = wiredInModule(PluginManager)

// Dynamically access available plugins
val dbPlugin = pluginRegistry.lookup(classOf[Plugin])  // Returns list of Plugin instances

Runtime Service Discovery

import scala.reflect.ClassTag

class ServiceDiscovery(modules: AnyRef*) {
  private val allWired = modules.map(wiredInModule)
  
  def findService[T](cls: Class[T]): Option[T] = {
    allWired.view.flatMap(_.lookup(cls)).headOption
  }
  
  def getAllServices[T](cls: Class[T]): List[T] = {
    allWired.flatMap(_.lookup(cls))
  }
}

object DatabaseModule {
  def databaseAccess: DatabaseAccess = new DatabaseAccess()
  def userRepository: UserRepository = new UserRepository()
}

object ServiceModule {
  def emailService: EmailService = new EmailService()
  def notificationService: NotificationService = new NotificationService()
}

val discovery = new ServiceDiscovery(DatabaseModule, ServiceModule)
val dbAccess = discovery.findService(classOf[DatabaseAccess])
val allServices = discovery.getAllServices(classOf[Service])  // If Service is a common base type

Dynamic Dependency Injection

class DynamicInjector(wired: Wired) {
  def createInstance[T](factory: DependencyFactory[T])(implicit tag: ClassTag[T]): T = {
    factory.create(wired)
  }
}

trait DependencyFactory[T] {
  def create(wired: Wired): T
}

object UserServiceFactory extends DependencyFactory[UserService] {
  def create(wired: Wired): UserService = {
    val db = wired.lookupSingleOrThrow(classOf[DatabaseAccess])
    val security = wired.lookupSingleOrThrow(classOf[SecurityFilter])
    new UserService(db, security)
  }
}

val injector = new DynamicInjector(wiredInModule(AppModule))
val userService = injector.createInstance(UserServiceFactory)

Advanced Patterns

Type-Safe Registry

class TypedRegistry[T](wired: Wired, cls: Class[T]) {
  def get: List[T] = wired.lookup(cls)
  def getFirst: Option[T] = get.headOption
  def getSingleOrThrow: T = wired.lookupSingleOrThrow(cls)
}

object RegistryFactory {
  def createTypedRegistry[T](module: AnyRef, cls: Class[T]): TypedRegistry[T] = {
    new TypedRegistry[T](wiredInModule(module), cls)
  }
}

val dbRegistry = RegistryFactory.createTypedRegistry(DatabaseModule, classOf[DatabaseAccess])
val database = dbRegistry.getSingleOrThrow

Module Composition

object CompositeModule {
  private val database = wiredInModule(DatabaseModule)
  private val services = wiredInModule(ServiceModule)
  
  def lookup[T](cls: Class[T]): List[T] = {
    database.lookup(cls) ++ services.lookup(cls)
  }
}

// Unified lookup across multiple modules
val db = CompositeModule.lookup(classOf[DatabaseAccess])
val email = CompositeModule.lookup(classOf[EmailService])

Behavior and Limitations

Member Analysis

  • Only public nullary methods (no parameters) are analyzed
  • Methods must return subtypes of AnyRef
  • Method names are not used for lookup - only return types matter
  • Inherited methods are included in the analysis

Type Mapping

  • Each unique return type creates one factory mapping
  • If multiple methods return the same type, behavior is undefined (one will be chosen)
  • Generic types are supported but may have limitations with type erasure
  • Primitive types and standard library types may not be mappable

Runtime Behavior

  • Factory functions are created that call the original methods
  • Lazy values are evaluated when the factory is called
  • Methods are called each time lookup is performed (no caching)
  • Thread safety depends on the thread safety of the original methods

Error Conditions

  • Type Not Found: lookup returns empty list, lookupSingleOrThrow throws exception
  • Multiple Matches: Undefined behavior when multiple methods return same type
  • Instantiation Errors: Exceptions from the original methods are propagated

Dependencies

  • Requires the util module: "com.softwaremill.macwire" %% "util" % "2.6.6"
  • The Wired type is defined in the util module, not the macros module
  • Uses Scala reflection for runtime type information

Install with Tessl CLI

npx tessl i tessl/maven-com-softwaremill-macwire--macros

docs

context-dependent-wiring.md

context-free-wiring.md

dynamic-instance-access.md

factory-based-wiring.md

index.md

tile.json