All Downloads are FREE. Search and download functionalities are using the official Maven repository.

playground.smithyql.parser.Parsers.scala Maven / Gradle / Ivy

The newest version!
package playground.smithyql.parser

import cats.Defer
import cats.parse.Numbers
import cats.parse.Parser
import cats.parse.Parser0
import cats.parse.Rfc5234
import cats.syntax.all.*
import playground.smithyql.*

object Parsers {

  type T[+A] = WithSource[A]

  object tokens {
    import Parser.*
    val newline: Parser[Unit] = Rfc5234.lf

    val comment: Parser[Comment] =
      (string("//") *> anyChar.repUntil0(newline | Parser.end).string.map(Comment(_)))
        .withContext("comment")

    val requiredWhitespace: Parser[Unit] = charsWhile(_.isWhitespace).void

    val whitespace: Parser0[Unit] = charsWhile0(_.isWhitespace).void

    val comments: Parser0[List[Comment]] = comment
      .repSep0(whitespace)
      .surroundedBy(whitespace)

    val pos = Parser.index.map(Position(_))

    def mergeComments[A]: (
      (
        (
          List[Comment],
          T[A],
        ),
        List[Comment],
      )
    ) => T[A] = { case ((commentsBefore, v), commentsAfter) =>
      v.withComments(commentsBefore, commentsAfter)
    }

    def mergeRange[A]: (
      (
        (
          Position,
          A,
        ),
        Position,
      )
    ) => T[A] = { case ((indexBefore, v), indexAfter) =>
      val range = SourceRange(indexBefore, indexAfter)

      WithSource.liftId(v).withRange(range)
    }

    def withComments[A](
      p: Parser[A],
      left: Boolean = true,
      right: Boolean = true,
    ): Parser[T[A]] = {

      val lhs =
        if (left)
          comments.withContext("commentsLHS")
        else
          Parser.pure(Nil)

      val rhs =
        if (right)
          comments.withContext("commentsRHS")
        else
          Parser.pure(Nil)

      (lhs.soft.with1 ~ withRange(p) ~ rhs).map(mergeComments)
    }.withContext("withComments")

    def withComments0[A](
      p: Parser0[A],
      left: Boolean = true,
      right: Boolean = true,
    ): Parser0[T[A]] = {

      val lhs =
        if (left)
          comments.withContext("commentsLHS")
        else
          Parser.pure(Nil)

      val rhs =
        if (right)
          comments.withContext("commentsRHS")
        else
          Parser.pure(Nil)

      (lhs.soft ~ withRange0(p) ~ rhs)
        .map(mergeComments)
        .withContext("withComments0")
    }

    def withRange[A](
      p: Parser[A]
    ): Parser[T[A]] = (pos.with1 ~ p ~ pos).map(mergeRange)

    def withRange0[A](
      p: Parser0[A]
    ): Parser0[T[A]] = (pos ~ p ~ pos).map(mergeRange)

    // A bit of a hack: replace the ranges of the given parser's WithSource
    // with ones containing the whole parser.
    // It's a short-term solution as the real one would involve adding new syntax nodes keeping the ranges.
    def expandRange0[S](
      p: Parser0[T[S]]
    ): Parser0[T[S]] = tokens.withRange0(p).map { forRange =>
      forRange.value.withRange(forRange.range)
    }

    val identifier: Parser[String] =
      (Rfc5234.alpha ~ Parser.charsWhile0(ch => ch.isLetterOrDigit || "_".contains(ch)))
        .map { case (ch, s) => s.prepended(ch) }
        .withContext("identifier")

    val number: Parser[String] = Numbers.jsonNumber

    val bool: Parser[Boolean] = string("true").as(true).orElse(string("false").as(false))

    // todo: allow quotes inside
    val stringLiteral: Parser[String] = anyChar
      .repUntil0(char('\"'))
      .map(_.mkString)
      .with1
      .surroundedBy(char('"'))
      .withContext("str_literal")

    val nullLiteral: Parser[Unit] = string("null")

    def punctuation(
      c: Char
    ): Parser[Unit] = char(c)

    val colon = punctuation(':')
    val equalsSign = punctuation('=')
    val dot = punctuation('.')
    val comma = punctuation(',')
    val hash = punctuation('#')
    val openBrace = punctuation('{')
    val closeBrace = punctuation('}')

    val openBracket = punctuation('[')
    val closeBracket = punctuation(']')

  }

  object parsers {

    import Parser.*

    val rawIdent: Parser[String] = tokens.identifier
    val ident: Parser[T[String]] = tokens.withComments(tokens.identifier)

