Search code examples
scalatype-safety

Typesafe Hierarchichal Structure


I want to write an API that allows me to easily create hierarchical keys like

/ foo / bar / baz

For this I've created the following classes:

sealed trait Key {
  def path: String

  def /(key: String): Node = Node(this, key)
}

case object Root extends Key {
  override def path: String = ""
}

final case class Node(parent: Key, key: String) extends Key {
  override def path: String = s"${parent.path}/$key"
}

And it works perfectly fine:

Root / "foo" / "bar"

Now, I also want to be able to include 'placeholders' like so:

Root / "foo" / %

And this shall then return something, that when a string gets applied returns a Key object with the given value replaced, i.e.

(Root / "foo" / %)("bar") == Root / "foo" / "bar"

Keep in mind that this should work on n-levels, like so:

Root / % / "foo" / % / "bar"

Solution

  • This is probably not very efficient, but it does what you asked for.

    Note that, answering my own comment, I decided that

    (Root / % / "foo" / % / "bar")("baz") == (Root / % / "foo" / "baz" / "bar")
    

    but there's not much to change to have a different outcome.

    object % // only for nicer API
    
    trait Slashable[T <: Slashable[T]] {
      def /(key: String): T
      def /(p: %.type): PlaceHeld[T] = PlaceHeld[T](str => /(str))
    }
    
    case object Root extends Key {
      override def path: String = ""
    }
    
    sealed trait Key extends Slashable[Key] {
      def path: String
    
      def /(key: String): Node = Node(this, key)
    }
    
    
    final case class Node(parent: Key, key: String) extends Key {
      override def path: String = s"${parent.path}/$key"
    }
    
    case class PlaceHeld[T <: Slashable[T]](replace: String => T) extends Slashable[PlaceHeld[T]]{
      def /(key: String) = PlaceHeld(str => replace(str)./(key))
      def apply(key: String) = replace(key)
    }
    

    EDIT

    You might want to replace your base type (without placeholders) by a simple (wrapped) Seq:

    case class Key(pathSeq: Seq[String]) extends Slashable[Key] {
      def path = pathSeq.mkString("/")
      def /(key: String) = Key(pathSeq :+ key)
    }
    
    object Root extends Slashable[Root] { 
      def path = ""                       //this is because we cannot do 
      def /(key: String) = Key(Seq(key))  //Root extends Key(Seq())
    }