فهرست منبع

Created a strictly styled AST for route parsing.

Some of the functions for parsing these can and should be unit tested in the core
plugin.

This will hopefully make the process less closely tied to the Play <-> Scala.js
environment.
Thomas Flucke 5 سال پیش
والد
کامیت
140e5f56f3
20فایلهای تغییر یافته به همراه370 افزوده شده و 154 حذف شده
  1. 4 2
      play/src/main/scala/com/tflucke/webroutes/formatter/PlayJsonFormatter.scala
  2. 20 11
      play/src/main/scala/com/tflucke/webroutes/parsers/PlayParser.scala
  3. 0 0
      play/src/sbt-test/webroutes/dummy/server/logs/application.log
  4. 4 1
      play/src/sbt-test/webroutes/user/server/conf/testing.conf
  5. 0 0
      play/src/sbt-test/webroutes/user/server/logs/application.log
  6. 72 36
      play/src/test/scala/com/tflucke/webroutes/unit/parsers/PlayParserTest.scala
  7. 15 16
      plugin/src/main/scala/com/tflucke/webroutes/RPCGenerator.scala
  8. 0 60
      plugin/src/main/scala/com/tflucke/webroutes/RouteDef.scala
  9. 7 7
      plugin/src/main/scala/com/tflucke/webroutes/endpoints/EndpointFile.scala
  10. 4 2
      plugin/src/main/scala/com/tflucke/webroutes/formatter/Formatter.scala
  11. 0 8
      plugin/src/main/scala/com/tflucke/webroutes/formatter/FormatterTypes.scala
  12. 10 8
      plugin/src/main/scala/com/tflucke/webroutes/formatter/NoFormatter.scala
  13. 4 2
      plugin/src/main/scala/com/tflucke/webroutes/formatter/PlainTextFormatter.scala
  14. 16 0
      plugin/src/main/scala/com/tflucke/webroutes/models/Format.scala
  15. 37 0
      plugin/src/main/scala/com/tflucke/webroutes/models/FullName.scala
  16. 12 0
      plugin/src/main/scala/com/tflucke/webroutes/models/Method.scala
  17. 42 0
      plugin/src/main/scala/com/tflucke/webroutes/models/MimeType.scala
  18. 63 0
      plugin/src/main/scala/com/tflucke/webroutes/models/RouteDef.scala
  19. 59 0
      plugin/src/main/scala/com/tflucke/webroutes/models/URL.scala
  20. 1 1
      plugin/src/main/scala/com/tflucke/webroutes/parsers/Parser.scala

+ 4 - 2
play/src/main/scala/com/tflucke/webroutes/formatter/PlayJsonFormatter.scala

@@ -1,8 +1,10 @@
 package com.tflucke.webroutes.formatter
 package com.tflucke.webroutes.formatter
 
 
+import com.tflucke.webroutes.models.FullName
+
 object PlayJsonFormatter extends JsonFormatter {
 object PlayJsonFormatter extends JsonFormatter {
-  def genDeserializer(body: String, typ: String): String =
+  def genDeserializer(body: String, typ: FullName): String =
     s"Json.parse($body).as[$typ]"
     s"Json.parse($body).as[$typ]"
-  def genSerializer(body: String, typ: String): String =
+  def genSerializer(body: String, typ: FullName): String =
     s"Json.stringify(Json.toJson($body))"
     s"Json.stringify(Json.toJson($body))"
 }
 }

+ 20 - 11
play/src/main/scala/com/tflucke/webroutes/parsers/PlayParser.scala

@@ -2,7 +2,7 @@ package com.tflucke.webroutes.parsers
 
 
 import sbt.File
 import sbt.File
 import scala.io.Source
 import scala.io.Source
-import com.tflucke.webroutes.RouteDef
+import com.tflucke.webroutes.models.{Format,FullName,RouteDef,URL}
 
 
 /** A parse which will extract information from a Play! Framework routes file.
 /** A parse which will extract information from a Play! Framework routes file.
   * 
   * 
@@ -52,28 +52,37 @@ object PlayParser extends Parser {
       val props = propPattern.findAllMatchIn(route.group(1)).map(m => (m.group(1), m.group(2))).toList
       val props = propPattern.findAllMatchIn(route.group(1)).map(m => (m.group(1), m.group(2))).toList
       def getProp(name: String): Option[String] = props.find(_._1.equalsIgnoreCase(name)).map(_._2)
       def getProp(name: String): Option[String] = props.find(_._1.equalsIgnoreCase(name)).map(_._2)
       val method = route.group(3)
       val method = route.group(3)
-      val path = route.group(4)
-      val pack = route.group(5)
-      val obj = route.group(6)
-      val function = route.group(7)
-      val args = (Option(route.group(9)).getOrElse("") + Option(route.group(10)).getOrElse("")).split(",").filter(!_.isEmpty)
+      val symbolReg = raw":(\w+)".r
+      val path = URL.fromString(route.group(4)) {
+        case symbolReg(sym) => Some(Symbol(sym))
+        case _ => None
+      }
+      val pack = FullName(route.group(5))
+      val obj = Symbol(route.group(6))
+      val function = Symbol(route.group(7))
+      val args = Option(route.group(9)).map[Seq[String]]({ first =>
+        Option(route.group(10)).map[Seq[String]](first +: _.split(",")).getOrElse(Seq(first))
+      }).getOrElse(Nil).map({ arg =>
+        val arr = arg.split(":")
+        (Symbol(arr(0).trim), FullName(arr(1).trim))
+      })
       val (retMime, retType) = getReturnType(getProp("mime"), getProp("type"))
       val (retMime, retType) = getReturnType(getProp("mime"), getProp("type"))
-      RouteDef(method, path, pack, obj, function, args, getProp("body"),
-        getProp("content") getOrElse "json",
-        retType,
+      RouteDef(method, path, pack, obj, function, args, getProp("body").map(FullName(_)),
+        getProp("content").map(Format.fromString _) getOrElse Format.JSON,
+        FullName(retType),
         retMime)
         retMime)
     }).toList
     }).toList
 
 
   def getReturnType(mime: Option[String], typ: Option[String]) =
   def getReturnType(mime: Option[String], typ: Option[String]) =
     (mime, typ) match {
     (mime, typ) match {
-      case (Some(retMime), Some(retType)) => (retMime, retType)
+      case (Some(retMime), Some(retType)) => (retMime.trim, retType.trim)
       case (Some("text/json"), None) => ("application/json", "Any")
       case (Some("text/json"), None) => ("application/json", "Any")
       case (Some("application/json"), None) => ("application/json", "Any")
       case (Some("application/json"), None) => ("application/json", "Any")
       case (Some("text/plain"), None) => ("text/plain", "String")
       case (Some("text/plain"), None) => ("text/plain", "String")
       case (Some(_), None) => ???
       case (Some(_), None) => ???
       case (None, Some("String")) => ("text/plain", "String")
       case (None, Some("String")) => ("text/plain", "String")
       case (None, Some("java.lang.String")) => ("text/plain", "String")
       case (None, Some("java.lang.String")) => ("text/plain", "String")
-      case (None, Some(retType)) => ("application/json", retType)
+      case (None, Some(retType)) => ("application/json", retType.trim)
       case (None, None) => ("text/plain", "String")
       case (None, None) => ("text/plain", "String")
     }
     }
 }
 }

+ 0 - 0
play/src/sbt-test/webroutes/dummy/server/logs/application.log


+ 4 - 1
play/src/sbt-test/webroutes/user/server/conf/testing.conf

@@ -4,4 +4,7 @@ include "application.conf"
 #play.filters.disabled += "play.filters.csrf.CSRFFilter"
 #play.filters.disabled += "play.filters.csrf.CSRFFilter"
 play.filters.csrf.header.bypassHeaders {
 play.filters.csrf.header.bypassHeaders {
   Csrf-Token = "nocheck"
   Csrf-Token = "nocheck"
-}
+}
+
+# TODO: See if test file can run evolutions instead
+play.evolutions.autoApply=true

+ 0 - 0
play/src/sbt-test/webroutes/user/server/logs/application.log


+ 72 - 36
play/src/test/scala/com/tflucke/webroutes/unit/parsers/PlayParserTest.scala

@@ -2,7 +2,7 @@ package com.tflucke.webroutes.unit.parsers
 
 
 import sbt.File
 import sbt.File
 import scala.io.Source
 import scala.io.Source
-import com.tflucke.webroutes.RouteDef
+import com.tflucke.webroutes.models.{FullName,Method,RouteDef,URL}
 import com.tflucke.webroutes.parsers.PlayParser
 import com.tflucke.webroutes.parsers.PlayParser
 import org.scalatest._
 import org.scalatest._
 import java.io.FileNotFoundException
 import java.io.FileNotFoundException
@@ -25,52 +25,66 @@ class PlayParserTest extends PropSpec with Matchers {
   }
   }
 
 
   val apis = PlayParser.parseFile(userFile)
   val apis = PlayParser.parseFile(userFile)
-  val queryApi = apis.find(api => api.fn == "query")
-  val addApi = apis.find(api => api.fn == "addUser")
-  val getApi = apis.find(api => api.fn == "get")
-  val updateApi = apis.find(api => api.fn == "updateUser")
-  val deleteApi = apis.find(api => api.fn == "delete")
+  val queryApi = apis.find(api => api.fn == 'query)
+  val addApi = apis.find(api => api.fn == 'addUser)
+  val getApi = apis.find(api => api.fn == 'get)
+  val updateApi = apis.find(api => api.fn == 'updateUser)
+  val deleteApi = apis.find(api => api.fn == 'delete)
 
 
   property("the users route file should produce a get api") {
   property("the users route file should produce a get api") {
     getApi should not be None
     getApi should not be None
   }
   }
   property("the get api should use the GET method") {
   property("the get api should use the GET method") {
-    getApi.get.method should be ("GET")
+    getApi.get.method should be (Method.GET)
   }
   }
   property("the get api should have the correct URL") {
   property("the get api should have the correct URL") {
-    getApi.get.url should be ("/user/:id")
+    getApi.get.url should matchPattern {
+      case URL(_, _, _, Seq(Left(""), Left("user"), Right('id)), _) =>
+    }
   }
   }
   property("the get api should have the correct package") {
   property("the get api should have the correct package") {
-    getApi.get.pack should be ("org.sample.users")
+    getApi.get.pack should matchPattern {
+      case FullName(Seq("org", "sample", "users"), Nil) =>
+    }
   }
   }
   property("the get api should have the UserController") {
   property("the get api should have the UserController") {
-    getApi.get.controller should be ("UserController")
+    getApi.get.controller.name should be ("UserController")
   }
   }
   property("the get api should have return type User") {
   property("the get api should have return type User") {
-    getApi.get.retType should be ("org.sample.users.shared.User")
+    getApi.get.retType should matchPattern {
+      case FullName(Seq("org", "sample", "users", "shared", "User"), Nil) =>
+    }
   }
   }
   property("the get api should have a single Long argument") {
   property("the get api should have a single Long argument") {
     getApi.get.args.size should be (1)
     getApi.get.args.size should be (1)
-    getApi.get.args(0).replace(" ", "") should be ("id:Long")
+    getApi.get.args(0) should be (('id, FullName("Long")))
   }
   }
 
 
   property("the users route file should produce a query api") {
   property("the users route file should produce a query api") {
     queryApi should not be None
     queryApi should not be None
   }
   }
   property("the query api should use the GET method") {
   property("the query api should use the GET method") {
-    queryApi.get.method should be ("GET")
+    queryApi.get.method should be (Method.GET)
   }
   }
   property("the query api should have the correct URL") {
   property("the query api should have the correct URL") {
-    queryApi.get.url should be ("/user")
+    queryApi.get.url should matchPattern {
+      case URL(_, _, _, Seq(Left(""), Left("user")), _) =>
+    }
   }
   }
   property("the query api should have the correct package") {
   property("the query api should have the correct package") {
-    queryApi.get.pack should be ("org.sample.users")
+    queryApi.get.pack should matchPattern {
+      case FullName(Seq("org", "sample", "users"), Nil) =>
+    }
   }
   }
   property("the query api should have the UserController") {
   property("the query api should have the UserController") {
-    queryApi.get.controller should be ("UserController")
+    queryApi.get.controller.name should be ("UserController")
   }
   }
   property("the query api should have return type Seq[User]") {
   property("the query api should have return type Seq[User]") {
-    queryApi.get.retType should be ("Seq[org.sample.users.shared.User]")
+    queryApi.get.retType should matchPattern {
+      case FullName(Seq("Seq"), Seq(
+        FullName(Seq("org", "sample", "users", "shared", "User"), Nil)
+      )) =>
+    }
   }
   }
   property("the query api should have no arguments") {
   property("the query api should have no arguments") {
     queryApi.get.args.size should be (0)
     queryApi.get.args.size should be (0)
@@ -80,71 +94,93 @@ class PlayParserTest extends PropSpec with Matchers {
     addApi should not be None
     addApi should not be None
   }
   }
   property("the add api should use the PUT method") {
   property("the add api should use the PUT method") {
-    addApi.get.method should be ("PUT")
+    addApi.get.method should be (Method.PUT)
   }
   }
   property("the add api should have the correct URL") {
   property("the add api should have the correct URL") {
-    addApi.get.url should be ("/user")
+    addApi.get.url should matchPattern {
+      case URL(_, _, _, Seq(Left(""), Left("user")), _) =>
+    }
   }
   }
   property("the add api should have the correct package") {
   property("the add api should have the correct package") {
-    addApi.get.pack should be ("org.sample.users")
+    addApi.get.pack should matchPattern {
+      case FullName(Seq("org", "sample", "users"), Nil) =>
+    }
   }
   }
   property("the add api should have the UserController") {
   property("the add api should have the UserController") {
-    addApi.get.controller should be ("UserController")
+    addApi.get.controller.name should be ("UserController")
   }
   }
   property("the add api should have return type User") {
   property("the add api should have return type User") {
-    addApi.get.retType should be ("org.sample.users.shared.User")
+    addApi.get.retType should matchPattern {
+      case FullName(Seq("org", "sample", "users", "shared", "User"), Nil) =>
+    }
   }
   }
   property("the add api should have only a User body argument") {
   property("the add api should have only a User body argument") {
-    addApi.get.bodyType should be (Some("org.sample.users.shared.User"))
+    addApi.get.bodyType should matchPattern {
+      case Some(FullName(Seq("org", "sample", "users", "shared", "User"), Nil)) =>
+    }
   }
   }
 
 
   property("the users route file should produce a update api") {
   property("the users route file should produce a update api") {
     updateApi should not be None
     updateApi should not be None
   }
   }
   property("the update api should use the POST method") {
   property("the update api should use the POST method") {
-    updateApi.get.method should be ("POST")
+    updateApi.get.method should be (Method.POST)
   }
   }
   property("the update api should have the correct URL") {
   property("the update api should have the correct URL") {
-    updateApi.get.url should be ("/user/:id")
+    updateApi.get.url should matchPattern {
+      case URL(_, _, _, Seq(Left(""), Left("user"), Right('id)), _) =>
+    }
   }
   }
   property("the update api should have the correct package") {
   property("the update api should have the correct package") {
-    updateApi.get.pack should be ("org.sample.users")
+    updateApi.get.pack should matchPattern {
+      case FullName(Seq("org", "sample", "users"), Nil) =>
+    }
   }
   }
   property("the update api should have the UserController") {
   property("the update api should have the UserController") {
-    updateApi.get.controller should be ("UserController")
+    updateApi.get.controller.name should be ("UserController")
   }
   }
   property("the update api should have return type User") {
   property("the update api should have return type User") {
-    updateApi.get.retType should be ("org.sample.users.shared.User")
+    updateApi.get.retType should matchPattern {
+      case FullName(Seq("org", "sample", "users", "shared", "User"), Nil) =>
+    }
   }
   }
   property("the update api should have a Long and User arguments") {
   property("the update api should have a Long and User arguments") {
     updateApi.get.args.size should be (1)
     updateApi.get.args.size should be (1)
-    updateApi.get.args(0).replace(" ", "") should be ("id:Long")
+    updateApi.get.args(0) should be (('id, FullName("Long")))
   }
   }
   property("the update api should have only a User body argument") {
   property("the update api should have only a User body argument") {
-    addApi.get.bodyType should be (Some("org.sample.users.shared.User"))
+    addApi.get.bodyType should matchPattern {
+      case Some(FullName(Seq("org", "sample", "users", "shared", "User"), Nil)) =>
+    }
   }
   }
 
 
   property("the users route file should produce a delete api") {
   property("the users route file should produce a delete api") {
     deleteApi should not be None
     deleteApi should not be None
   }
   }
   property("the delete api should use the DELETE method") {
   property("the delete api should use the DELETE method") {
-    deleteApi.get.method should be ("DELETE")
+    deleteApi.get.method should be (Method.DELETE)
   }
   }
   property("the delete api should have the correct URL") {
   property("the delete api should have the correct URL") {
-    deleteApi.get.url should be ("/user/:id")
+    deleteApi.get.url should matchPattern {
+      case URL(_, _, _, Seq(Left(""), Left("user"), Right('id)), _) =>
+    }
   }
   }
   property("the delete api should have the correct package") {
   property("the delete api should have the correct package") {
-    deleteApi.get.pack should be ("org.sample.users")
+    deleteApi.get.pack should matchPattern {
+      case FullName(Seq("org", "sample", "users"), Nil) =>
+    }
   }
   }
   property("the delete api should have the UserController") {
   property("the delete api should have the UserController") {
-    deleteApi.get.controller should be ("UserController")
+    deleteApi.get.controller.name should be ("UserController")
   }
   }
   property("the delete api should have return type User") {
   property("the delete api should have return type User") {
-    deleteApi.get.retType should be ("org.sample.users.shared.User")
+    deleteApi.get.retType should matchPattern {
+      case FullName(Seq("org", "sample", "users", "shared", "User"), Nil) =>
+    }
   }
   }
   property("the delete api should have a Long and User arguments") {
   property("the delete api should have a Long and User arguments") {
     deleteApi.get.args.size should be (1)
     deleteApi.get.args.size should be (1)
-    getApi.get.args(0).replace(" ", "") should be ("id:Long")
+    getApi.get.args(0) should be (('id, FullName("Long")))
   }
   }
 
 
   // "PlayParser" when {
   // "PlayParser" when {

+ 15 - 16
plugin/src/main/scala/com/tflucke/webroutes/RPCGenerator.scala

@@ -1,6 +1,7 @@
 package com.tflucke.webroutes
 package com.tflucke.webroutes
 
 
 import sbt.{File,IO}
 import sbt.{File,IO}
+import com.tflucke.webroutes.models.FullName
 import com.tflucke.webroutes.parsers.Parser
 import com.tflucke.webroutes.parsers.Parser
 import com.tflucke.webroutes.endpoints.EndpointFile
 import com.tflucke.webroutes.endpoints.EndpointFile
 
 
@@ -17,7 +18,7 @@ object RPCGenerator {
     outpath: File
     outpath: File
   ): Seq[File] = {
   ): Seq[File] = {
     definitionFiles.flatMap({ file =>
     definitionFiles.flatMap({ file =>
-      val outFiles = file.definitions.groupBy[(String, String)]({
+      val outFiles = file.definitions.groupBy[(FullName, Symbol)]({
         case (route, _) => (route.pack, route.controller)
         case (route, _) => (route.pack, route.controller)
       }).map({ case ((pack, controller), routes) => {
       }).map({ case ((pack, controller), routes) => {
         val content = s"""package ${pack}
         val content = s"""package ${pack}
@@ -25,15 +26,14 @@ object RPCGenerator {
 import com.tflucke.webroutes.{APIRoute,APIRouteBody}
 import com.tflucke.webroutes.{APIRoute,APIRouteBody}
 import play.api.libs.json.{JsValue,Reads,Writes,Json}
 import play.api.libs.json.{JsValue,Reads,Writes,Json}
 import org.scalajs.dom.XMLHttpRequest
 import org.scalajs.dom.XMLHttpRequest
-import org.scalajs.dom.ext.Ajax.InputData
 
 
-object ${controller} {
+object ${controller.name} {
 ${routes.map({
 ${routes.map({
   case (route, formatters) => route.toDefinition(formatters)
   case (route, formatters) => route.toDefinition(formatters)
 }).mkString("\n")}
 }).mkString("\n")}
 }
 }
 """
 """
-        val outFile = makeControllerFile(pack, controller, outpath)
+        val outFile = makeControllerFile(pack.syms, controller, outpath)
         IO.write(outFile, content)
         IO.write(outFile, content)
         outFile
         outFile
       } }).toSeq
       } }).toSeq
@@ -41,19 +41,18 @@ ${routes.map({
     })
     })
   }
   }
 
 
-  def makeControllerFile(path: String, controller: String, baseDir: File): File = {
-    val pattern = "\\.?([^.]+)(.*)".r
-    path match {
-      case pattern(head, rest) => {
-        val nextDir = new File(baseDir, head)
-        if (!nextDir.exists) {
-          nextDir.mkdir()
-        }
-        makeControllerFile(rest, controller, nextDir)
-      }
-      case _ => {
-        new File(baseDir, s"${controller}.scala")
+  def makeControllerFile(
+    path: Seq[String],
+    controller: Symbol,
+    baseDir: File
+  ): File = path match {
+    case head::rest => {
+      val nextDir = new File(baseDir, head)
+      if (!nextDir.exists) {
+        nextDir.mkdir()
       }
       }
+      makeControllerFile(rest, controller, nextDir)
     }
     }
+    case _ => new File(baseDir, s"${controller}.scala")
   }
   }
 }
 }

+ 0 - 60
plugin/src/main/scala/com/tflucke/webroutes/RouteDef.scala

@@ -1,60 +0,0 @@
-package com.tflucke.webroutes
-
-import com.tflucke.webroutes.formatter._
-
-/* TODO: Right now a lot of this is hard coded strings which may become fragile.
- */
-/** The metainformation for an endpoint nessicary to create an RPC API.
-  * 
-  * @author Thomas Flucke
-  */
-case class RouteDef(
-  method: String,
-  url: String,
-  pack: String,
-  controller: String,
-  fn: String,
-  args: Array[String],
-  bodyType: Option[String] = None,
-  bodyFormat: String = "json",
-  retType: String = "Any",
-  mime: String = "application/json"
-) extends FormatterTypes {
-  def interpolatedUrl: String = {
-    ":(?<symbol>[^/]+)".r.replaceAllIn(url, (matc) => "\\${${symbol}}")
-  }
-
-  def formatToMime(format: String) = format match {
-    case JSON => "application/json"
-    case TEXT => "text/plain"
-    case BINARY => ???
-    case FORM => ???
-  }
-
-  def mimeToFormat(mime: String) = mime match {
-    case "application/json" => JSON
-    case "text/json" => JSON
-    case "text/plain" => TEXT
-    case _ => ???
-  }
-
-  def toDefinition(fmters: Map[String, Formatter]): String = bodyType match {
-    case Some(bType) => s"""
-def ${fn}(${args.mkString(", ")}) : APIRouteBody[${bType}, ${retType}] = new APIRouteBody[${bType}, ${retType}](\"${method}\", s\"${interpolatedUrl}\", \"${mime}\") {
-    protected def convertBody(body: ${bType}): InputData =
-        ${fmters(bodyFormat).genSerializer("body", "InputData")}
-    protected def convertResult(xhr: XMLHttpRequest): ${retType} =
-        ${fmters(mimeToFormat(mime)).genDeserializer("xhr.responseText", retType)}
-    protected def acceptHeader = \"$mime\"
-    protected def contentTypeHeader = \"${formatToMime(bodyFormat)}\"
-}
-"""
-    case None => s"""
-def ${fn}(${args.mkString(", ")}) : APIRoute[${retType}] = new APIRoute[${retType}](\"${method}\", s\"${interpolatedUrl}\", \"${mime}\") {
-    protected def convert(xhr: XMLHttpRequest): ${retType} =
-        ${fmters(mimeToFormat(mime)).genDeserializer("xhr.responseText", retType)}
-    protected def acceptHeader = \"$mime\"
-}
-"""
-  }
-}

