{"action":"created","comment":{"url":"https://api.github.com/repos/enear/enear.github.com/pulls/comments/56653515","id":56653515,"diff_hunk":"@@ -0,0 +1,470 @@\n+---\n+layout: post\n+title: Building a lexer and parser with Scala's Parser Combinators\n+---\n+\n+\n+As part of an ongoing project at [**e.near**](http://www.enear.co/), one of our Scala teams was recently tasked with a requirement to build an interpreter for executing workflows which are modelled with a textal DSL. These workflows had to be validated for errors, compiled to a simpler bytecode-like representation and stored, in order to be interpreted with as little overhead as possible.\n+\n+To deal with the parsing phases, we stumbled upon [Scala's Parser Combinators](https://github.com/scala/scala-parser-combinators), which turned out to be a great tool for writing modular and composable lexers and parsers in idiomatic Scala, with little to no boilerplate code.\n+\n+While there are a few Web resources on how to use parser combinators for building simple parsers, there were none so far that describe how to build full lexical and syntactical analyzers from scratch.\n+\n+We will be going through the process of building a lexer, to scan text into a sequence of tokens, and a parser, to parse said tokens into an abstract syntax tree (AST).\n+\n+\n+### Language overview\n+\n+The language we will be here is a subset of the \"workflow code\" we have implemented for our customer. A program implements a workflow, i.e. an executable directed acyclic graph of instructions. A workflow keeps an internal set of facts and may call services which consume facts to produce other facts.\n+\n+Below is an example of a valid program. Keep in mind that blocks are delimited by [indentation](https://en.wikipedia.org/wiki/Off-side_rule), in a similar way to languages like Python.\n+\n+```\n+read input name, country\n+switch:\n+  country == \"PT\" ->\n+    call service \"A\"\n+    exit\n+  otherwise ->\n+    call service \"B\"\n+    switch:\n+      name == \"unknown\" ->\n+        exit\n+      otherwise ->\n+        call service \"C\"\n+        exit\n+```\n+\n+We want this to be parsed into:\n+\n+```scala\n+AndThen(\n+  ReadInput(List(name, country)),\n+  Choice(List(\n+    IfThen(\n+      Equals(country, PT),\n+      AndThen(CallService(A), Exit)\n+    ),\n+    OtherwiseThen(\n+      AndThen(\n+        CallService(B),\n+        Choice(List(\n+          IfThen(Equals(name, unknown), Exit),\n+          OtherwiseThen(AndThen(CallService(C), Exit))\n+        ))\n+      )\n+    )\n+  ))\n+)\n+```\n+\n+A possible [BNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form) representation for this grammar is outlined below.\n+\n+```\n+<block> ::= (<statement>)+\n+\n+<statement> ::= \"exit\"\n+              | \"read input\" (<identifier> \",\")* <identifier>\n+              | \"call service\" <stringLiteral>\n+              | \"switch\" \":\" INDENT (<ifThen>)+ [otherwiseThen] DEDENT\n+\n+<ifThen> ::= <condition> \"->\" INDENT <block> DEDENT\n+\n+<otherwiseThen> ::= \"otherwise\" \"->\" INDENT <block> DEDENT\n+\n+<condition> ::= <identifier> \"==\" <stringLiteral>\n+```\n+\n+\n+### Parser combinators\n+\n+A [parser combinator](https://en.wikipedia.org/wiki/Parser_combinator) is simply a function which accepts parsers as input and returns a new parser as output, similarly to how higher-order functions rely on calling other functions that are passed as input to produce a new function as output.\n+\n+As an example, assuming we have a parser `int` that recognizes integer literals and a parser `plus` that recognizes the '+' character, we can produce a parser that recognizes the sequence `int plus int` as an integer addition.\n+\n+The Scala standard library includes an implementation of parser combinators which is hosted at:\n+<https://github.com/scala/scala-parser-combinators>\n+\n+To use it, you will simply need the following dependency on your project:\n+`\"org.scala-lang.modules\" %% \"scala-parser-combinators\" % \"1.0.4\"`\n+\n+\n+### Building a Lexer\n+\n+We will be needing tokens for identifiers and string literals, as well as all the reserved keywords and punctuation marks: `exit`, `read input`, `call service`, `switch`, `otherwise`, `:`, `->`, `==`, and `,`.\n+\n+We also need to produce artifical tokens that represent increases and decreases in identation: `INDENT` and `DEDENT`, respectively. Please ignore these for now, as we will be going over them at later phase.\n+\n+```scala\n+sealed trait WorkflowToken\n+\n+case class IDENTIFIER(str: String) extends WorkflowToken\n+case class LITERAL(str: String) extends WorkflowToken\n+case class INDENTATION(spaces: Int) extends WorkflowToken\n+case object EXIT extends WorkflowToken\n+case object READINPUT extends WorkflowToken\n+case object CALLSERVICE extends WorkflowToken\n+case object SWITCH extends WorkflowToken\n+case object OTHERWISE extends WorkflowToken\n+case object COLON extends WorkflowToken\n+case object ARROW extends WorkflowToken\n+case object EQUALS extends WorkflowToken\n+case object COMMA extends WorkflowToken\n+case object INDENT extends WorkflowToken\n+case object DEDENT extends WorkflowToken\n+```\n+\n+Our lexer extends from [RegexParsers](http://www.scala-lang.org/api/rc/index.html#scala.util.parsing.combinator.RegexParsers). `RegexParsers` is tailored for building character parsers using [regular expressions](https://en.wikipedia.org/wiki/Regular_expression). It provides implicit conversions from `String` and `Regex` to `Parser[String]`, allowing us to use them as the starting point for composing increasingly complex parsers.\n+\n+```scala\n+object WorkflowLexer extends RegexParsers {\n+```\n+\n+Let's start by specifying which characters should be ignored as whitespace:\n+\n+```scala\n+override def skipWhitespace = true\n+override val whiteSpace = \"[ \\t\\r\\f]+\".r\n+```\n+\n+Now let's try building a parser for identifiers:\n+\n+```scala\n+def identifier: Parser[IDENTIFIER] = {\n+  \"[a-zA-Z_][a-zA-Z0-9_]*\".r ^^ { str => IDENTIFIER(str) }\n+}\n+```\n+\n+The `^^` operator acts as a map over the parse result. The regex `\"[a-zA-Z_][a-zA-Z0-9_]*\".r` is implicitly converted to an instance of `Parser[String]`, on which we map a function `(String => IDENTIFIER)`, thus returning a instance of `Parser[IDENTIFIER]`.\n+\n+The parsers for string literals and identations are similar:\n+\n+```scala\n+def literal: Parser[LITERAL] = {\n+  \"\"\"\"[^\"]*\"\"\"\".r ^^ { str =>\n+    val content = str.substring(1, str.length - 1)\n+    LITERAL(content)\n+  }\n+}\n+\n+def indentation: Parser[INDENTATION] = {\n+  \"\\n[ ]*\".r ^^ { whitespace =>\n+    val nSpaces = whitespace.length - 1\n+    INDENTATION(nSpaces)\n+  }\n+}\n+```\n+\n+Generating parsers for keywords is trivial:\n+\n+```scala\n+def exit          = \"exit\"          ^^ (_ => EXIT)\n+def readInput     = \"read input\"    ^^ (_ => READINPUT)\n+def callService   = \"call service\"  ^^ (_ => CALLSERVICE)\n+def switch        = \"switch\"        ^^ (_ => SWITCH)\n+def otherwise     = \"otherwise\"     ^^ (_ => OTHERWISE)\n+def colon         = \":\"             ^^ (_ => COLON)\n+def arrow         = \"->\"            ^^ (_ => ARROW)\n+def equals        = \"==\"            ^^ (_ => EQUALS)\n+def comma         = \",\"             ^^ (_ => COMMA)\n+```\n+\n+We will now compose all of these into a parser capable of recognizing every token. We will take advantage the following operators:\n+\n+ - `|` (or), for recognizing any of our token parsers;\n+ - `rep1`, which recognizes one or more repetitions of its argument;\n+ - `phrase`, which attempts to consume all input until no more is left.\n+\n+```scala\n+def tokens: Parser[List[WorkflowToken]] = {\n+  phrase(rep1(exit | readInput | callService | switch | otherwise | colon | arrow\n+     | equals | comma | literal | identifier | indentation)) ^^ { rawTokens =>\n+    processIndentations(rawTokens)\n+  }\n+}\n+```\n+\n+Note that the order of operands matters when dealing with ambiguity. If we were to place `identifier` before `exit`, `readInput`, etc., our parser would never recognize them as special keywords, since they would be successfully parsed as identifiers instead.\n+\n+\n+We apply a brief post-processing step to our parse result with the `processIndentations` method. This is used to produce the artifical `INDENT` and `DEDENT` tokens from the `INDENTATION` tokens. Each increase in indentation level will be pushed to a stack, producing an `INDENT`, and decreases in indentation level will be popped from the indentation stack, producing `DEDENT`s.\n+\n+```scala\n+private def processIndentations(tokens: List[WorkflowToken],\n+                                indents: List[Int] = List(0)): List[WorkflowToken] = {\n+  tokens.headOption match {\n+\n+    // if there is an increase in indentation level, we push this new level into the stack\n+    // and produce an INDENT\n+    case Some(INDENTATION(spaces)) if spaces > indents.head =>\n+      INDENT :: processIndentations(tokens.tail, spaces :: indents)\n+\n+    // if there is a decrease, we pop from the stack until we have matched the new level,\n+    // producing a DEDENT for each pop\n+    case Some(INDENTATION(spaces)) if spaces < indents.head =>\n+      val (dropped, kept) = indents.partition(_ > spaces)\n+      (dropped map (_ => DEDENT)) ::: processIndentations(tokens.tail, kept)\n+\n+    // if the indentation level stays unchanged, no tokens are produced\n+    case Some(INDENTATION(spaces)) if spaces == indents.head =>\n+      processIndentations(tokens.tail, indents)\n+\n+    // other tokens are ignored\n+    case Some(token) =>\n+      token :: processIndentations(tokens.tail, indents)\n+\n+    // the final step is to produce a DEDENT for each indentation level still remaining, thus\n+    // \"closing\" the remaining open INDENTS\n+    case None =>\n+      indents.filter(_ > 0).map(_ => DEDENT)\n+\n+  }\n+}\n+```\n+\n+And we're all set! This token parser will produce a `ParseResult[List[WorkflowToken]]` by consuming a `Reader[Char]`. `RegexParsers` defines its own `Reader[Char]`, which is internally called by the `parse` method it provides. Let's then define an `apply` method for `WorkflowLexer`:\n+\n+```scala\n+trait WorkflowCompilationError\n+case class WorkflowLexerError(msg: String) extends WorkflowCompilationError\n+\n+def apply(code: String): Either[WorkflowLexerError, List[WorkflowToken]] = {\n+  parse(tokens, code) match {\n+    case NoSuccess(msg, next) => Left(WorkflowLexerError(msg))\n+    case Success(result, next) => Right(result)\n+  }\n+}\n+```\n+\n+Let's try our lexer on the example above:\n+\n+```scala\n+scala> WorkflowLexer(code)\n+res0: Either[WorkflowLexerError,List[WorkflowToken]] = Right(List(READINPUT, IDENTIFIER(name), COMMA,\n+IDENTIFIER(country), SWITCH, COLON, INDENT, IDENTIFIER(country), EQUALS, LITERAL(PT), ARROW, INDENT, \n+CALLSERVICE, LITERAL(A), EXIT, DEDENT, OTHERWISE, ARROW, INDENT, CALLSERVICE, LITERAL(B), SWITCH, COLON, \n+INDENT, IDENTIFIER(name), EQUALS, LITERAL(unknown), ARROW, INDENT, EXIT, DEDENT, OTHERWISE, ARROW, \n+INDENT, CALLSERVICE, LITERAL(C), EXIT, DEDENT, DEDENT, DEDENT, DEDENT))\n+```\n+\n+\n+\n+### Building a Parser\n+\n+Now that we have taken care of the lexical analysis, we are still missing the syntactic analysis step, i.e. transforming a sequence of tokens into an abstract syntax tree (AST). Unlike `RegexParsers` which generate `String` parsers, we will be needing a `WorkflowToken` parser.\n+\n+\n+```scala\n+object WorkflowParser extends Parsers {\n+  override type Elem = WorkflowToken\n+```\n+\n+\n+We also need to define a `Reader[WorkflowToken]` which will be used by the parser to read from a sequence of `WorkflowToken`s. This is pretty straightforward:\n+\n+\n+```scala\n+class WorkflowTokenReader(tokens: Seq[WorkflowToken]) extends Reader[WorkflowToken] {\n+  override def first: WorkflowToken = tokens.head\n+  override def atEnd: Boolean = tokens.isEmpty\n+  override def pos: Position = tokens.headOption.map(_.pos).getOrElse(NoPosition)\n+  override def rest: Reader[WorkflowToken] = new WorkflowTokenReader(tokens.tail)\n+}\n+```\n+\n+\n+Moving on with the parser implementation, the process is similar to the one used to build the lexer. We define simple parsers and compose them into more complex ones. Only this time around our parsers will be returning ASTs instead of tokens:\n+\n+\n+```scala\n+sealed trait WorkflowAST\n+case class AndThen(step1: WorkflowAST, step2: WorkflowAST) extends WorkflowAST\n+case class ReadInput(inputs: Seq[String]) extends WorkflowAST\n+case class CallService(serviceName: String) extends WorkflowAST\n+case class Choice(alternatives: Seq[ConditionThen]) extends WorkflowAST\n+case object Exit extends WorkflowAST\n+\n+sealed trait ConditionThen { def thenBlock: WorkflowAST }\n+case class IfThen(predicate: Condition, thenBlock: WorkflowAST) extends ConditionThen\n+case class OtherwiseThen(thenBlock: WorkflowAST) extends ConditionThen\n+\n+sealed trait Condition\n+case class Equals(factName: String, factValue: String) extends Condition\n+```\n+\n+\n+Being a `WorkflowToken` parser, we inherit an implicit conversion from `WorkflowToken` to `Parser[WorkflowToken]`. This is useful for parsing parameterless tokens, such as `EXIT`, `CALLSERVICE`, etc. For `IDENTIFIER` and `LITERAL` we can pattern match on these tokens with the `accept` method.\n+\n+\n+```scala\n+private def identifier: Parser[IDENTIFIER] = {\n+  accept(\"identifier\", { case id @ IDENTIFIER(name) => id })\n+}\n+\n+private def literal: Parser[LITERAL] = {\n+  accept(\"string literal\", { case lit @ LITERAL(name) => lit })\n+}\n+```\n+\n+\n+Grammar rules may be implemented as such:\n+\n+\n+```scala\n+def condition: Parser[Equals] = {\n+  (identifier ~ EQUALS ~ literal) ^^ { case id ~ eq ~ lit => Equals(id, lit) }\n+}\n+```\n+\n+\n+This is similar to what we did previously to produce tokens; here we mapped the parse result (a composition of the results of parsers `identifier`, `EQUALS` and `literal`) to an instance of `Equals`. Notice how pattern matching may be used to expressively unpack the result of a parser composition by sequencing (i.e. the `~` operator).\n+\n+The implementation of the remaining rules looks very much like the grammar we have specified above:\n+\n+\n+```scala\n+def program: Parser[WorkflowAST] = {\n+  phrase(block)\n+}\n+\n+def block: Parser[WorkflowAST] = {\n+  rep1(statement) ^^ { case stmtList => stmtList reduceRight AndThen }\n+}\n+\n+def statement: Parser[WorkflowAST] = {\n+  val exit = EXIT ^^ (_ => Exit)\n+  val readInput = READINPUT ~ rep(identifier ~ COMMA) ~ identifier ^^ {\n+    case read ~ inputs ~ IDENTIFIER(lastInput) => ReadInput(inputs.map(_._1.str) ++ List(lastInput))\n+  }\n+  val callService = CALLSERVICE ~ literal ^^ {\n+    case call ~ LITERAL(serviceName) => CallService(serviceName)\n+  }\n+  val switch = SWITCH ~ COLON ~ INDENT ~ rep1(ifThen) ~ opt(otherwiseThen) ~ DEDENT ^^ {\n+    case _ ~ _ ~ _ ~ ifs ~ otherwise ~ _ => Choice(ifs ++ otherwise)\n+  }\n+  exit | readInput | callService | switch\n+}\n+\n+def ifThen: Parser[IfThen] = {\n+  (condition ~ ARROW ~ INDENT ~ block ~ DEDENT) ^^ {\n+    case cond ~ _ ~ _ ~ block ~ _ => IfThen(cond, block)\n+  }\n+}\n+\n+def otherwiseThen: Parser[OtherwiseThen] = {\n+  (OTHERWISE ~ ARROW ~ INDENT ~ block ~ DEDENT) ^^ {\n+    case _ ~ _ ~ _ ~ block ~ _ => OtherwiseThen(block)\n+  }\n+}\n+```\n+\n+\n+Just like we did with the lexer, we also define a monadic apply method that we can later use to express a pipeline of operations:\n+\n+\n+```scala\n+case class WorkflowParserError(msg: String) extends WorkflowCompilationError\n+\n+def apply(tokens: Seq[WorkflowToken]): Either[WorkflowParserError, WorkflowAST] = {\n+  val reader = new WorkflowTokenReader(tokens)\n+  program(reader) match {\n+    case NoSuccess(msg, next) => Left(WorkflowParserError(msg))\n+    case Success(result, next) => Right(result)\n+  }\n+}\n+```\n+\n+\n+### Pipelining\n+\n+We have now covered both the lexical and syntactical analysers. All that's left is to chain them together:\n+\n+\n+```scala\n+object WorkflowCompiler {\n+  def apply(code: String): Either[WorkflowCompilationError, WorkflowAST] = {\n+    for {\n+      tokens <- WorkflowLexer(code).right\n+      ast <- WorkflowParser(tokens).right\n+    } yield ast\n+  }\n+}\n+```\n+\n+\n+Let's try it on our sample program:\n+\n+```scala\n+scala> WorkflowCompiler(code)\n+res0: Either[WorkflowCompilationError,WorkflowAST] = Right(AndThen(ReadInput(List(name, country)),\n+Choice(List(IfThen(Equals(country,PT),AndThen(CallService(A),Exit)),OtherwiseThen(AndThen(CallService(B),\n+Choice(List(IfThen(Equals(name,unknown),Exit), OtherwiseThen(AndThen(CallService(C),Exit))))))))))\n+```\n+\n+Great! Our compiler has proven to be capable of parsing valid programs.","path":"_posts/2016-03-18-parser-combinators.md","position":416,"original_position":404,"commit_id":"025f4fa5ad25ef754be2f0cc40a35d7935a157ce","original_commit_id":"c4c6e23e4e838a2e558b14fa7bd5c3cd0b821aad","user":{"login":"jtjeferreira","id":943051,"avatar_url":"https://avatars.githubusercontent.com/u/943051?v=3","gravatar_id":"","url":"https://api.github.com/users/jtjeferreira","html_url":"https://github.com/jtjeferreira","followers_url":"https://api.github.com/users/jtjeferreira/followers","following_url":"https://api.github.com/users/jtjeferreira/following{/other_user}","gists_url":"https://api.github.com/users/jtjeferreira/gists{/gist_id}","starred_url":"https://api.github.com/users/jtjeferreira/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jtjeferreira/subscriptions","organizations_url":"https://api.github.com/users/jtjeferreira/orgs","repos_url":"https://api.github.com/users/jtjeferreira/repos","events_url":"https://api.github.com/users/jtjeferreira/events{/privacy}","received_events_url":"https://api.github.com/users/jtjeferreira/received_events","type":"User","site_admin":false},"body":"new section on error handling","created_at":"2016-03-18T13:15:27Z","updated_at":"2016-03-18T13:15:27Z","html_url":"https://github.com/enear/enear.github.com/pull/1#discussion_r56653515","pull_request_url":"https://api.github.com/repos/enear/enear.github.com/pulls/1","_links":{"self":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/comments/56653515"},"html":{"href":"https://github.com/enear/enear.github.com/pull/1#discussion_r56653515"},"pull_request":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/enear/enear.github.com/pulls/1","id":63356912,"html_url":"https://github.com/enear/enear.github.com/pull/1","diff_url":"https://github.com/enear/enear.github.com/pull/1.diff","patch_url":"https://github.com/enear/enear.github.com/pull/1.patch","issue_url":"https://api.github.com/repos/enear/enear.github.com/issues/1","number":1,"state":"open","locked":false,"title":"Parser combinators tutorial","user":{"login":"pedropramos","id":3950222,"avatar_url":"https://avatars.githubusercontent.com/u/3950222?v=3","gravatar_id":"","url":"https://api.github.com/users/pedropramos","html_url":"https://github.com/pedropramos","followers_url":"https://api.github.com/users/pedropramos/followers","following_url":"https://api.github.com/users/pedropramos/following{/other_user}","gists_url":"https://api.github.com/users/pedropramos/gists{/gist_id}","starred_url":"https://api.github.com/users/pedropramos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pedropramos/subscriptions","organizations_url":"https://api.github.com/users/pedropramos/orgs","repos_url":"https://api.github.com/users/pedropramos/repos","events_url":"https://api.github.com/users/pedropramos/events{/privacy}","received_events_url":"https://api.github.com/users/pedropramos/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-03-18T10:52:55Z","updated_at":"2016-03-18T13:15:27Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cf755185db163d7166427a5b6d2eb12a84f17aed","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/enear/enear.github.com/pulls/1/commits","review_comments_url":"https://api.github.com/repos/enear/enear.github.com/pulls/1/comments","review_comment_url":"https://api.github.com/repos/enear/enear.github.com/pulls/comments{/number}","comments_url":"https://api.github.com/repos/enear/enear.github.com/issues/1/comments","statuses_url":"https://api.github.com/repos/enear/enear.github.com/statuses/025f4fa5ad25ef754be2f0cc40a35d7935a157ce","head":{"label":"pedropramos:parser-combinators-tutorial","ref":"parser-combinators-tutorial","sha":"025f4fa5ad25ef754be2f0cc40a35d7935a157ce","user":{"login":"pedropramos","id":3950222,"avatar_url":"https://avatars.githubusercontent.com/u/3950222?v=3","gravatar_id":"","url":"https://api.github.com/users/pedropramos","html_url":"https://github.com/pedropramos","followers_url":"https://api.github.com/users/pedropramos/followers","following_url":"https://api.github.com/users/pedropramos/following{/other_user}","gists_url":"https://api.github.com/users/pedropramos/gists{/gist_id}","starred_url":"https://api.github.com/users/pedropramos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pedropramos/subscriptions","organizations_url":"https://api.github.com/users/pedropramos/orgs","repos_url":"https://api.github.com/users/pedropramos/repos","events_url":"https://api.github.com/users/pedropramos/events{/privacy}","received_events_url":"https://api.github.com/users/pedropramos/received_events","type":"User","site_admin":false},"repo":{"id":54194200,"name":"enear.github.com","full_name":"pedropramos/enear.github.com","owner":{"login":"pedropramos","id":3950222,"avatar_url":"https://avatars.githubusercontent.com/u/3950222?v=3","gravatar_id":"","url":"https://api.github.com/users/pedropramos","html_url":"https://github.com/pedropramos","followers_url":"https://api.github.com/users/pedropramos/followers","following_url":"https://api.github.com/users/pedropramos/following{/other_user}","gists_url":"https://api.github.com/users/pedropramos/gists{/gist_id}","starred_url":"https://api.github.com/users/pedropramos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pedropramos/subscriptions","organizations_url":"https://api.github.com/users/pedropramos/orgs","repos_url":"https://api.github.com/users/pedropramos/repos","events_url":"https://api.github.com/users/pedropramos/events{/privacy}","received_events_url":"https://api.github.com/users/pedropramos/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/pedropramos/enear.github.com","description":"The Jekyll Butler.","fork":true,"url":"https://api.github.com/repos/pedropramos/enear.github.com","forks_url":"https://api.github.com/repos/pedropramos/enear.github.com/forks","keys_url":"https://api.github.com/repos/pedropramos/enear.github.com/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pedropramos/enear.github.com/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pedropramos/enear.github.com/teams","hooks_url":"https://api.github.com/repos/pedropramos/enear.github.com/hooks","issue_events_url":"https://api.github.com/repos/pedropramos/enear.github.com/issues/events{/number}","events_url":"https://api.github.com/repos/pedropramos/enear.github.com/events","assignees_url":"https://api.github.com/repos/pedropramos/enear.github.com/assignees{/user}","branches_url":"https://api.github.com/repos/pedropramos/enear.github.com/branches{/branch}","tags_url":"https://api.github.com/repos/pedropramos/enear.github.com/tags","blobs_url":"https://api.github.com/repos/pedropramos/enear.github.com/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pedropramos/enear.github.com/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pedropramos/enear.github.com/git/refs{/sha}","trees_url":"https://api.github.com/repos/pedropramos/enear.github.com/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pedropramos/enear.github.com/statuses/{sha}","languages_url":"https://api.github.com/repos/pedropramos/enear.github.com/languages","stargazers_url":"https://api.github.com/repos/pedropramos/enear.github.com/stargazers","contributors_url":"https://api.github.com/repos/pedropramos/enear.github.com/contributors","subscribers_url":"https://api.github.com/repos/pedropramos/enear.github.com/subscribers","subscription_url":"https://api.github.com/repos/pedropramos/enear.github.com/subscription","commits_url":"https://api.github.com/repos/pedropramos/enear.github.com/commits{/sha}","git_commits_url":"https://api.github.com/repos/pedropramos/enear.github.com/git/commits{/sha}","comments_url":"https://api.github.com/repos/pedropramos/enear.github.com/comments{/number}","issue_comment_url":"https://api.github.com/repos/pedropramos/enear.github.com/issues/comments{/number}","contents_url":"https://api.github.com/repos/pedropramos/enear.github.com/contents/{+path}","compare_url":"https://api.github.com/repos/pedropramos/enear.github.com/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pedropramos/enear.github.com/merges","archive_url":"https://api.github.com/repos/pedropramos/enear.github.com/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pedropramos/enear.github.com/downloads","issues_url":"https://api.github.com/repos/pedropramos/enear.github.com/issues{/number}","pulls_url":"https://api.github.com/repos/pedropramos/enear.github.com/pulls{/number}","milestones_url":"https://api.github.com/repos/pedropramos/enear.github.com/milestones{/number}","notifications_url":"https://api.github.com/repos/pedropramos/enear.github.com/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pedropramos/enear.github.com/labels{/name}","releases_url":"https://api.github.com/repos/pedropramos/enear.github.com/releases{/id}","deployments_url":"https://api.github.com/repos/pedropramos/enear.github.com/deployments","created_at":"2016-03-18T10:45:29Z","updated_at":"2016-03-18T10:45:31Z","pushed_at":"2016-03-18T13:15:11Z","git_url":"git://github.com/pedropramos/enear.github.com.git","ssh_url":"git@github.com:pedropramos/enear.github.com.git","clone_url":"https://github.com/pedropramos/enear.github.com.git","svn_url":"https://github.com/pedropramos/enear.github.com","homepage":"http://getpoole.com","size":108,"stargazers_count":0,"watchers_count":0,"language":"CSS","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"enear:master","ref":"master","sha":"503e8edf2f1d93dcf3c9bf103478bf040bfee7eb","user":{"login":"enear","id":16413367,"avatar_url":"https://avatars.githubusercontent.com/u/16413367?v=3","gravatar_id":"","url":"https://api.github.com/users/enear","html_url":"https://github.com/enear","followers_url":"https://api.github.com/users/enear/followers","following_url":"https://api.github.com/users/enear/following{/other_user}","gists_url":"https://api.github.com/users/enear/gists{/gist_id}","starred_url":"https://api.github.com/users/enear/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/enear/subscriptions","organizations_url":"https://api.github.com/users/enear/orgs","repos_url":"https://api.github.com/users/enear/repos","events_url":"https://api.github.com/users/enear/events{/privacy}","received_events_url":"https://api.github.com/users/enear/received_events","type":"Organization","site_admin":false},"repo":{"id":53951990,"name":"enear.github.com","full_name":"enear/enear.github.com","owner":{"login":"enear","id":16413367,"avatar_url":"https://avatars.githubusercontent.com/u/16413367?v=3","gravatar_id":"","url":"https://api.github.com/users/enear","html_url":"https://github.com/enear","followers_url":"https://api.github.com/users/enear/followers","following_url":"https://api.github.com/users/enear/following{/other_user}","gists_url":"https://api.github.com/users/enear/gists{/gist_id}","starred_url":"https://api.github.com/users/enear/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/enear/subscriptions","organizations_url":"https://api.github.com/users/enear/orgs","repos_url":"https://api.github.com/users/enear/repos","events_url":"https://api.github.com/users/enear/events{/privacy}","received_events_url":"https://api.github.com/users/enear/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/enear/enear.github.com","description":"The Jekyll Butler.","fork":true,"url":"https://api.github.com/repos/enear/enear.github.com","forks_url":"https://api.github.com/repos/enear/enear.github.com/forks","keys_url":"https://api.github.com/repos/enear/enear.github.com/keys{/key_id}","collaborators_url":"https://api.github.com/repos/enear/enear.github.com/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/enear/enear.github.com/teams","hooks_url":"https://api.github.com/repos/enear/enear.github.com/hooks","issue_events_url":"https://api.github.com/repos/enear/enear.github.com/issues/events{/number}","events_url":"https://api.github.com/repos/enear/enear.github.com/events","assignees_url":"https://api.github.com/repos/enear/enear.github.com/assignees{/user}","branches_url":"https://api.github.com/repos/enear/enear.github.com/branches{/branch}","tags_url":"https://api.github.com/repos/enear/enear.github.com/tags","blobs_url":"https://api.github.com/repos/enear/enear.github.com/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/enear/enear.github.com/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/enear/enear.github.com/git/refs{/sha}","trees_url":"https://api.github.com/repos/enear/enear.github.com/git/trees{/sha}","statuses_url":"https://api.github.com/repos/enear/enear.github.com/statuses/{sha}","languages_url":"https://api.github.com/repos/enear/enear.github.com/languages","stargazers_url":"https://api.github.com/repos/enear/enear.github.com/stargazers","contributors_url":"https://api.github.com/repos/enear/enear.github.com/contributors","subscribers_url":"https://api.github.com/repos/enear/enear.github.com/subscribers","subscription_url":"https://api.github.com/repos/enear/enear.github.com/subscription","commits_url":"https://api.github.com/repos/enear/enear.github.com/commits{/sha}","git_commits_url":"https://api.github.com/repos/enear/enear.github.com/git/commits{/sha}","comments_url":"https://api.github.com/repos/enear/enear.github.com/comments{/number}","issue_comment_url":"https://api.github.com/repos/enear/enear.github.com/issues/comments{/number}","contents_url":"https://api.github.com/repos/enear/enear.github.com/contents/{+path}","compare_url":"https://api.github.com/repos/enear/enear.github.com/compare/{base}...{head}","merges_url":"https://api.github.com/repos/enear/enear.github.com/merges","archive_url":"https://api.github.com/repos/enear/enear.github.com/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/enear/enear.github.com/downloads","issues_url":"https://api.github.com/repos/enear/enear.github.com/issues{/number}","pulls_url":"https://api.github.com/repos/enear/enear.github.com/pulls{/number}","milestones_url":"https://api.github.com/repos/enear/enear.github.com/milestones{/number}","notifications_url":"https://api.github.com/repos/enear/enear.github.com/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/enear/enear.github.com/labels{/name}","releases_url":"https://api.github.com/repos/enear/enear.github.com/releases{/id}","deployments_url":"https://api.github.com/repos/enear/enear.github.com/deployments","created_at":"2016-03-15T14:37:48Z","updated_at":"2016-03-15T15:05:29Z","pushed_at":"2016-03-18T13:15:12Z","git_url":"git://github.com/enear/enear.github.com.git","ssh_url":"git@github.com:enear/enear.github.com.git","clone_url":"https://github.com/enear/enear.github.com.git","svn_url":"https://github.com/enear/enear.github.com","homepage":"http://getpoole.com","size":106,"stargazers_count":0,"watchers_count":0,"language":"CSS","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":1,"mirror_url":null,"open_issues_count":1,"forks":1,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/1"},"html":{"href":"https://github.com/enear/enear.github.com/pull/1"},"issue":{"href":"https://api.github.com/repos/enear/enear.github.com/issues/1"},"comments":{"href":"https://api.github.com/repos/enear/enear.github.com/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/enear/enear.github.com/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/enear/enear.github.com/statuses/025f4fa5ad25ef754be2f0cc40a35d7935a157ce"}}}}