    // doesn't accept comments
    val qualifiedIdent: Parser[QualifiedIdentifier] = {
      val segments =
        rawIdent.repSep(
          // soft: allows backtracking if dot isn't present (for operation names)
          tokens.whitespace.soft *> tokens.dot *> tokens.whitespace
        ) <* tokens.whitespace

      (
        // soft: allows backtracking if hash isn't present (for operation names)
        segments.soft ~ (tokens.hash *> tokens.whitespace *> rawIdent),
      ).map(QualifiedIdentifier.apply.tupled)
    }.withContext("qualified_ident")

    val useClause: Parser[UseClause[T]] = {
      string("use") *> tokens.requiredWhitespace *>
        string("service") *> tokens.requiredWhitespace *>
        tokens.withRange(qualifiedIdent)
    }.map(UseClause.apply[T]).withContext("useClause")

    val intLiteral = tokens.number.map(IntLiteral[T](_))

    val boolLiteral = tokens.bool.map(BooleanLiteral[T](_))

    val stringLiteral = tokens.stringLiteral.map(StringLiteral[T](_))
    val nullLiteral = tokens.nullLiteral.map(_ => NullLiteral[T]())

    lazy val node: Parser[InputNode[T]] = Parser
      .defer {
        intLiteral |
          boolLiteral |
          stringLiteral |
          nullLiteral |
          struct |
          listed
      }
      .withContext("node")

    lazy val struct: Parser[Struct[T]] = {
      type TField = Binding[T]

      val field: Parser[TField] =
        (
          ident.map(_.map(Identifier.apply)) <* tokens.colon.orElse(tokens.equalsSign),
          tokens.withComments(node),
        ).mapN(Binding.apply[T])

      // field, then optional whitespace, then optional coma, then optionally more `fields`
      val fields: Parser0[Struct.Fields[T]] = field
        .repSep0WithTrailing(tokens.comma)
        .map(Struct.Fields[T](_))

      tokens
        .expandRange0(tokens.withComments0(fields))
        .with1
        .between(tokens.openBrace, tokens.closeBrace)
        .map(Struct.apply[T](_))
        .withContext("struct")
    }

    // this is mostly copy-pasted from structs, might not work lmao
    lazy val listed: Parser[Listed[T]] = {
      type TField = T[InputNode[T]]

      val field: Parser[TField] = tokens.withComments(node)

      // field, then optional whitespace, then optional coma, then optionally more `fields`
      val fields: Parser0[List[TField]] = field.repSep0WithTrailing(tokens.comma)

      tokens
        .expandRange0(tokens.withComments0(fields))
        .with1
        .between(tokens.openBracket, tokens.closeBracket)
        .map(Listed.apply[T](_))
        .withContext("listed")
    }

    val queryOperationName: Parser[QueryOperationName[T]] = {

      val serviceRef = tokens.withRange(qualifiedIdent <* tokens.whitespace) <* tokens.dot

      val operationName = tokens.withRange(rawIdent).map(_.map(OperationName[WithSource](_)))

      val sr = (serviceRef <* tokens.whitespace).?

      (
        sr.with1 ~
          operationName
      ).map(QueryOperationName.apply[WithSource].tupled)
        .withContext("query_operation_name")
    }

    val query: Parser[Query[T]] =
      (
        tokens.withComments(queryOperationName, left = false),
        tokens.withComments(struct, right = false),
      ).mapN(Query.apply).withContext("query")

    val runQuery
      : Parser[RunQuery[T]] = tokens.withComments(query).map(RunQuery(_)).withContext("run_query")

    val statement: Parser[Statement[T]] = runQuery.withContext("statement")

    val prelude = tokens
      .withComments(useClause, right = false)
      .rep0
      .map(Prelude.apply)
      .withContext("prelude")

    val sourceFile: Parser0[SourceFile[T]] = (prelude, tokens.withComments0(statement.rep0))
      .mapN(SourceFile.apply[T])

    implicit class ParserOps[A](
      parser: Parser[A]
    ) {

      // like repSep0, but allows a trailing occurrence of the separator
      // if there's at least one occurrence of the main parser/
      def repSep0WithTrailing(
        sep: Parser[Unit]
      ): Parser0[List[A]] = Defer[Parser0].fix[List[A]] { self =>
        (parser ~ (sep *> self).orElse(Parser.pure(Nil)))
          .map { case (h, t) => h :: t }
          .orElse(Parser.pure(Nil))
      }

    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy