Add initial url parsing and pattern matching
Credit to http4s everywhere.
This commit is contained in:
parent
29a7c572ba
commit
8ddf2bcdc1
142
.gitignore
vendored
Normal file
142
.gitignore
vendored
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
|
||||||
|
# Created by https://www.gitignore.io/api/sbt,scala,bloop,metals,intellij,sublimetext
|
||||||
|
# Edit at https://www.gitignore.io/?templates=sbt,scala,bloop,metals,intellij,sublimetext
|
||||||
|
|
||||||
|
### Bloop ###
|
||||||
|
.bloop/
|
||||||
|
|
||||||
|
### Intellij ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Intellij Patch ###
|
||||||
|
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||||
|
|
||||||
|
# *.iml
|
||||||
|
# modules.xml
|
||||||
|
# .idea/misc.xml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# Sonarlint plugin
|
||||||
|
.idea/sonarlint
|
||||||
|
|
||||||
|
### Metals ###
|
||||||
|
.metals/
|
||||||
|
|
||||||
|
### SBT ###
|
||||||
|
# Simple Build Tool
|
||||||
|
# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
|
||||||
|
|
||||||
|
dist/*
|
||||||
|
target/
|
||||||
|
lib_managed/
|
||||||
|
src_managed/
|
||||||
|
project/boot/
|
||||||
|
project/plugins/project/
|
||||||
|
.history
|
||||||
|
.cache
|
||||||
|
.lib/
|
||||||
|
|
||||||
|
### Scala ###
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
|
||||||
|
### SublimeText ###
|
||||||
|
# Cache files for Sublime Text
|
||||||
|
*.tmlanguage.cache
|
||||||
|
*.tmPreferences.cache
|
||||||
|
*.stTheme.cache
|
||||||
|
|
||||||
|
# Workspace files are user-specific
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Project files should be checked into the repository, unless a significant
|
||||||
|
# proportion of contributors will probably not be using Sublime Text
|
||||||
|
# *.sublime-project
|
||||||
|
|
||||||
|
# SFTP configuration file
|
||||||
|
sftp-config.json
|
||||||
|
|
||||||
|
# Package control specific files
|
||||||
|
Package Control.last-run
|
||||||
|
Package Control.ca-list
|
||||||
|
Package Control.ca-bundle
|
||||||
|
Package Control.system-ca-bundle
|
||||||
|
Package Control.cache/
|
||||||
|
Package Control.ca-certs/
|
||||||
|
Package Control.merged-ca-bundle
|
||||||
|
Package Control.user-ca-bundle
|
||||||
|
oscrypto-ca-bundle.crt
|
||||||
|
bh_unicode_properties.cache
|
||||||
|
|
||||||
|
# Sublime-github package stores a github token in this file
|
||||||
|
# https://packagecontrol.io/packages/sublime-github
|
||||||
|
GitHub.sublime-settings
|
||||||
|
|
||||||
|
# End of https://www.gitignore.io/api/sbt,scala,bloop,metals,intellij,sublimetext
|
||||||
|
|
||||||
|
/.idea/
|
23
.scalafmt.conf
Normal file
23
.scalafmt.conf
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
style = default
|
||||||
|
|
||||||
|
maxColumn = 100
|
||||||
|
|
||||||
|
// Vertical alignment is pretty, but leads to bigger diffs
|
||||||
|
align = none
|
||||||
|
|
||||||
|
// Insist on trailing commas for better difs in element construction
|
||||||
|
trailingCommas = always
|
||||||
|
danglingParentheses = true
|
||||||
|
|
||||||
|
rewrite.rules = [
|
||||||
|
AvoidInfix
|
||||||
|
RedundantBraces
|
||||||
|
RedundantParens
|
||||||
|
AsciiSortImports
|
||||||
|
PreferCurlyFors
|
||||||
|
]
|
||||||
|
|
||||||
|
project.excludeFilters = [
|
||||||
|
"scalafix-inputs",
|
||||||
|
"scalafix-outputs"
|
||||||
|
]
|
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
language: scala
|
||||||
|
|
||||||
|
install:
|
||||||
|
- rvm use 2.3.0 --install --fuzzy
|
||||||
|
- gem update --system
|
||||||
|
- gem install sass
|
||||||
|
- gem install jekyll -v 3.2.1
|
||||||
|
|
||||||
|
scala:
|
||||||
|
- 2.12.8
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- stage: verify
|
||||||
|
|
||||||
|
script:
|
||||||
|
- sbt ++$TRAVIS_SCALA_VERSION todomvc/fullOptJS::webpack
|
||||||
|
- sbt docs/mdoc
|
||||||
|
- sbt docs/makeMicrosite
|
||||||
|
- mkdir -p router-docs/site/todomvc
|
||||||
|
- cp -R router-docs/target/site/* ./router-docs/site/
|
||||||
|
- cp ./router-docs/site/Readme.html ./router-docs/site/index.html
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: pages
|
||||||
|
skip-cleanup: true
|
||||||
|
github-token: $GITHUB_TOKEN # Set in the settings page of your repository, as a secure variable
|
||||||
|
keep-history: true
|
||||||
|
local-dir: router-docs/site
|
||||||
|
target-branch: gh-pages
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
|
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
outwatch-router
|
||||||
|
===
|
||||||
|
|
||||||
|
Easy routing for [outwatch](https://outwatch.github.io) on scala.js
|
||||||
|
|
||||||
|
Most of this code is adapted from [http4s](http4s.org)'s route parsing and path pattern matching.
|
||||||
|
|
||||||
|
See [documentation](https://clovellytech.github.io/outwatch-router/index.html)
|
77
build.sbt
Normal file
77
build.sbt
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import dependencies._
|
||||||
|
|
||||||
|
cancelable in Global := true
|
||||||
|
|
||||||
|
val commonSettings = Seq(
|
||||||
|
organization := "com.clovellytech",
|
||||||
|
version := Version.version,
|
||||||
|
scalaVersion := Version.scalaVersion,
|
||||||
|
resolvers ++= addResolvers,
|
||||||
|
scalacOptions ++= options.scalac,
|
||||||
|
scalacOptions in (Compile, console) := options.scalacConsole,
|
||||||
|
updateOptions := updateOptions.value.withLatestSnapshots(false)
|
||||||
|
) ++ compilerPlugins
|
||||||
|
|
||||||
|
val withTests : String = "compile->compile;test->test"
|
||||||
|
val testOnly : String = "test->test"
|
||||||
|
|
||||||
|
lazy val docs = (project in file("./router-docs"))
|
||||||
|
.settings(name := "outwatch-router-docs")
|
||||||
|
.enablePlugins(MdocPlugin)
|
||||||
|
.settings(commonSettings)
|
||||||
|
.dependsOn(router)
|
||||||
|
|
||||||
|
lazy val copyFastOptJS = TaskKey[Unit]("copyFastOptJS", "Copy javascript files to target directory")
|
||||||
|
|
||||||
|
lazy val router = (project in file("./outwatch-router"))
|
||||||
|
.settings(name := "outwatch-router")
|
||||||
|
.enablePlugins(ScalaJSPlugin)
|
||||||
|
.enablePlugins(ScalaJSBundlerPlugin)
|
||||||
|
.settings(commonSettings)
|
||||||
|
.settings(
|
||||||
|
scalaJSModuleKind := ModuleKind.CommonJSModule,
|
||||||
|
scalacOptions += "-P:scalajs:sjsDefinedByDefault",
|
||||||
|
useYarn := true, // makes scalajs-bundler use yarn instead of npm
|
||||||
|
jsEnv in Test := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv,
|
||||||
|
scalaJSUseMainModuleInitializer := true,
|
||||||
|
scalaJSModuleKind := ModuleKind.CommonJSModule, // configure Scala.js to emit a JavaScript module instead of a top-level script
|
||||||
|
version in webpack := "4.16.1",
|
||||||
|
version in startWebpackDevServer := "3.1.4",
|
||||||
|
webpackDevServerExtraArgs := Seq("--progress", "--color"),
|
||||||
|
webpackConfigFile in fastOptJS := Some(baseDirectory.value / "webpack.config.dev.js"),
|
||||||
|
// https://scalacenter.github.io/scalajs-bundler/cookbook.html#performance
|
||||||
|
webpackBundlingMode in fastOptJS := BundlingMode.LibraryOnly(),
|
||||||
|
resolvers += "jitpack" at "https://jitpack.io",
|
||||||
|
libraryDependencies ++= Seq(
|
||||||
|
"io.github.outwatch" % "outwatch" % "ea240c6d04",
|
||||||
|
"org.http4s" %% "parboiled" % "1.0.0",
|
||||||
|
"org.scalatest" %%% "scalatest" % "3.0.5" % Test
|
||||||
|
),
|
||||||
|
copyFastOptJS := {
|
||||||
|
val inDir = (crossTarget in (Compile, fastOptJS)).value
|
||||||
|
val outDir = (crossTarget in (Compile, fastOptJS)).value / "dev"
|
||||||
|
val files = Seq("outwatch-router-fastopt-loader.js", "outwatch-router-frontend-fastopt.js", "outwatch-router-frontend-fastopt.js.map") map { p => (inDir / p, outDir / p) }
|
||||||
|
IO.copy(files, overwrite = true, preserveLastModified = true, preserveExecutable = true)
|
||||||
|
},
|
||||||
|
// hot reloading configuration:
|
||||||
|
// https://github.com/scalacenter/scalajs-bundler/issues/180
|
||||||
|
addCommandAlias("dev", "; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer"),
|
||||||
|
addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS")
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val exampleApp = (project in file("router-example"))
|
||||||
|
.settings(name := "outwatch-example")
|
||||||
|
.settings(commonSettings)
|
||||||
|
.dependsOn(router)
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
.settings(name := "outwatch-router-root")
|
||||||
|
.settings(commonSettings)
|
||||||
|
.settings(
|
||||||
|
skip in publish := true,
|
||||||
|
aggregate in reStart := false,
|
||||||
|
)
|
||||||
|
.dependsOn(router)
|
||||||
|
.aggregate(router)
|
||||||
|
|
||||||
|
|
43
docs/Intro.md
Normal file
43
docs/Intro.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
Outwatch Router
|
||||||
|
===
|
||||||
|
|
||||||
|
```scala mdoc
|
||||||
|
import cats._
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.Applicative
|
||||||
|
import cats.data.Kleisli
|
||||||
|
import outwatch.router._, Router._
|
||||||
|
|
||||||
|
sealed abstract class Page
|
||||||
|
case class RootPage() extends Page
|
||||||
|
case class Login() extends Page
|
||||||
|
case class Register() extends Page
|
||||||
|
case class Profile(userId: String) extends Page
|
||||||
|
case class NotFound() extends Page
|
||||||
|
|
||||||
|
object Page{
|
||||||
|
def root: Page = RootPage()
|
||||||
|
def login: Page = Login()
|
||||||
|
def register: Page = Register()
|
||||||
|
def profile(userId: String): Page = Profile(userId)
|
||||||
|
def notFound: Page = NotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
def routes[F[_]: Applicative]: AppRouter[F, Page] = Kleisli[F, Path, Page] {
|
||||||
|
case Root => Page.root.pure[F]
|
||||||
|
case Root / "login" => Page.login.pure[F]
|
||||||
|
case Root / "register" => Page.register.pure[F]
|
||||||
|
case Root / "profile" / userId => Page.profile(userId).pure[F]
|
||||||
|
case _ => Page.notFound.pure[F]
|
||||||
|
}
|
||||||
|
|
||||||
|
val router = routes[Id]
|
||||||
|
|
||||||
|
router.run(Root)
|
||||||
|
router.run(Root / "login")
|
||||||
|
router.run(Root / "profile" / "saopa98f")
|
||||||
|
|
||||||
|
router.run(Path("/profile/asd"))
|
||||||
|
router.run(Path("/apsinoasn"))
|
||||||
|
```
|
||||||
|
|
220
outwatch-router/src/main/scala/outwatch/router/Path.scala
Normal file
220
outwatch-router/src/main/scala/outwatch/router/Path.scala
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// Completely copied from org.http4s.dsl.impl
|
||||||
|
|
||||||
|
package outwatch.router
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
|
import java.nio.{ByteBuffer, CharBuffer}
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
|
|
||||||
|
/** Base class for path extractors. */
|
||||||
|
trait Path {
|
||||||
|
def /(child: String) = new /(this, child)
|
||||||
|
def toList: List[String]
|
||||||
|
def parent: Path
|
||||||
|
def lastOption: Option[String]
|
||||||
|
def startsWith(other: Path): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
object Path {
|
||||||
|
|
||||||
|
/** Constructs a path from a single string by splitting on the `'/'`
|
||||||
|
* character.
|
||||||
|
*
|
||||||
|
* Leading slashes do not create an empty path segment. This is to
|
||||||
|
* reflect that there is no distinction between a request to
|
||||||
|
* `http://www.example.com` from `http://www.example.com/`.
|
||||||
|
*
|
||||||
|
* Trailing slashes result in a path with an empty final segment,
|
||||||
|
* unless the path is `"/"`, which is `Root`.
|
||||||
|
*
|
||||||
|
* Segments are URL decoded.
|
||||||
|
*
|
||||||
|
* {{{
|
||||||
|
* scala> Path("").toList
|
||||||
|
* res0: List[String] = List()
|
||||||
|
* scala> Path("/").toList
|
||||||
|
* res1: List[String] = List()
|
||||||
|
* scala> Path("a").toList
|
||||||
|
* res2: List[String] = List(a)
|
||||||
|
* scala> Path("/a").toList
|
||||||
|
* res3: List[String] = List(a)
|
||||||
|
* scala> Path("/a/").toList
|
||||||
|
* res4: List[String] = List(a, "")
|
||||||
|
* scala> Path("//a").toList
|
||||||
|
* res5: List[String] = List("", a)
|
||||||
|
* scala> Path("/%2F").toList
|
||||||
|
* res0: List[String] = List(/)
|
||||||
|
* }}}
|
||||||
|
*/
|
||||||
|
def apply(str: String): Path =
|
||||||
|
if (str == "" || str == "/")
|
||||||
|
Root
|
||||||
|
else {
|
||||||
|
val segments = str.split("/", -1)
|
||||||
|
// .head is safe because split always returns non-empty array
|
||||||
|
val segments0 = if (segments.head == "") segments.drop(1) else segments
|
||||||
|
segments0.foldLeft(Root: Path)((path, seg) => path / UrlCodingUtils.urlDecode(seg))
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply(first: String, rest: String*): Path =
|
||||||
|
rest.foldLeft(Root / first)(_ / _)
|
||||||
|
|
||||||
|
def apply(list: List[String]): Path =
|
||||||
|
list.foldLeft(Root: Path)(_ / _)
|
||||||
|
|
||||||
|
def unapplySeq(path: Path): Some[List[String]] =
|
||||||
|
Some(path.toList)
|
||||||
|
//
|
||||||
|
// def unapplySeq[F[_]](request: Request[F]): Some[List[String]] =
|
||||||
|
// Some(Path(request.pathInfo).toList)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final case class /(parent: Path, child: String) extends Path {
|
||||||
|
lazy val toList: List[String] = parent.toList ++ List(child)
|
||||||
|
|
||||||
|
def lastOption: Some[String] = Some(child)
|
||||||
|
|
||||||
|
lazy val asString: String = s"$parent/${UrlCodingUtils.pathEncode(child)}"
|
||||||
|
override def toString: String = asString
|
||||||
|
def startsWith(other: Path): Boolean = {
|
||||||
|
val components = other.toList
|
||||||
|
toList.take(components.length) === components
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root extractor:
|
||||||
|
* {{{
|
||||||
|
* Path("/") match {
|
||||||
|
* case Root => ...
|
||||||
|
* }
|
||||||
|
* }}}
|
||||||
|
*/
|
||||||
|
case object Root extends Path {
|
||||||
|
def toList: List[String] = Nil
|
||||||
|
def parent: Path = this
|
||||||
|
def lastOption: None.type = None
|
||||||
|
override def toString = ""
|
||||||
|
def startsWith(other: Path): Boolean = other == Root
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private[router] object UrlCodingUtils {
|
||||||
|
|
||||||
|
private val lower = ('a' to 'z').toSet
|
||||||
|
private val upper = ('A' to 'Z').toSet
|
||||||
|
private val num = ('0' to '9').toSet
|
||||||
|
val Unreserved: Set[Char] = lower ++ upper ++ num ++ "-_.~"
|
||||||
|
|
||||||
|
private val toSkip : Set[Char] = Unreserved ++ "!$&'()*+,;=:/?@"
|
||||||
|
|
||||||
|
private val HexUpperCaseChars: Array[Char] = ('A' to 'F').toArray
|
||||||
|
/**
|
||||||
|
* Percent-encodes a string. Depending on the parameters, this method is
|
||||||
|
* appropriate for URI or URL form encoding. Any resulting percent-encodings
|
||||||
|
* are normalized to uppercase.
|
||||||
|
*
|
||||||
|
* @param toEncode the string to encode
|
||||||
|
* @param charset the charset to use for characters that are percent encoded
|
||||||
|
* @param spaceIsPlus if space is not skipped, determines whether it will be
|
||||||
|
* rendreed as a `"+"` or a percent-encoding according to `charset`.
|
||||||
|
* @param toSkip a predicate of characters exempt from encoding. In typical
|
||||||
|
* use, this is composed of all Unreserved URI characters and sometimes a
|
||||||
|
* subset of Reserved URI characters.
|
||||||
|
*/
|
||||||
|
def urlEncode(
|
||||||
|
toEncode: String,
|
||||||
|
charset: Charset = UTF_8,
|
||||||
|
spaceIsPlus: Boolean = false,
|
||||||
|
toSkip: Char => Boolean = toSkip): String = {
|
||||||
|
val in = charset.encode(toEncode)
|
||||||
|
val out = CharBuffer.allocate((in.remaining() * 3).toInt)
|
||||||
|
while (in.hasRemaining) {
|
||||||
|
val c = in.get().toChar
|
||||||
|
if (toSkip(c)) {
|
||||||
|
out.put(c)
|
||||||
|
} else if (c == ' ' && spaceIsPlus) {
|
||||||
|
out.put('+')
|
||||||
|
} else {
|
||||||
|
out.put('%')
|
||||||
|
out.put(HexUpperCaseChars((c >> 4) & 0xF))
|
||||||
|
out.put(HexUpperCaseChars(c & 0xF))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.flip()
|
||||||
|
out.toString
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SkipEncodeInPath =
|
||||||
|
Unreserved ++ ":@!$&'()*+,;="
|
||||||
|
|
||||||
|
def pathEncode(s: String, charset: Charset = UTF_8): String =
|
||||||
|
UrlCodingUtils.urlEncode(s, charset, false, SkipEncodeInPath)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percent-decodes a string.
|
||||||
|
*
|
||||||
|
* @param toDecode the string to decode
|
||||||
|
* @param charset the charset of percent-encoded characters
|
||||||
|
* @param plusIsSpace true if `'+'` is to be interpreted as a `' '`
|
||||||
|
* @param toSkip a predicate of characters whose percent-encoded form
|
||||||
|
* is left percent-encoded. Almost certainly should be left empty.
|
||||||
|
*/
|
||||||
|
def urlDecode(
|
||||||
|
toDecode: String,
|
||||||
|
charset: Charset = UTF_8,
|
||||||
|
plusIsSpace: Boolean = false,
|
||||||
|
toSkip: Char => Boolean = Function.const(false)): String = {
|
||||||
|
val in = CharBuffer.wrap(toDecode)
|
||||||
|
// reserve enough space for 3-byte UTF-8 characters. 4-byte characters are represented
|
||||||
|
// as surrogate pairs of characters, and will get a luxurious 6 bytes of space.
|
||||||
|
val out = ByteBuffer.allocate(in.remaining() * 3)
|
||||||
|
while (in.hasRemaining) {
|
||||||
|
val mark = in.position()
|
||||||
|
val c = in.get()
|
||||||
|
if (c == '%') {
|
||||||
|
if (in.remaining() >= 2) {
|
||||||
|
val xc = in.get()
|
||||||
|
val yc = in.get()
|
||||||
|
// scalastyle:off magic.number
|
||||||
|
val x = Character.digit(xc, 0x10)
|
||||||
|
val y = Character.digit(yc, 0x10)
|
||||||
|
// scalastyle:on magic.number
|
||||||
|
if (x != -1 && y != -1) {
|
||||||
|
val oo = (x << 4) + y
|
||||||
|
if (!toSkip(oo.toChar)) {
|
||||||
|
out.put(oo.toByte)
|
||||||
|
} else {
|
||||||
|
out.put('%'.toByte)
|
||||||
|
out.put(xc.toByte)
|
||||||
|
out.put(yc.toByte)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.put('%'.toByte)
|
||||||
|
in.position(mark + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is an invalid encoding. Fail gracefully by treating the '%' as
|
||||||
|
// a literal.
|
||||||
|
out.put(c.toByte)
|
||||||
|
while (in.hasRemaining) out.put(in.get().toByte)
|
||||||
|
}
|
||||||
|
} else if (c == '+' && plusIsSpace) {
|
||||||
|
out.put(' '.toByte)
|
||||||
|
} else {
|
||||||
|
// normally `out.put(c.toByte)` would be enough since the url is %-encoded,
|
||||||
|
// however there are cases where a string can be partially decoded
|
||||||
|
// so we have to make sure the non us-ascii chars get preserved properly.
|
||||||
|
if (this.toSkip(c)) {
|
||||||
|
out.put(c.toByte)
|
||||||
|
} else {
|
||||||
|
out.put(charset.encode(String.valueOf(c)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.flip()
|
||||||
|
charset.decode(out).toString
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package outwatch.router
|
||||||
|
|
||||||
|
import cats.data.Kleisli
|
||||||
|
|
||||||
|
object Router {
|
||||||
|
type AppRouter[F[_], A] = Kleisli[F, Path, A]
|
||||||
|
val AppRouter = Kleisli
|
||||||
|
|
||||||
|
}
|
84
project/Dependencies.scala
Normal file
84
project/Dependencies.scala
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import sbt._
|
||||||
|
import sbt.librarymanagement.DependencyBuilders
|
||||||
|
import org.portablescala.sbtplatformdeps._
|
||||||
|
|
||||||
|
object dependencies {
|
||||||
|
val addResolvers = Seq(
|
||||||
|
"52north for postgis" at "http://52north.org/maven/repo/releases/",
|
||||||
|
Resolver.sonatypeRepo("releases"),
|
||||||
|
Resolver.sonatypeRepo("snapshots")
|
||||||
|
)
|
||||||
|
|
||||||
|
val compilerPlugins = Seq(
|
||||||
|
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.9"),
|
||||||
|
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
|
||||||
|
)
|
||||||
|
|
||||||
|
val bcrypt = "3.1"
|
||||||
|
val cats = "1.4.0"
|
||||||
|
val catsMtl = "0.4.0"
|
||||||
|
val catsEffect = "1.2.0"
|
||||||
|
val circe = "0.11.0"
|
||||||
|
val circeConfig = "0.6.1"
|
||||||
|
val doobie = "0.6.0"
|
||||||
|
val flyway = "5.2.4"
|
||||||
|
val fs2 = "1.0.2"
|
||||||
|
val h4sm = "0.0.17"
|
||||||
|
val http4s = "0.20.0-M5"
|
||||||
|
val logback = "1.2.3"
|
||||||
|
val monocle = "1.5.0"
|
||||||
|
val postgis = "1.3.3"
|
||||||
|
val postgres = "42.2.5"
|
||||||
|
val scalaCheck = "1.14.0"
|
||||||
|
val scalaTest = "3.0.5"
|
||||||
|
val simulacrum = "0.14.0"
|
||||||
|
|
||||||
|
val httpDeps = Seq(
|
||||||
|
"http4s-blaze-server",
|
||||||
|
"http4s-blaze-client",
|
||||||
|
"http4s-circe",
|
||||||
|
"http4s-dsl"
|
||||||
|
).map("org.http4s" %% _ % http4s) ++ Seq(
|
||||||
|
"circe-core",
|
||||||
|
"circe-generic",
|
||||||
|
"circe-parser",
|
||||||
|
"circe-java8"
|
||||||
|
).map("io.circe" %% _ % circe)
|
||||||
|
|
||||||
|
val testDeps = Seq(
|
||||||
|
"org.scalatest" %% "scalatest" % scalaTest,
|
||||||
|
"org.tpolecat" %% "doobie-scalatest" % doobie,
|
||||||
|
"org.scalacheck" %% "scalacheck" % scalaCheck,
|
||||||
|
"com.clovellytech" %% "h4sm-dbtesting" % h4sm
|
||||||
|
)
|
||||||
|
|
||||||
|
val testDepsInTestOnly = testDeps.map(_ % "test")
|
||||||
|
|
||||||
|
val dbDeps = Seq(
|
||||||
|
"org.flywaydb" % "flyway-core" % flyway,
|
||||||
|
"org.postgresql" % "postgresql" % postgres,
|
||||||
|
"org.postgis" % "postgis-jdbc" % postgis
|
||||||
|
) ++ Seq(
|
||||||
|
"doobie-core",
|
||||||
|
"doobie-postgres",
|
||||||
|
"doobie-hikari"
|
||||||
|
).map("org.tpolecat" %% _ % doobie)
|
||||||
|
|
||||||
|
val commonDeps = Seq(
|
||||||
|
"io.circe" %% "circe-config" % circeConfig,
|
||||||
|
"ch.qos.logback" % "logback-classic" % logback,
|
||||||
|
"com.github.mpilquist" %% "simulacrum" % simulacrum
|
||||||
|
) ++ Seq(
|
||||||
|
"h4sm-auth",
|
||||||
|
"h4sm-files",
|
||||||
|
"h4sm-permissions"
|
||||||
|
).map("com.clovellytech" %% _ % h4sm) ++ Seq(
|
||||||
|
"monocle-core",
|
||||||
|
"monocle-generic",
|
||||||
|
"monocle-macro",
|
||||||
|
"monocle-state",
|
||||||
|
"monocle-refined"
|
||||||
|
).map("com.github.julien-truffaut" %% _ % monocle)
|
||||||
|
|
||||||
|
val allDeps = httpDeps ++ dbDeps ++ commonDeps ++ testDepsInTestOnly
|
||||||
|
}
|
58
project/Options.scala
Normal file
58
project/Options.scala
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
object options {
|
||||||
|
|
||||||
|
val scalac = Seq(
|
||||||
|
// format: off
|
||||||
|
"-deprecation", // Emit warning and location for usages of deprecated APIs.
|
||||||
|
"-encoding", "utf-8", // Specify character encoding used by source files.
|
||||||
|
"-explaintypes", // Explain type errors in more detail.
|
||||||
|
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
|
||||||
|
"-language:existentials", // Existential types (besides wildcard types) can be written and inferred
|
||||||
|
"-language:experimental.macros", // Allow macro definition (besides implementation and application)
|
||||||
|
"-language:higherKinds", // Allow higher-kinded types
|
||||||
|
"-language:implicitConversions", // Allow definition of implicit functions called views
|
||||||
|
"-unchecked", // Enable additional warnings where generated code depends on assumptions.
|
||||||
|
"-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access.
|
||||||
|
"-Xfatal-warnings", // Fail the compilation if there are any warnings.
|
||||||
|
"-Xfuture", // Turn on future language features.
|
||||||
|
"-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver.
|
||||||
|
"-Xlint:by-name-right-associative", // By-name parameter of right associative operator.
|
||||||
|
"-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error.
|
||||||
|
"-Xlint:delayedinit-select", // Selecting member of DelayedInit.
|
||||||
|
"-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element.
|
||||||
|
"-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
|
||||||
|
"-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`.
|
||||||
|
"-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id.
|
||||||
|
"-Xlint:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'.
|
||||||
|
"-Xlint:nullary-unit", // Warn when nullary methods return Unit.
|
||||||
|
"-Xlint:option-implicit", // Option.apply used implicit view.
|
||||||
|
"-Xlint:package-object-classes", // Class or object defined in package object.
|
||||||
|
"-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds.
|
||||||
|
"-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field.
|
||||||
|
"-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component.
|
||||||
|
"-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope.
|
||||||
|
"-Xlint:unsound-match", // Pattern match may not be typesafe.
|
||||||
|
"-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver.
|
||||||
|
"-Ypartial-unification", // Enable partial unification in type constructor inference
|
||||||
|
"-Ywarn-dead-code", // Warn when dead code is identified.
|
||||||
|
"-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined.
|
||||||
|
"-Ywarn-inaccessible", // Warn about inaccessible types in method signatures.
|
||||||
|
"-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`.
|
||||||
|
"-Ywarn-nullary-unit", // Warn when nullary methods return Unit.
|
||||||
|
"-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'.
|
||||||
|
"-Ywarn-numeric-widen", // Warn when numerics are widened.
|
||||||
|
"-Ywarn-unused:implicits", // Warn if an implicit parameter is unused.
|
||||||
|
"-Ywarn-unused:imports", // Warn if an import selector is not referenced.
|
||||||
|
"-Ywarn-unused:locals", // Warn if a local definition is unused.
|
||||||
|
"-Ywarn-unused:params", // Warn if a value parameter is unused.
|
||||||
|
"-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused.
|
||||||
|
"-Ywarn-unused:privates", // Warn if a private member is unused.
|
||||||
|
"-Ywarn-value-discard", // Warn when non-Unit expression results are unused.
|
||||||
|
"-Yrangepos"
|
||||||
|
// format: on
|
||||||
|
)
|
||||||
|
|
||||||
|
val badScalacConsoleFlags = Seq("-Xfatal-warnings", "-Ywarn-unused:imports")
|
||||||
|
|
||||||
|
val scalacConsole = scalac.filterNot(badScalacConsoleFlags.contains(_))
|
||||||
|
}
|
4
project/Version.scala
Normal file
4
project/Version.scala
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
object Version{
|
||||||
|
val version = "0.0.1"
|
||||||
|
val scalaVersion = "2.12.8"
|
||||||
|
}
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
sbt.version=1.2.3
|
4
project/plugins.sbt
Normal file
4
project/plugins.sbt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.13.1")
|
||||||
|
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.26")
|
||||||
|
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8" )
|
||||||
|
addSbtPlugin("com.47deg" % "sbt-microsites" % "0.8.0")
|
Loading…
Reference in New Issue
Block a user