+ 7 - 7
plugin/src/main/scala/com/tflucke/webroutes/endpoints/EndpointFile.scala

@@ -1,7 +1,7 @@
 package com.tflucke.webroutes.endpoints
 package com.tflucke.webroutes.endpoints
 
 
 import sbt.File
 import sbt.File
-import com.tflucke.webroutes.RouteDef
+import com.tflucke.webroutes.models.{Format,RouteDef}
 import com.tflucke.webroutes.parsers.Parser
 import com.tflucke.webroutes.parsers.Parser
 import com.tflucke.webroutes.formatter._
 import com.tflucke.webroutes.formatter._
 
 
@@ -12,13 +12,13 @@ case class EndpointFile(
   val jsonFmt: JsonFormatter = NoJsonFormatter,
   val jsonFmt: JsonFormatter = NoJsonFormatter,
   val binFmt:  BinaryFormatter = NoBinaryFormatter,
   val binFmt:  BinaryFormatter = NoBinaryFormatter,
   val formFmt: FormFormatter = NoFormFormatter
   val formFmt: FormFormatter = NoFormFormatter
-) extends FormatterTypes {
+) {
 
 
-  def definitions: Seq[(RouteDef, Map[String, Formatter])] = 
+  def definitions: Seq[(RouteDef, Map[Format.Format, Formatter])] = 
     parser.parseFile(file).map((_, Map(
     parser.parseFile(file).map((_, Map(
-      TEXT -> textFmt,
-      JSON -> jsonFmt,
-      BINARY -> binFmt,
-      FORM -> formFmt
+      Format.TEXT -> textFmt,
+      Format.JSON -> jsonFmt,
+      Format.BINARY -> binFmt,
+      Format.FORM -> formFmt
     )))
     )))
 }
 }

