Search code examples
javascalareflectionreflections

Using reflection to verify all instances of a trait have a unique field


Consider the following code. Animals should have a unique ID. I want to dynamically verify (in a test) that all concrete subtypes of Animal have unique IDs. I want my test to fail if, for example, Cat and Fish both try to pass in uniqueId = 2.

sealed trait Animal {
  val uniqueId: Int
}

abstract class Mammal(val uniqueId: Int) extends Animal

abstract class Fish(val uniqueId: Int) extends Animal


case class Dog(age: Int, name: String) extends Mammal(1)

case class Cat(favouriteFood: String) extends Mammal(2)

case class Salmon(isCute: Boolean) extends Fish(3)

I'm using reflections to get the classes.

    import org.reflections.Reflections
    val reflections                 = new Reflections("package.blah")
    val allSubtypes: Seq[Class[_ <: Animal]] = reflections.getSubTypesOf(classOf[Animal]).asScala.toList
    val concreteSubtypes: Seq[Class[_ <: Animal]] = allSubtypes.filter(c => !Modifier.isAbstract(c.getModifiers))

I'm starting to suspect it might not be possible but would love to be wrong! I have the classes but no way to instantiate instances of them because all the constructors differ and I'm not sure if I can access the uniqueId from just the class.


Solution

  • As Tomer Shetah's comment says, this doesn't seem to be possible as stated. But it may work if you can split Animal and AnimalCompanion, and make the id actually unique per AnimalCompanion:

    abstract class AnimalCompanion(val uniqueId: Int)
    
    sealed trait Animal {
      def companion: AnimalCompanion
      def uniqueId = companion.uniqueId
    }
    
    abstract class Mammal(val companion: AnimalCompanion) extends Animal
    
    abstract class Fish(val companion: AnimalCompanion) extends Animal
    
    case class Dog(age: Int, name: String) extends Mammal(Dog)
    
    object Dog extends AnimalCompanion(1)
    
    case class Cat(favouriteFood: String) extends Mammal(Cat)
    
    object Cat extends AnimalCompanion(2)
    
    case class Salmon(isCute: Boolean) extends Fish(Salmon)
    
    object Salmon extends AnimalCompanion(3)
    

    Then you find all subtypes of AnimalCompanion in the same way, and because they are all objects they'll have the MODULE$ field holding the instance and that instance will have the uniqueId field and getter method.

    Without testing, something like this should work:

    val allCompanionClasses = reflections.getSubTypesOf(classOf[AnimalCompanion]).asScala
    val allCompanionInstances = allCompanionClasses.map(_.getField("MODULE$").get(null))
    val allUniqueIds = allCompanionInstances.map(x => x.getClass().getMethod("uniqueId").invoke(x))
    

    and then it remains to check they are actually unique e.g. by

    allUniqueIds.toSet.size == allUniqueIds.size
    

    Without changing the structure, I'd consider a different approach: provide the instances yourself, just verify they include all classes.

    val instances = Set(Dog(0, ""), Cat(""), Salmon(true))
    
    // allSubtypes defined in the question
    assert(allSubtypes.toSet == instances.map(_.getClass))
    // same logic as above but we already have a Set
    assert(instances.map(_.uniqueId).size == instances.size)