Skip to content

Commit

Permalink
Add Loop (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
alirezameskin committed Jul 28, 2020
1 parent c85ba4b commit 853bb25
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 49 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ phony -l en -t "{{ contact.firstName }} {{ contact.lastName }}" -c 10
```

```bash
echo 'INSERT INTO users (first_name, last_name, age) VALUES("{{ contact.firstName }}", "{{ contact.lastName }}", {{alphanumeric.number(18, 68)}});' > /tmp/tpl.txt

echo "" > /tmp/tpl.txt
echo 'INSERT INTO users (first_name, last_name, age) VALUES("{{ contact.firstName }}", "{{ contact.lastName }}", {{alphanumeric.number(18, 68)}});' >> /tmp/tpl.txt
echo "SET @USER_ID = @@IDENTITY;" >> /tmp/tpl.txt
echo '{{#loop 10}}' >> /tmp/tpl.txt
echo 'INSERT INTO login_history(user_id,ip, timestamp) VALUES(@USER_ID, "{{internet.ip}}", "{{calendar.unixTime}}");' >> /tmp/tpl.txt
echo '{{#endloop}}' >> /tmp/tpl.txt
echo '{{#loop 20}}' >> /tmp/tpl.txt
echo 'INSERT INTO user_locations(user_id, latitude, longitude, timestamp) VALUES(@USER_ID, "{{location.latitude}}", "{{location.longitude}}");' >> /tmp/tpl.txt
echo '{{#endloop}}' >> /tmp/tpl.txt

phony -f /tmp/tpl.txt -c 10
```
Expand Down
15 changes: 11 additions & 4 deletions cli/src/main/scala/phony/cli/GenerateCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.effect.{ExitCode, IO}
import cats.implicits._
import fs2.Stream
import phony.cats.effect.{SyncLocale, SyncRandomUtility}
import phony.cli.template.parser.{FunctionCall, TemplateAST, TemplateExpressionParser, Text}
import phony.cli.template.parser._
import phony.cli.template.tokenizer.TemplateTokenizer
import phony.cli.template.{DefaultFunctionResolver, FunctionResolver}
import phony.{Phony, RandomUtility}
Expand All @@ -15,12 +15,17 @@ object GenerateCommand {

type Program = List[TemplateAST]

def eval(token: TemplateAST, resolver: FunctionResolver[IO]) =
def eval(token: TemplateAST, resolver: FunctionResolver[IO]): IO[String] =
token match {
case Text(t) => IO(t)
case op: FunctionCall =>
if (resolver.resolve.isDefinedAt(op)) resolver.resolve.apply(op)
else IO.raiseError(new Exception(s"Invalid Function ${op.func}(" + op.args.mkString(", ") + ")"))
case Loop(count, asts) =>
(0 until count).foldRight(IO("")) { (_, acc) =>
val res = asts.traverse(a => eval(a, resolver)).map(_.mkString)
acc.map2(res)((s1, s2) => s1 + s2)
}
}

def execute(program: Program, resolver: FunctionResolver[IO]): IO[String] =
Expand All @@ -33,9 +38,11 @@ object GenerateCommand {
} yield ast)

def functionResolver(language: String): IO[FunctionResolver[IO]] = {
implicit val locale = SyncLocale[IO](language)
implicit val locale = SyncLocale[IO](language)

implicit def utility: RandomUtility[IO] = new SyncRandomUtility[IO]()
val P = new Phony[IO]

val P = new Phony[IO]

IO(new DefaultFunctionResolver(P))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class DefaultFunctionResolver(val P: Phony[IO]) extends FunctionResolver[IO] {
case FunctionCall("calendar.date", format :: Nil) => P.calendar.date(format)
case FunctionCall("calendar.iso8601", Nil) => P.calendar.iso8601
case FunctionCall("calendar.timezone", Nil) => P.calendar.timezone
case FunctionCall("calendar.unixTime", Nil) => P.calendar.unixTime.map(_.toString)

case FunctionCall("contact.firstName", Nil) => P.contact.firstName
case FunctionCall("contact.lastName", Nil) => P.contact.lastName
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package phony.cli.template.parser

sealed trait TemplateAST
case class Text(value: String) extends TemplateAST
case class Text(value: String) extends TemplateAST
case class FunctionCall(func: String, args: Seq[String] = Nil) extends TemplateAST
case class Loop(count: Int, templates: List[TemplateAST]) extends TemplateAST
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
package phony.cli.template.parser

import phony.cli
import phony.cli.template.tokenizer.{Expression, PlainText, TemplateToken}
import phony.cli.util.BaseRegexParser
import phony.cli.template.tokenizer._

object TemplateExpressionParser extends BaseRegexParser {
import scala.util.parsing.combinator.Parsers
import scala.util.parsing.input.{NoPosition, Position, Reader}

def functionName: Parser[String] =
ident ~ rep(ident | ".") ^^ (n => n._1 + n._2.mkString(""))
object TemplateExpressionParser extends Parsers {
override type Elem = TemplateToken

def paramString: Parser[String] = """\"[0-9a-zA-Z\-"#\?]+\"""".r
class TemplateTokenReader(tokens: Seq[TemplateToken]) extends Reader[TemplateToken] {
override def first: TemplateToken = tokens.head
override def atEnd: Boolean = tokens.isEmpty
override def pos: Position = NoPosition
override def rest: Reader[TemplateToken] = new TemplateTokenReader(tokens.tail)
}

def functionWithoutArgs: Parser[FunctionCall] =
functionName ^^ (n => FunctionCall(n))
def functionCall: Parser[FunctionCall] = accept(
"functionCall", {
case f: FunctionCallToken => FunctionCall(f.func, f.args)
}
)

def functionWithArgs: Parser[FunctionCall] =
functionName ~ "(" ~ repsep(paramString | decimalNumber, ",") ~ ")" ^^ {
case func ~ _ ~ args ~ _ => FunctionCall(func, args)
def plainText: Parser[Text] = accept(
"text", {
case t: PlainTextToken => Text(t.value)
}
)

def program: Parser[TemplateAST] =
phrase(functionWithArgs | functionWithoutArgs)
def loopStart: Parser[Int] = accept(
"loopStart", {
case t: LoopStartToken => t.count
}
)

def apply(tokens: Seq[TemplateToken]): Either[Throwable, List[TemplateAST]] = {
val parts = tokens.map {
case Expression(expr) =>
parse(program, expr) match {
case Success(result, _) => Right(result)
case NoSuccess(msg, _) => Left(new Exception(s"Error: $msg"))
}
case PlainText(v) => Right(Text(v))
}.toList
def loop: Parser[Loop] =
loopStart ~ rep(plainText | functionCall) <~ LoopEndToken ^^ {
case c ~ tokens => Loop(c, tokens)
}

cli.util.sequence(parts)
def program: Parser[List[TemplateAST]] =
rep(loop | functionCall | plainText)

def apply(tokens: List[TemplateToken]): Either[Throwable, List[TemplateAST]] = {
val reader = new TemplateTokenReader(tokens)

program(reader) match {
case Success(result, _) => Right(result)
case NoSuccess(msg, _) => Left(new RuntimeException(msg))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package phony.cli.template.tokenizer

sealed trait TemplateToken
case class Expression(expr: String) extends TemplateToken
case class PlainText(value: String) extends TemplateToken
case class FunctionCallToken(func: String, args: Seq[String] = Nil) extends TemplateToken
case class PlainTextToken(value: String) extends TemplateToken
case class LoopStartToken(count: Int) extends TemplateToken
case object LoopEndToken extends TemplateToken
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,36 @@ package phony.cli.template.tokenizer
import phony.cli.util.BaseRegexParser

object TemplateTokenizer extends BaseRegexParser {
override def skipWhitespace: Boolean = false
val expressionRegex = """\{\{[a-z_A-Z0-9\|\s\.\(\)\"\,#]+\}\}""".r

val expressionRegex = """\{\{[a-z_A-Z0-9\|\s\.\(\)\"\,]+\}\}""".r
def plainText: Parser[PlainTextToken] =
notMatch(expressionRegex) ^^ PlainTextToken

def plainText: Parser[PlainText] =
notMatch(expressionRegex) ^^ PlainText
def expression: Parser[FunctionCallToken] = functionWithArgs | functionWithoutArgs

def expression: Parser[Expression] =
expressionRegex ^^ { s =>
Expression(
s.substring(2).dropRight(2).trim
)
def functionName: Parser[String] =
ident ~ rep(ident | ".") ^^ (n => n._1 + n._2.mkString(""))

def functionWithoutArgs: Parser[FunctionCallToken] =
"""{{""" ~> functionName <~ """}}""" ^^ (n => FunctionCallToken(n))

def functionWithArgs: Parser[FunctionCallToken] =
"""{{""" ~> functionName ~ """(""" ~ repsep(paramString | decimalNumber, ",") ~ """)""" <~ """}}""".r ^^ {
case func ~ _ ~ args ~ _ => FunctionCallToken(func, args)
}

def paramString: Parser[String] = """\"[0-9a-zA-Z\-"#\?]+\"""".r

def loopStart: Parser[LoopStartToken] = """{{#loop""" ~ decimalNumber ~ """}}""" ^^ {
case _ ~ number ~ _ => LoopStartToken(number.toInt)
}

def loopEnd: Parser[LoopEndToken.type] = "{{#endloop}}" ^^ { _ =>
LoopEndToken
}

def tokens: Parser[List[TemplateToken]] =
rep1(expression | plainText)
rep1(loopStart | loopEnd | expression | plainText)

def apply(str: String): Either[Throwable, List[TemplateToken]] =
parse(tokens, str) match {
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/phony/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ trait EitherInstances {
new Phony[Either[Throwable, ?]]

object languages {
implicit def ENGLISH: Locale[Either[Throwable, ?]] = DefaultLocale[Either[Throwable, ?]]("en")
implicit def ENGLISH: Locale[Either[Throwable, ?]] = DefaultLocale[Either[Throwable, *]]("en")
}
}
13 changes: 7 additions & 6 deletions core/src/main/scala/phony/resource/DefaultLocale.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package phony.resource

import java.io.InputStream

import cats.MonadError
import cats.implicits._
import cats.{Functor, MonadError}
import io.circe.generic.auto._
import io.circe.parser._
import phony.Locale
import phony.data._

import scala.util.{Failure, Success, Try}

class DefaultLocale[F[_]: Functor](val dataProvider: F[LocaleProvider]) extends Locale[F] {
class DefaultLocale[F[_]](val dataProvider: F[LocaleProvider])(implicit ev: MonadError[F, Throwable])
extends Locale[F] {
override def name: F[NameData] = dataProvider.map(_.names)

override def internet: F[InternetData] = dataProvider.map(_.internet)
Expand All @@ -30,8 +31,8 @@ object DefaultLocale {

val data: F[LocaleProvider] = (for {
resource <- resource(language)
content <- content(resource)
data <- toJson(content)
content <- content(resource)
data <- toJson(content)
} yield data).fold(ev.raiseError, ev.pure)

new DefaultLocale[F](data)
Expand All @@ -44,14 +45,14 @@ object DefaultLocale {

private def content(stream: InputStream) =
Try(scala.io.Source.fromInputStream(stream).getLines.mkString("\n")) match {
case Success(value) => Right(value)
case Success(value) => Right(value)
case Failure(exception) => Left(exception)
}

private[phony] def resource(language: String): Either[Throwable, InputStream] =
Try(resourceUrl(language).openStream) match {
case Success(value) => Right(value)
case Failure(_) => Left(new RuntimeException(s"Not supported language ${language}"))
case Failure(_) => Left(new RuntimeException(s"Not supported language ${language}"))
}

private[phony] def resourceUrl(language: String) =
Expand Down

0 comments on commit 853bb25

Please sign in to comment.