+ 4 - 2
plugin/src/main/scala/com/tflucke/webroutes/formatter/Formatter.scala

@@ -1,8 +1,10 @@
 package com.tflucke.webroutes.formatter
 package com.tflucke.webroutes.formatter
 
 
+import com.tflucke.webroutes.models.FullName
+
 sealed trait Formatter {
 sealed trait Formatter {
-  def genDeserializer(body: String, typ: String): String
-  def genSerializer(body: String, typ: String): String
+  def genDeserializer(body: String, typ: FullName): String
+  def genSerializer(body: String, typ: FullName): String
 }
 }
 
 
 trait JsonFormatter extends Formatter
 trait JsonFormatter extends Formatter

+ 0 - 8
plugin/src/main/scala/com/tflucke/webroutes/formatter/FormatterTypes.scala

@@ -1,8 +0,0 @@
-package com.tflucke.webroutes.formatter
-
-trait FormatterTypes {
-  val TEXT = "text"
-  val JSON = "json"
-  val BINARY = "blob"
-  val FORM = "formdata"
-}

+ 10 - 8
plugin/src/main/scala/com/tflucke/webroutes/formatter/NoFormatter.scala

@@ -1,21 +1,23 @@
 package com.tflucke.webroutes.formatter
 package com.tflucke.webroutes.formatter
 
 
