Initial implementation
parent
92f6827f17
commit
9f04b9bdd2
|
@ -0,0 +1,48 @@
|
|||
version = 2.0.0
|
||||
maxColumn = 120
|
||||
style = defaultWithAlign
|
||||
docstrings = ScalaDoc
|
||||
assumeStandardLibraryStripMargin = true
|
||||
|
||||
continuationIndent {
|
||||
callSite = 2
|
||||
defnSite = 2
|
||||
}
|
||||
|
||||
align = more
|
||||
align.tokens.add = [{code = ":", owner = "Term.Param"}]
|
||||
|
||||
align {
|
||||
arrowEnumeratorGenerator = true
|
||||
openParenCallSite = true
|
||||
openParenDefnSite = false
|
||||
}
|
||||
|
||||
newlines {
|
||||
alwaysBeforeTopLevelStatements = true
|
||||
alwaysBeforeElseAfterCurlyIf = false
|
||||
}
|
||||
|
||||
danglingParentheses = true
|
||||
|
||||
rewrite {
|
||||
rules = [
|
||||
RedundantBraces
|
||||
RedundantParens
|
||||
SortModifiers
|
||||
PreferCurlyFors
|
||||
ExpandImportSelectors
|
||||
]
|
||||
sortModifiers.order = [
|
||||
"override", "private", "protected", "final", "implicit", "sealed", "abstract", "lazy"
|
||||
]
|
||||
redundantBraces.stringInterpolation = true
|
||||
}
|
||||
|
||||
includeCurlyBraceInSelectChains = true
|
||||
optIn.breakChainOnFirstMethodDot = true
|
||||
verticalMultilineAtDefinitionSiteArityThreshold = 5
|
||||
verticalMultiline.arityThreshold = 5
|
||||
verticalMultiline.atDefnSite = true
|
||||
verticalMultiline.newlineBeforeImplicitKW = false
|
||||
project.git = true
|
|
@ -0,0 +1,37 @@
|
|||
val Http4sVersion = "0.20.8"
|
||||
val CirceVersion = "0.11.1"
|
||||
val LogbackVersion = "1.2.3"
|
||||
val BetterFilesVersion = "3.8.0"
|
||||
val DirectoryWatcherVersion = "0.9.6"
|
||||
val ScalaLoggingVersion = "3.9.2"
|
||||
|
||||
val root = (project in file("."))
|
||||
.settings(
|
||||
organization := "name.orhideous",
|
||||
name := "twicher",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
scalaVersion := "2.12.8",
|
||||
libraryDependencies ++= Seq(
|
||||
"org.http4s" %% "http4s-blaze-server" % Http4sVersion,
|
||||
"org.http4s" %% "http4s-circe" % Http4sVersion,
|
||||
"org.http4s" %% "http4s-dsl" % Http4sVersion,
|
||||
"io.circe" %% "circe-generic" % CirceVersion,
|
||||
"com.github.pathikrit" %% "better-files" % BetterFilesVersion,
|
||||
"io.methvin" %% "directory-watcher-better-files" % DirectoryWatcherVersion,
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % ScalaLoggingVersion,
|
||||
"ch.qos.logback" % "logback-classic" % LogbackVersion
|
||||
),
|
||||
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3"),
|
||||
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.0")
|
||||
)
|
||||
|
||||
scalacOptions ++= Seq(
|
||||
"-deprecation",
|
||||
"-encoding",
|
||||
"UTF-8",
|
||||
"-language:higherKinds",
|
||||
"-language:postfixOps",
|
||||
"-feature",
|
||||
"-Ypartial-unification",
|
||||
"-Xfatal-warnings"
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# suppress inspection "UnusedProperty" for whole file
|
||||
sbt.version=1.3.5
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.3")
|
||||
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
|
|
@ -0,0 +1,10 @@
|
|||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
|
@ -0,0 +1,9 @@
|
|||
package name.orhideous.twicher
|
||||
|
||||
sealed trait Error extends Exception
|
||||
|
||||
object Error {
|
||||
|
||||
case object NoSuchQuote extends Error
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package name.orhideous.twicher
|
||||
|
||||
import cats.effect.ExitCode
|
||||
import cats.effect.IO
|
||||
import cats.effect.IOApp
|
||||
import cats.implicits._
|
||||
import name.orhideous.twicher.persistence.QuotesFileRepository
|
||||
import org.http4s.implicits._
|
||||
import org.http4s.server.Router
|
||||
import org.http4s.server.blaze._
|
||||
|
||||
object Main extends IOApp {
|
||||
private val host = sys.env.getOrElse("TWICHER_HOST", "0.0.0.0")
|
||||
private val port = sys.env.getOrElse("TWICHER_PORT", "9000").toInt
|
||||
private val dir = sys.env.getOrElse("TWICHER_DIR", "./data")
|
||||
|
||||
private val repository = QuotesFileRepository(dir)
|
||||
private val routes = Router[IO]("/" -> Routes(repository)).orNotFound
|
||||
|
||||
override def run(args: List[String]): IO[ExitCode] =
|
||||
BlazeServerBuilder[IO]
|
||||
.bindHttp(port, host)
|
||||
.withHttpApp(routes)
|
||||
.serve
|
||||
.compile
|
||||
.drain
|
||||
.as(ExitCode.Success)
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package name.orhideous.twicher
|
||||
|
||||
import cats.effect.IO
|
||||
import io.circe.generic.auto._
|
||||
import name.orhideous.twicher.persistence.QuotesRepository
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityCodec._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object Routes {
|
||||
|
||||
def apply(repository: QuotesRepository): HttpRoutes[IO] = {
|
||||
|
||||
val dsl = new Http4sDsl[IO] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of[IO] {
|
||||
case GET -> Root =>
|
||||
repository.list.flatMap(Ok(_))
|
||||
|
||||
case GET -> Root / "random" =>
|
||||
repository.random.flatMap(Ok(_))
|
||||
|
||||
case GET -> Root / IntVar(id) =>
|
||||
repository.read(id).flatMap(Ok(_))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package name.orhideous.twicher.persistence
|
||||
|
||||
case class Quote(id: Int, text: String)
|
|
@ -0,0 +1,78 @@
|
|||
package name.orhideous.twicher.persistence
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.WatchEvent
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import better.files.File
|
||||
import cats.effect.IO
|
||||
import com.typesafe.scalalogging.StrictLogging
|
||||
import io.methvin.better.files.RecursiveFileMonitor
|
||||
import name.orhideous.twicher.Error
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.Random
|
||||
|
||||
class QuotesFileRepository(private val quotesDir: File) extends QuotesRepository with StrictLogging {
|
||||
|
||||
private final val cache = TrieMap.empty[Int, Quote]
|
||||
private final val rnd = new Random
|
||||
|
||||
private implicit val watcherEC: ExecutionContext =
|
||||
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1))
|
||||
|
||||
private final val watcher = new RecursiveFileMonitor(quotesDir, logger = logger.underlying) {
|
||||
override def onEvent(eventType: WatchEvent.Kind[Path], file: File, count: Int): Unit = {
|
||||
logger.info(s"Detected change in $quotesDir, reloading")
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
watcher.start()
|
||||
reload()
|
||||
|
||||
override def list: IO[Vector[Quote]] = IO.pure(cache.values.toVector)
|
||||
|
||||
override def random: IO[Quote] =
|
||||
if (cache.isEmpty) {
|
||||
IO.raiseError(Error.NoSuchQuote)
|
||||
} else {
|
||||
read(cache.keySet.toVector(rnd.nextInt(cache.size)))
|
||||
}
|
||||
|
||||
override def read(id: Int): IO[Quote] = cache.get(id) match {
|
||||
case Some(quote) => IO.pure(quote)
|
||||
case None => IO.raiseError(Error.NoSuchQuote)
|
||||
}
|
||||
|
||||
private def reload(): Unit = {
|
||||
val quotes = QuotesFileRepository.loadQuotes(quotesDir).map(q => q.id -> q).toMap
|
||||
cache.clear()
|
||||
cache ++= quotes
|
||||
logger.info(s"Loaded ${quotes.size} quotes from $quotesDir")
|
||||
}
|
||||
}
|
||||
|
||||
object QuotesFileRepository {
|
||||
private final val pattern = ".*(\\d+)\\.txt$".r
|
||||
|
||||
def apply(quotesDir: String): QuotesFileRepository = new QuotesFileRepository(File(quotesDir))
|
||||
|
||||
private final implicit val charset: Charset = Charset.forName("UTF-8")
|
||||
|
||||
private def loadQuotes(quotesDir: File): Seq[Quote] =
|
||||
quotesDir
|
||||
.globRegex(pattern)
|
||||
.toSeq
|
||||
.par
|
||||
.map(parseFile)
|
||||
.seq
|
||||
|
||||
private def parseFile(file: File): Quote = {
|
||||
val pattern(id) = file.name
|
||||
val text = file.contentAsString.stripLineEnd.trim
|
||||
Quote(id.toInt, text)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package name.orhideous.twicher.persistence
|
||||
|
||||
import cats.effect.IO
|
||||
|
||||
trait QuotesRepository {
|
||||
|
||||
def read(id: Int): IO[Quote]
|
||||
|
||||
def list: IO[Vector[Quote]]
|
||||
|
||||
def random: IO[Quote]
|
||||
}
|
Loading…
Reference in New Issue