├── project └── build.properties ├── .gitignore ├── src ├── main │ └── scala │ │ └── co │ │ └── enear │ │ └── parsercombinators │ │ ├── compiler │ │ ├── WorkflowCompilationError.scala │ │ └── WorkflowCompiler.scala │ │ ├── lexer │ │ ├── WorkflowToken.scala │ │ └── WorkflowLexer.scala │ │ └── parser │ │ ├── WorkflowAST.scala │ │ └── WorkflowParser.scala └── test │ └── scala │ └── co │ └── enear │ └── parsercombinators │ └── WorkflowCompilerSpec.scala └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /RUNNING_PID 2 | /logs/ 3 | /project/*-shim.sbt 4 | /project/project/ 5 | /project/target/ 6 | /target/ 7 | /.idea/ 8 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/compiler/WorkflowCompilationError.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.compiler 2 | 3 | sealed trait WorkflowCompilationError 4 | 5 | case class WorkflowLexerError(location: Location, msg: String) extends WorkflowCompilationError 6 | case class WorkflowParserError(location: Location, msg: String) extends WorkflowCompilationError 7 | 8 | case class Location(line: Int, column: Int) { 9 | override def toString = s"$line:$column" 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/compiler/WorkflowCompiler.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.compiler 2 | 3 | import co.enear.parsercombinators.lexer.WorkflowLexer 4 | import co.enear.parsercombinators.parser.{WorkflowParser, WorkflowAST} 5 | 6 | object WorkflowCompiler { 7 | def apply(code: String): Either[WorkflowCompilationError, WorkflowAST] = { 8 | for { 9 | tokens <- WorkflowLexer(code).right 10 | ast <- WorkflowParser(tokens).right 11 | } yield ast 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/lexer/WorkflowToken.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.lexer 2 | 3 | import scala.util.parsing.input.Positional 4 | 5 | sealed trait WorkflowToken extends Positional 6 | 7 | case class IDENTIFIER(str: String) extends WorkflowToken 8 | case class LITERAL(str: String) extends WorkflowToken 9 | case class INDENTATION(spaces: Int) extends WorkflowToken 10 | case class EXIT() extends WorkflowToken 11 | case class READINPUT() extends WorkflowToken 12 | case class CALLSERVICE() extends WorkflowToken 13 | case class SWITCH() extends WorkflowToken 14 | case class OTHERWISE() extends WorkflowToken 15 | case class COLON() extends WorkflowToken 16 | case class ARROW() extends WorkflowToken 17 | case class EQUALS() extends WorkflowToken 18 | case class COMMA() extends WorkflowToken 19 | case class INDENT() extends WorkflowToken 20 | case class DEDENT() extends WorkflowToken 21 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/parser/WorkflowAST.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.parser 2 | 3 | import scala.util.parsing.input.Positional 4 | 5 | sealed trait WorkflowAST extends Positional 6 | case class AndThen(step1: WorkflowAST, step2: WorkflowAST) extends WorkflowAST 7 | case class ReadInput(inputs: Seq[String]) extends WorkflowAST 8 | case class CallService(serviceName: String) extends WorkflowAST 9 | case class Choice(alternatives: Seq[ConditionThen]) extends WorkflowAST 10 | case object Exit extends WorkflowAST 11 | 12 | sealed trait ConditionThen extends Positional { def thenBlock: WorkflowAST } 13 | case class IfThen(predicate: Condition, thenBlock: WorkflowAST) extends ConditionThen 14 | case class OtherwiseThen(thenBlock: WorkflowAST) extends ConditionThen 15 | 16 | sealed trait Condition extends Positional 17 | case class Equals(factName: String, factValue: String) extends Condition 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 e.near 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/test/scala/co/enear/parsercombinators/WorkflowCompilerSpec.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators 2 | 3 | import co.enear.parsercombinators.compiler.{Location, WorkflowCompiler, WorkflowParserError} 4 | import co.enear.parsercombinators.parser._ 5 | import org.scalatest.{FlatSpec, Matchers} 6 | 7 | class WorkflowCompilerSpec extends FlatSpec with Matchers { 8 | 9 | val validCode = 10 | """ 11 | |read input name, country 12 | |switch: 13 | | country == "PT" -> 14 | | call service "A" 15 | | exit 16 | | otherwise -> 17 | | call service "B" 18 | | switch: 19 | | name == "unknown" -> 20 | | exit 21 | | otherwise -> 22 | | call service "C" 23 | | exit 24 | """.stripMargin.trim 25 | 26 | val invalidCode = 27 | """ 28 | |read input name, country 29 | |switch: 30 | | country == PT -> 31 | | call service "A" 32 | | exit 33 | | otherwise -> 34 | | call service "B" 35 | | switch: 36 | | name == "unknown" -> 37 | | exit 38 | | otherwise -> 39 | | call service "C" 40 | | exit 41 | """.stripMargin.trim 42 | 43 | val successfulAST = AndThen( 44 | ReadInput(List("name", "country")), 45 | Choice(List( 46 | IfThen( Equals("country", "PT"), AndThen(CallService("A"), Exit) ), 47 | OtherwiseThen( 48 | AndThen( 49 | CallService("B"), 50 | Choice(List( 51 | IfThen( Equals("name", "unknown"), Exit ), 52 | OtherwiseThen( AndThen(CallService("C"), Exit) ) 53 | )) 54 | ) 55 | ) 56 | )) 57 | ) 58 | 59 | val errorMsg = WorkflowParserError(Location(3,14), "string literal expected") 60 | 61 | 62 | 63 | 64 | "Workflow compiler" should "successfully parse a valid workflow" in { 65 | WorkflowCompiler(validCode) shouldBe Right(successfulAST) 66 | } 67 | 68 | it should "return an error with an invalid workflow" in { 69 | WorkflowCompiler(invalidCode) shouldBe Left(errorMsg) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/parser/WorkflowParser.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.parser 2 | 3 | import co.enear.parsercombinators.compiler.{Location, WorkflowParserError} 4 | import co.enear.parsercombinators.lexer._ 5 | 6 | import scala.util.parsing.combinator.Parsers 7 | import scala.util.parsing.input.{NoPosition, Position, Reader} 8 | 9 | object WorkflowParser extends Parsers { 10 | override type Elem = WorkflowToken 11 | 12 | class WorkflowTokenReader(tokens: Seq[WorkflowToken]) extends Reader[WorkflowToken] { 13 | override def first: WorkflowToken = tokens.head 14 | override def atEnd: Boolean = tokens.isEmpty 15 | override def pos: Position = tokens.headOption.map(_.pos).getOrElse(NoPosition) 16 | override def rest: Reader[WorkflowToken] = new WorkflowTokenReader(tokens.tail) 17 | } 18 | 19 | 20 | def apply(tokens: Seq[WorkflowToken]): Either[WorkflowParserError, WorkflowAST] = { 21 | val reader = new WorkflowTokenReader(tokens) 22 | program(reader) match { 23 | case NoSuccess(msg, next) => Left(WorkflowParserError(Location(next.pos.line, next.pos.column), msg)) 24 | case Success(result, next) => Right(result) 25 | } 26 | } 27 | 28 | def program: Parser[WorkflowAST] = positioned { 29 | phrase(block) 30 | } 31 | 32 | def block: Parser[WorkflowAST] = positioned { 33 | rep1(statement) ^^ { case stmtList => stmtList reduceRight AndThen } 34 | } 35 | 36 | def statement: Parser[WorkflowAST] = positioned { 37 | val exit = EXIT() ^^ (_ => Exit) 38 | val readInput = READINPUT() ~ rep(identifier ~ COMMA()) ~ identifier ^^ { 39 | case read ~ inputs ~ IDENTIFIER(lastInput) => ReadInput(inputs.map(_._1.str) ++ List(lastInput)) 40 | } 41 | val callService = CALLSERVICE() ~ literal ^^ { 42 | case call ~ LITERAL(serviceName) => CallService(serviceName) 43 | } 44 | val switch = SWITCH() ~ COLON() ~ INDENT() ~ rep1(ifThen) ~ opt(otherwiseThen) ~ DEDENT() ^^ { 45 | case _ ~ _ ~ _ ~ ifs ~ otherwise ~ _ => Choice(ifs ++ otherwise) 46 | } 47 | exit | readInput | callService | switch 48 | } 49 | 50 | def ifThen: Parser[IfThen] = positioned { 51 | (condition ~ ARROW() ~ INDENT() ~ block ~ DEDENT()) ^^ { 52 | case cond ~ _ ~ _ ~ block ~ _ => IfThen(cond, block) 53 | } 54 | } 55 | 56 | def otherwiseThen: Parser[OtherwiseThen] = positioned { 57 | (OTHERWISE() ~ ARROW() ~ INDENT() ~ block ~ DEDENT()) ^^ { 58 | case _ ~ _ ~ _ ~ block ~ _ => OtherwiseThen(block) 59 | } 60 | } 61 | 62 | def condition: Parser[Equals] = positioned { 63 | (identifier ~ EQUALS() ~ literal) ^^ { case IDENTIFIER(id) ~ eq ~ LITERAL(lit) => Equals(id, lit) } 64 | } 65 | 66 | private def identifier: Parser[IDENTIFIER] = positioned { 67 | accept("identifier", { case id @ IDENTIFIER(name) => id }) 68 | } 69 | 70 | private def literal: Parser[LITERAL] = positioned { 71 | accept("string literal", { case lit @ LITERAL(name) => lit }) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/co/enear/parsercombinators/lexer/WorkflowLexer.scala: -------------------------------------------------------------------------------- 1 | package co.enear.parsercombinators.lexer 2 | 3 | import co.enear.parsercombinators.compiler.{Location, WorkflowLexerError} 4 | 5 | import scala.util.parsing.combinator.RegexParsers 6 | 7 | object WorkflowLexer extends RegexParsers { 8 | override def skipWhitespace = true 9 | override val whiteSpace = "[ \t\r\f]+".r 10 | 11 | def apply(code: String): Either[WorkflowLexerError, List[WorkflowToken]] = { 12 | parse(tokens, code) match { 13 | case NoSuccess(msg, next) => Left(WorkflowLexerError(Location(next.pos.line, next.pos.column), msg)) 14 | case Success(result, next) => Right(result) 15 | } 16 | } 17 | 18 | def tokens: Parser[List[WorkflowToken]] = { 19 | phrase(rep1(exit | readInput | callService | switch | otherwise | colon | arrow 20 | | equals | comma | literal | identifier | indentation)) ^^ { rawTokens => 21 | processIndentations(rawTokens) 22 | } 23 | } 24 | 25 | private def processIndentations(tokens: List[WorkflowToken], 26 | indents: List[Int] = List(0)): List[WorkflowToken] = { 27 | tokens.headOption match { 28 | 29 | // if there is an increase in indentation level, we push this new level into the stack 30 | // and produce an INDENT 31 | case Some(INDENTATION(spaces)) if spaces > indents.head => 32 | INDENT() :: processIndentations(tokens.tail, spaces :: indents) 33 | 34 | // if there is a decrease, we pop from the stack until we have matched the new level and 35 | // we produce a DEDENT for each pop 36 | case Some(INDENTATION(spaces)) if spaces < indents.head => 37 | val (dropped, kept) = indents.partition(_ > spaces) 38 | (dropped map (_ => DEDENT())) ::: processIndentations(tokens.tail, kept) 39 | 40 | // if the indentation level stays unchanged, no tokens are produced 41 | case Some(INDENTATION(spaces)) if spaces == indents.head => 42 | processIndentations(tokens.tail, indents) 43 | 44 | // other tokens are ignored 45 | case Some(token) => 46 | token :: processIndentations(tokens.tail, indents) 47 | 48 | // the final step is to produce a DEDENT for each indentation level still remaining, thus 49 | // "closing" the remaining open INDENTS 50 | case None => 51 | indents.filter(_ > 0).map(_ => DEDENT()) 52 | 53 | } 54 | } 55 | 56 | def identifier: Parser[IDENTIFIER] = positioned { 57 | "[a-zA-Z_][a-zA-Z0-9_]*".r ^^ { str => IDENTIFIER(str) } 58 | } 59 | 60 | def literal: Parser[LITERAL] = positioned { 61 | """"[^"]*"""".r ^^ { str => 62 | val content = str.substring(1, str.length - 1) 63 | LITERAL(content) 64 | } 65 | } 66 | 67 | def indentation: Parser[INDENTATION] = positioned { 68 | "\n[ ]*".r ^^ { whitespace => 69 | val nSpaces = whitespace.length - 1 70 | INDENTATION(nSpaces) 71 | } 72 | } 73 | 74 | def exit = positioned { "exit" ^^ (_ => EXIT()) } 75 | def readInput = positioned { "read input" ^^ (_ => READINPUT()) } 76 | def callService = positioned { "call service" ^^ (_ => CALLSERVICE()) } 77 | def switch = positioned { "switch" ^^ (_ => SWITCH()) } 78 | def otherwise = positioned { "otherwise" ^^ (_ => OTHERWISE()) } 79 | def colon = positioned { ":" ^^ (_ => COLON()) } 80 | def arrow = positioned { "->" ^^ (_ => ARROW()) } 81 | def equals = positioned { "==" ^^ (_ => EQUALS()) } 82 | def comma = positioned { "," ^^ (_ => COMMA()) } 83 | 84 | } 85 | --------------------------------------------------------------------------------