+import com.tflucke.webroutes.models.FullName
+
 object NoTextFormatter extends TextFormatter {
 object NoTextFormatter extends TextFormatter {
-  def genDeserializer(body: String, typ: String): String = ???
-  def genSerializer(body: String, typ: String): String = ???
+  def genDeserializer(body: String, typ: FullName): String = ???
+  def genSerializer(body: String, typ: FullName): String = ???
 }
 }
 
 
 object NoJsonFormatter extends JsonFormatter {
 object NoJsonFormatter extends JsonFormatter {
-  def genDeserializer(body: String, typ: String): String = ???
-  def genSerializer(body: String, typ: String): String = ???
+  def genDeserializer(body: String, typ: FullName): String = ???
+  def genSerializer(body: String, typ: FullName): String = ???
 }
 }
 
 
 object NoBinaryFormatter extends BinaryFormatter {
 object NoBinaryFormatter extends BinaryFormatter {
-  def genDeserializer(body: String, typ: String): String = ???
-  def genSerializer(body: String, typ: String): String = ???
+  def genDeserializer(body: String, typ: FullName): String = ???
+  def genSerializer(body: String, typ: FullName): String = ???
 }
 }
 
 
 object NoFormFormatter extends FormFormatter {
 object NoFormFormatter extends FormFormatter {
-  def genDeserializer(body: String, typ: String): String = ???
-  def genSerializer(body: String, typ: String): String = ???
+  def genDeserializer(body: String, typ: FullName): String = ???
+  def genSerializer(body: String, typ: FullName): String = ???
 }
 }

