summaryrefslogtreecommitdiffstats
path: root/src/main/scala/router.scala
blob: f1fbfa7ac9379206924eeb54928d3e341d426827 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package router

import scala.collection.mutable.ArrayBuffer
import scala.util.matching.Regex

class Router[T] {
  val routes = new ArrayBuffer[Route[T]]()

  def addRoute (
    path:        String,
    target:      T,
    defaults:    Map[String, String] = Map(),
    validations: Map[String, Regex]  = Map()
  ) {
    routes += new Route(path, defaults, validations, target)
  }

  def insertRoute (
    path:        String,
    target:      T,
    defaults:    Map[String, String] = Map(),
    validations: Map[String, Regex]  = Map(),
    at:          Int                 = 0
  ) {
    routes insert (
      at min routes.length,
      new Route(path, defaults, validations, target)
    )
  }

  def route (path: String): Option[Match[T]] = {
    def _route (
      components: Seq[String],
      routes:     List[Route[T]]
    ): Option[Match[T]] = routes match {
      case r :: rs => r.route(components) match {
        case Some(found) => Some(found)
        case None        => _route(components, rs)
      }
      case _ => None
    }
    _route(path.split("/"), routes.toList)
  }

  def uriFor (mapping: Map[String, String]): String = {
    throw new Error("unimplemented")
  }
}

class Route[T] (
  val path:        String,
  val defaults:    Map[String, String],
  val validations: Map[String, Regex],
  val target:      T
) {
  def route(
    parts:      Seq[String],
    components: Seq[String]         = components,
    mapping:    Map[String, String] = defaults
  ): Option[Match[T]] = {
    if (components.length == 0 && parts.length == 0) {
      Some(new Match[T](path, mapping, target))
    }
    else if (components.length == 0 || parts.length == 0) {
      None
    }
    else {
      components.head match {
        case Optional(name) => {
          throw new Error("unsupported")
        }
        case Variable(name) => {
          if (validate(name, parts.head)) {
            route(parts.tail, components.tail, mapping + (name -> parts.head))
          }
          else {
            None
          }
        }
        case literal => parts.head match {
          case `literal` => route(parts.tail, components.tail, mapping)
          case _         => None
        }
      }
    }
  }

  override def toString = path

  private lazy val components =
    path.split("/").filter(_.length > 0)

  private lazy val length =
    components.length

  private lazy val lengthWithoutOptionals =
    components.filter(!isOptional(_)).length

  private lazy val requiredVariableComponentNames =
    for (c <- components if isVariable(c) && !isOptional(c))
      yield getComponentName(c)

  private lazy val optionalVariableComponentNames =
    for (c <- components if isVariable(c) && isOptional(c))
      yield getComponentName(c)

  private val Optional = """^\?:(.*)$""".r
  private val Variable = """^\??:(.*)$""".r

  private def isOptional (component: String): Boolean = component match {
    case Optional(_) => true
    case _           => false
  }

  private def isVariable (component: String): Boolean = component match {
    case Variable(_) => true
    case _           => false
  }

  private def getComponentName (component: String) = component match {
    case Variable(name) => Some(name)
    case _              => None
  }

  private def validate (name: String, component: String): Boolean =
    validations get name match {
      case Some(rx) => rx.findFirstIn(component).nonEmpty
      case None     => true
    }
}

class Match[T] (
  val path:    String,
  val mapping: Map[String, String],
  val target:  T
)