+ 4 - 2
plugin/src/main/scala/com/tflucke/webroutes/formatter/PlainTextFormatter.scala

@@ -1,6 +1,8 @@
 package com.tflucke.webroutes.formatter
 package com.tflucke.webroutes.formatter
 
 
+import com.tflucke.webroutes.models.FullName
+
 object PlainTextFormatter extends TextFormatter {
 object PlainTextFormatter extends TextFormatter {
-  def genDeserializer(body: String, typ: String): String = s"$body"
-  def genSerializer(body: String, typ: String): String = s"$body.toString"
+  def genDeserializer(body: String, typ: FullName): String = s"$body"
+  def genSerializer(body: String, typ: FullName): String = s"$body.toString"
 }
 }

+ 16 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/Format.scala

@@ -0,0 +1,16 @@
+package com.tflucke.webroutes.models
+
+/** Enum representing each of the possible formats to transmit data.
+  * 
+  * @author Thomas Flucke
+  */
+/* These were based on the formats used in javascript AJAX calls, but that may be
+ * too inflexible for future uses.  An alternative could be to replace this with
+ * MimeTypes and restrict the mimetypes to a subset defined here where necessary. 
+ */
+object Format extends Enumeration {
+  type Format = Value
+  val FORM, JSON, TEXT, BINARY = Value
+
+  implicit def fromString(str: String) = withName(str.toUpperCase)
+}

+ 37 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/FullName.scala

@@ -0,0 +1,37 @@
+package com.tflucke.webroutes.models
+
+/** Represents a full name of a package or type which can be used to reference it
+  * from any context.
+  * 
+  * @author Thomas Flucke
+  */
+/* This right now seems pretty strictly tied into scala.  May want to see if this
+ * can be broken up so that scala specific logic is in a single unit.
+ */
+case class FullName(
+  syms: Seq[String],
+  params: Seq[FullName] = Nil
+) {
+  override def toString = s"""${syms.mkString(".")}""" + (
+    if (params.nonEmpty) s"""[${params.mkString(",")}]""" else ""
+  )
+
+  def :+(sym: String) = FullName(syms.toSeq :+ sym)
+  def ++(newSyms: Seq[String]) = FullName(syms.toSeq ++ newSyms.toSeq)
+  def +:(sym: String) = FullName(sym +: syms.toSeq)
+  def ++:(newSyms: Seq[String]) = FullName(newSyms.toSeq ++: syms.toSeq)
+}
+
+object FullName {
+  def apply(strs: Seq[String]): FullName = new FullName(strs)
+  // TODO: Unit test this
+  def apply(str: String): FullName = {
+    val regex = raw"^([^\[]+)(\[(.+)\])?$$".r
+    str match {
+      case regex(base, _, null) => new FullName(base.split("\\s*\\.\\s*"))
+      case regex(base, _, generic) => new FullName(
+        base.split("\\s*\\.\\s*"),
+        generic.split("\\s*,\\s*").map(FullName(_)).toSeq
+      )}
+  }
+}

+ 12 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/Method.scala

@@ -0,0 +1,12 @@
+package com.tflucke.webroutes.models
+
+/** Enumeration of all possible HTTP methods.
+  * 
+  * @author Thomas Flucke
+  */
+object Method extends Enumeration {
+  type Method = Value
+  val GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS, TRACE, CONNECT = Value
+
+  implicit def fromString(str: String) = withName(str.toUpperCase)
+}

+ 42 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/MimeType.scala

@@ -0,0 +1,42 @@
+package com.tflucke.webroutes.models
+
+/** Represents an IANA mime type.
+  * 
+  * @author Thomas Flucke
+  */
+case class MimeType(
+  typ: String,
+  subtype: String,
+  params: Map[String, String] = Map.empty
+) {
+  def addParameter(key: String, value: String) =
+    MimeType(typ, subtype, params + (key -> value))
+
+  def addParameters(pairs: (String, String)*) =
+    MimeType(typ, subtype, params ++ pairs)
+
+  private def paramStr = params.map({case (k, v) => s";$k=$v"}).mkString("")
+
+  override def toString = s"$typ/${subtype}$paramStr"
+}
+
+object MimeType {
+  val text = MimeType("text", "plain")
+  val binary = MimeType("application", "octet-stream")
+  val form = MimeType("application", "x-www-form-urlencoded")
+  val json = MimeType("application", "json")
+
+  val regex = """^([-\w.+]+)/([-\w+.]+)(;([-\w+.]+=[-\w+.]+)?)*$""".r
+
+  // TODO: Unit test this
+  implicit def fromString(str: String) = str match {
+    case regex(typ, subtype, _, params) =>
+      MimeType(typ, subtype, Option(params).map(_.split(";").toSeq).getOrElse(Nil)
+        .map(str =>
+          str.split("=") match {
+            case Array(k, v) => (k, v)
+            case _ => throw new Exception("Unreachable code!")
+          }
+        ).toMap)
+  }
+}

+ 63 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/RouteDef.scala

@@ -0,0 +1,63 @@
+package com.tflucke.webroutes.models
+
+import com.tflucke.webroutes.formatter._
+
+/** The metainformation for an endpoint nessicary to create an RPC API.
+  * 
+  * @author Thomas Flucke
+  */
+case class RouteDef(
+  method: Method.Method,
+  url: URL,
+  pack: FullName,
+  controller: Symbol,
+  fn: Symbol,
+  args: Seq[(Symbol, FullName)],
+  bodyType: Option[FullName] = None,
+  bodyFormat: Format.Format = Format.JSON,
+  retType: FullName = FullName(Seq("Any")),
+  mime: MimeType = MimeType.json
+) {
+
+  def formatToMime(format: Format.Format) = format match {
+    case Format.JSON => MimeType.json
+    case Format.TEXT => MimeType.text
+    case Format.BINARY => ???
+    case Format.FORM => ???
+  }
+
+  def mimeToFormat(mime: MimeType) = mime match {
+    case MimeType("application", "json", _) => Format.JSON
+    case MimeType("text", "json", _) => Format.JSON
+    case MimeType("text", "plain", _) => Format.TEXT
+    case _ => ???
+  }
+
+  // TODO: Externalize below into a language backend
+  def scalaInterpolator(sym: Symbol) = s"$${${sym.name}}"
+
+  private def argStr =
+    args.map({case (sym, typ) => s"${sym.name}: $typ"}).mkString(", ")
+
+  def toDefinition(fmters: Map[Format.Format, Formatter]): String = bodyType match {
+    case Some(bType) => s"""
+def ${fn.name}($argStr) = new APIRouteBody[$bType, $retType](\"$method\", s\"${url.interpolatable(scalaInterpolator)}\", \"$mime\") {
+    protected def convertBody(body: $bType): org.scalajs.dom.ext.Ajax.InputData =
+        ${fmters(bodyFormat).genSerializer("body", FullName(Seq(
+          "org", "scalajs", "dom", "ext", "Ajax", "InputData"
+        )))}
+    protected def convertResult(xhr: XMLHttpRequest): $retType =
+        ${fmters(mimeToFormat(mime)).genDeserializer("xhr.responseText", retType)}
+    protected def acceptHeader = \"$mime\"
+    protected def contentTypeHeader = \"${formatToMime(bodyFormat)}\"
+}
+"""
+    case None => s"""
+def ${fn.name}($argStr) = new APIRoute[$retType](\"$method\", s\"${url.interpolatable(scalaInterpolator)}\", \"$mime\") {
+    protected def convert(xhr: XMLHttpRequest): $retType =
+        ${fmters(mimeToFormat(mime)).genDeserializer("xhr.responseText", retType)}
+    protected def acceptHeader = \"$mime\"
+}
+"""
+  }
+}

+ 59 - 0
plugin/src/main/scala/com/tflucke/webroutes/models/URL.scala

@@ -0,0 +1,59 @@
+package com.tflucke.webroutes.models
+
+/** Represents URL which will respond to HTTP requests.
+  * 
+  * The path of the URL is composed of both literals and variables which may be
+  * filled by a language specific interpolater.
+  * 
+  * The relative paths are represented with an empty string hostname.
+  * 
+  * @author Thomas Flucke
+  */
+case class URL(
+  protocol: Option[String] = None,
+  host: String = "",
+  port: Option[Short] = None,
+  path: Seq[URL.PathStep] = Seq.empty,
+  query: Map[String, URL.PathStep] = Map.empty[String, URL.PathStep]
+) {
+  def interpolatable(fn: Symbol => String) = protocol.map(_+"://").getOrElse("") +
+    host +
+    port.map(p => f":$p%hu").getOrElse("") +
+    path.map(_.fold(identity, fn)).mkString("/")
+}
+
+object URL {
+  type PathStep = Either[String, Symbol]
+
+  // TODO: Unit test this
+  def fromString(str: String)(symParser: String => Option[Symbol]) = {
+    val protoReg = raw"""(\w+://)?"""
+    val hostReg = raw"([-a-zA-Z0-9\.]*)"
+    val portReg = raw"(:\d+)?"
+    val pathReg = raw"(/[^\\]*)*"
+    val queryReg = raw"\??(([-a-zA-Z0-9\.]*=[-a-zA-Z0-9\.]*)(&[-a-zA-Z0-9\.]*=[-a-zA-Z0-9\.]*)*)?"
+    val regex = s"^${protoReg}${hostReg}${portReg}${pathReg}${queryReg}$$".r
+
+    def toSteps(strs: Seq[String]): Seq[PathStep] = strs match {
+      case Nil => Nil
+      case head::rest => symParser(head).toRight(head) +: toSteps(rest)
+    }
+
+    def parsePort(str: String) =
+      Some(Integer.parseInt(str.substring(1), 10).toShort)
+
+    def parseProto(str: String) = Some(str.substring(0, -3))
+
+    str match {
+      case regex(null, host, null, path, query, _, _) =>
+        URL(None, host, None, toSteps(Seq(path.split("/", -1):_*)))
+      case regex(proto, host, null, path, query, _, _) => 
+        URL(parseProto(proto), host, None, toSteps(Seq(path.split("/", -1):_*)))
+      case regex(null, host, port, path, query, _, _) =>
+        URL(None, host, parsePort(port), toSteps(Seq(path.split("/", -1):_*)))
+      case regex(proto, host, port, path, query, _, _) =>
+        URL(parseProto(proto), host, parsePort(port),
+          toSteps(Seq(path.split("/", -1):_*)))
+    }
+  }
+}

+ 1 - 1
plugin/src/main/scala/com/tflucke/webroutes/parsers/Parser.scala

@@ -1,7 +1,7 @@
 package com.tflucke.webroutes.parsers
 package com.tflucke.webroutes.parsers
 
 
 import sbt.File
 import sbt.File
-import com.tflucke.webroutes.RouteDef
+import com.tflucke.webroutes.models.RouteDef
 
 
 /** Interface which takes a file and parses the API definitions.
 /** Interface which takes a file and parses the API definitions.
   * 
   *