diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3596aa78..2516374c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,9 +64,9 @@ jobs: - name: Start up test databases run: docker compose up --force-recreate -d --wait --quiet-pull - - name: Check headers + - name: Check headers and formatting if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04' - run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck - name: scalaJSLink if: matrix.project == 'rootJS' diff --git a/.jvmopts b/.jvmopts index 7c70b0a5..98c291c4 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,6 +1,6 @@ -Dsbt.color=always -Dsbt.supershell=true -Xms2g --Xmx4g +-Xmx6g -Xss4m -XX:+UseG1GC diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 00000000..679b050d --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,84 @@ +version = 3.11.1 + +runner.dialect = scala213source3 + +maxColumn = 96 + +includeCurlyBraceInSelectChains = true +includeNoParensInSelectChains = true + +optIn { + breakChainOnFirstMethodDot = false + forceBlankLineBeforeDocstring = true +} + +binPack { + literalArgumentLists = true + parentConstructors = Never +} + +danglingParentheses { + defnSite = false + callSite = false + ctrlSite = false + + exclude = [] +} + +newlines { + topLevelStatementBlankLines = [ + { regex = "^Pkg", blanks { after = 1 } } + ] + beforeCurlyLambdaParams = multilineWithCaseOnly + afterCurlyLambda = squash + implicitParamListModifierPrefer = before + sometimesBeforeColonInMethodReturnType = true +} + +align.preset = none +align.stripMargin = true + +assumeStandardLibraryStripMargin = true + +docstrings { + style = Asterisk + oneline = unfold +} + +project.git = true + +trailingCommas = never + +rewrite { + rules = [PreferCurlyFors, RedundantParens, Imports] + + imports { + sort = scalastyle + selectors = fold + contiguousGroups = no + groups = [ + ["javax?\\..*"] + ["scala\\..*"] + [".*"] + ["grackle\\..*"] + ["demo\\..*"] + ] + } + + redundantBraces { + maxLines = 1 + stringInterpolation = true + } +} + +rewriteTokens { + "⇒": "=>" + "→": "->" + "←": "<-" +} + +fileOverride { + "glob:**/scala-3/**" { + runner.dialect = scala3 + } +} diff --git a/benchmarks/src/main/scala/ParserBenchmark.scala b/benchmarks/src/main/scala/ParserBenchmark.scala index 0ce8d46f..d49b000c 100644 --- a/benchmarks/src/main/scala/ParserBenchmark.scala +++ b/benchmarks/src/main/scala/ParserBenchmark.scala @@ -15,27 +15,29 @@ package grackle.benchmarks -import grackle.Schema -import org.openjdk.jmh.annotations._ -import org.openjdk.jmh.infra.Blackhole; - import java.util.concurrent.TimeUnit + import scala.io.Source +import org.openjdk.jmh.annotations._ +import org.openjdk.jmh.infra.Blackhole + +import grackle.Schema + /** * To do comparative benchmarks between versions: * - * benchmarks/run-benchmark ParserBenchmark + * benchmarks/run-benchmark ParserBenchmark * * This will generate results in `benchmarks/results`. * * Or to run the benchmark from within sbt: * - * jmh:run -i 10 -wi 10 -f 2 -t 1 grackle.benchmarks.ParserBenchmark + * jmh:run -i 10 -wi 10 -f 2 -t 1 grackle.benchmarks.ParserBenchmark * - * Which means "10 iterations", "10 warm-up iterations", "2 forks", "1 thread". - * Please note that benchmarks should be usually executed at least in - * 10 iterations (as a rule of thumb), but more is better. + * Which means "10 iterations", "10 warm-up iterations", "2 forks", "1 thread". Please note that + * benchmarks should be usually executed at least in 10 iterations (as a rule of thumb), but + * more is better. */ @State(Scope.Thread) @BenchmarkMode(Array(Mode.Throughput)) @@ -56,4 +58,3 @@ class ParserBenchmark { } } - diff --git a/build.sbt b/build.sbt index a3bfaf66..7754aee1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,49 +1,54 @@ -import nl.zolotko.sbt.jfr.{JfrRecording, JfrRecorderOptions} import scala.concurrent.duration.DurationInt import scala.sys.process._ -val catsVersion = "2.13.0" -val catsParseVersion = "1.1.0" -val catsEffectVersion = "3.7.0" -val circeVersion = "0.14.15" +import nl.zolotko.sbt.jfr.{JfrRecorderOptions, JfrRecording} +import spray.revolver.Actions._ + +val catsVersion = "2.13.0" +val catsParseVersion = "1.1.0" +val catsEffectVersion = "3.7.0" +val circeVersion = "0.14.15" val disciplineMunitVersion = "2.0.0" -val doobieVersion = "1.0.0-RC12" -val fs2Version = "3.13.0" -val http4sVersion = "0.23.34" -val kindProjectorVersion = "0.13.4" -val literallyVersion = "1.2.0" -val logbackVersion = "1.5.34" -val log4catsVersion = "2.8.0" -val mssqlDriverVersion = "13.4.0.jre11" -val munitVersion = "1.3.3" +val doobieVersion = "1.0.0-RC12" +val fs2Version = "3.13.0" +val http4sVersion = "0.23.34" +val kindProjectorVersion = "0.13.4" +val literallyVersion = "1.2.0" +val logbackVersion = "1.5.34" +val log4catsVersion = "2.8.0" +val mssqlDriverVersion = "13.4.0.jre11" +val munitVersion = "1.3.3" val munitCatsEffectVersion = "2.2.0" val munitScalaCheckVersion = "1.3.0" -val oracleDriverVersion = "23.26.2.0.0" -val skunkVersion = "1.0.0" -val shapeless2Version = "2.3.13" -val shapeless3Version = "3.6.0" -val sourcePosVersion = "1.2.0" -val typenameVersion = "1.1.2" +val oracleDriverVersion = "23.26.2.0.0" +val skunkVersion = "1.0.0" +val shapeless2Version = "2.3.13" +val shapeless3Version = "3.6.0" +val sourcePosVersion = "1.2.0" +val typenameVersion = "1.1.2" val Scala2 = "2.13.18" val Scala3 = "3.3.7" -ThisBuild / scalaVersion := Scala2 -ThisBuild / crossScalaVersions := Seq(Scala2, Scala3) -ThisBuild / tlJdkRelease := Some(11) - -ThisBuild / tlBaseVersion := "0.27" -ThisBuild / startYear := Some(2019) -ThisBuild / licenses := Seq(License.Apache2) -ThisBuild / developers := List( - Developer("milessabin", "Miles Sabin", "miles@milessabin.com", url("http://milessabin.com/blog")), - Developer("tpolecat", "Rob Norris", "rnorris@gemini.edu", url("http://www.tpolecat.org")), +ThisBuild / scalaVersion := Scala2 +ThisBuild / crossScalaVersions := Seq(Scala2, Scala3) +ThisBuild / tlJdkRelease := Some(11) + +ThisBuild / tlBaseVersion := "0.27" +ThisBuild / startYear := Some(2019) +ThisBuild / licenses := Seq(License.Apache2) +ThisBuild / developers := List( + Developer( + "milessabin", + "Miles Sabin", + "miles@milessabin.com", + url("http://milessabin.com/blog")), + Developer("tpolecat", "Rob Norris", "rnorris@gemini.edu", url("http://www.tpolecat.org")) ) -ThisBuild / tlFatalWarnings := sys.env.contains("CI") -ThisBuild / tlCiScalafmtCheck := false -ThisBuild / tlCiReleaseBranches := Seq("main") -ThisBuild / githubWorkflowBuild ~= { steps => +ThisBuild / tlFatalWarnings := sys.env.contains("CI") +ThisBuild / tlCiReleaseBranches := Seq("main") +ThisBuild / githubWorkflowBuild ~= { steps => Seq( WorkflowStep.Sbt( commands = List("headerCheckAll"), @@ -100,40 +105,42 @@ def runDocker(cmd: String): Unit = { } lazy val commonSettings = Seq( - //scalacOptions --= Seq("-Wunused:params", "-Wunused:imports", "-Wunused:patvars", "-Wdead-code", "-Wunused:locals", "-Wunused:privates", "-Wunused:implicits"), + // scalacOptions --= Seq("-Wunused:params", "-Wunused:imports", "-Wunused:patvars", "-Wdead-code", "-Wunused:locals", "-Wunused:privates", "-Wunused:implicits"), scalacOptions -= "-Wunused:privates", // Temporarily disable unused privates warning due to spurious warnings in Scala 3.3.7 scalacOptions ++= Seq("-Xlint:-pattern-shadow").filterNot(_ => tlIsScala3.value), resolvers += Resolver.sonatypeCentralSnapshots, libraryDependencies ++= Seq( - "org.scalameta" %%% "munit" % munitVersion % "test", - "org.scalameta" %%% "munit-scalacheck" % munitScalaCheckVersion % "test", - "org.typelevel" %%% "cats-laws" % catsVersion % "test", - "org.typelevel" %%% "discipline-munit" % disciplineMunitVersion % "test", + "org.scalameta" %%% "munit" % munitVersion % "test", + "org.scalameta" %%% "munit-scalacheck" % munitScalaCheckVersion % "test", + "org.typelevel" %%% "cats-laws" % catsVersion % "test", + "org.typelevel" %%% "discipline-munit" % disciplineMunitVersion % "test", "org.typelevel" %%% "munit-cats-effect" % munitCatsEffectVersion % "test", - "io.circe" %%% "circe-literal" % circeVersion % "test", - "io.circe" %%% "circe-jawn" % circeVersion % "test", - "io.circe" %%% "circe-parser" % circeVersion % "test", + "io.circe" %%% "circe-literal" % circeVersion % "test", + "io.circe" %%% "circe-jawn" % circeVersion % "test", + "io.circe" %%% "circe-parser" % circeVersion % "test" ) ++ Seq( - compilerPlugin("org.typelevel" %% "kind-projector" % kindProjectorVersion cross CrossVersion.full), + compilerPlugin( + "org.typelevel" %% "kind-projector" % kindProjectorVersion cross CrossVersion.full) ).filterNot(_ => tlIsScala3.value), headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.cppStyleLineComment), - headerLicense := Some(HeaderLicense.Custom( - """|Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA) - |Copyright (c) 2016-2025 Grackle Contributors - | - |Licensed under the Apache License, Version 2.0 (the "License"); - |you may not use this file except in compliance with the License. - |You may obtain a copy of the License at - | - | http://www.apache.org/licenses/LICENSE-2.0 - | - |Unless required by applicable law or agreed to in writing, software - |distributed under the License is distributed on an "AS IS" BASIS, - |WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - |See the License for the specific language governing permissions and - |limitations under the License. - |""".stripMargin - )) + headerLicense := Some( + HeaderLicense.Custom( + """|Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA) + |Copyright (c) 2016-2025 Grackle Contributors + | + |Licensed under the Apache License, Version 2.0 (the "License"); + |you may not use this file except in compliance with the License. + |You may obtain a copy of the License at + | + | http://www.apache.org/licenses/LICENSE-2.0 + | + |Unless required by applicable law or agreed to in writing, software + |distributed under the License is distributed on an "AS IS" BASIS, + |WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + |See the License for the specific language governing permissions and + |limitations under the License. + |""".stripMargin + )) ) lazy val nativeSettings = Seq( @@ -162,9 +169,7 @@ lazy val modules: List[CompositeProject] = List( profile ) -lazy val root = tlCrossRootProject - .aggregate(modules:_*) - .disablePlugins(RevolverPlugin) +lazy val root = tlCrossRootProject.aggregate(modules: _*).disablePlugins(RevolverPlugin) lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) @@ -176,13 +181,13 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "grackle-core", libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-parse" % catsParseVersion, - "org.typelevel" %%% "cats-core" % catsVersion, - "org.typelevel" %%% "literally" % literallyVersion, - "io.circe" %%% "circe-core" % circeVersion, - "org.tpolecat" %%% "typename" % typenameVersion, - "org.tpolecat" %%% "sourcepos" % sourcePosVersion, - "co.fs2" %%% "fs2-core" % fs2Version, + "org.typelevel" %%% "cats-parse" % catsParseVersion, + "org.typelevel" %%% "cats-core" % catsVersion, + "org.typelevel" %%% "literally" % literallyVersion, + "io.circe" %%% "circe-core" % circeVersion, + "org.tpolecat" %%% "typename" % typenameVersion, + "org.tpolecat" %%% "sourcepos" % sourcePosVersion, + "co.fs2" %%% "fs2-core" % fs2Version ) ) .jsSettings( @@ -199,7 +204,7 @@ lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn(core) .settings(commonSettings) .settings( - name := "grackle-circe", + name := "grackle-circe" ) .nativeSettings(nativeSettings) @@ -223,8 +228,8 @@ lazy val sqlcore = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "grackle-sql-core", libraryDependencies ++= Seq( - "io.circe" %%% "circe-generic" % circeVersion % "test", - "co.fs2" %%% "fs2-io" % fs2Version % "test", + "io.circe" %%% "circe-generic" % circeVersion % "test", + "co.fs2" %%% "fs2-io" % fs2Version % "test" ) ) .nativeSettings(nativeSettings) @@ -237,7 +242,7 @@ lazy val sqlpg = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn(sqlcore % "test->test;compile->compile", circe) .settings(commonSettings) .settings( - name := "grackle-sql-pg", + name := "grackle-sql-pg" ) .nativeSettings(nativeSettings) @@ -252,9 +257,9 @@ lazy val doobiecore = project Test / fork := true, Test / parallelExecution := false, libraryDependencies ++= Seq( - "org.tpolecat" %% "doobie-core" % doobieVersion, - "org.typelevel" %% "log4cats-core" % log4catsVersion, - "ch.qos.logback" % "logback-classic" % logbackVersion % "test" + "org.tpolecat" %% "doobie-core" % doobieVersion, + "org.typelevel" %% "log4cats-core" % log4catsVersion, + "ch.qos.logback" % "logback-classic" % logbackVersion % "test" ) ) @@ -262,13 +267,16 @@ lazy val doobiepg = project .in(file("modules/doobie-pg")) .enablePlugins(AutomateHeaderPlugin) .disablePlugins(RevolverPlugin) - .dependsOn(doobiecore % "test->test;compile->compile", sqlpg.jvm % "test->test;compile->compile") + .dependsOn( + doobiecore % "test->test;compile->compile", + sqlpg.jvm % "test->test;compile->compile") .settings(commonSettings) .settings( name := "grackle-doobie-pg", Test / fork := true, Test / parallelExecution := false, - Test / testOptions += Tests.Setup(_ => runDocker("docker compose up -d --wait --quiet-pull postgres")), + Test / testOptions += Tests + .Setup(_ => runDocker("docker compose up -d --wait --quiet-pull postgres")), libraryDependencies ++= Seq( "org.tpolecat" %% "doobie-postgres-circe" % doobieVersion ) @@ -284,7 +292,8 @@ lazy val doobieoracle = project name := "grackle-doobie-oracle", Test / fork := true, Test / parallelExecution := false, - Test / testOptions += Tests.Setup(_ => runDocker("docker compose up -d --wait --quiet-pull oracle")), + Test / testOptions += Tests + .Setup(_ => runDocker("docker compose up -d --wait --quiet-pull oracle")), libraryDependencies ++= Seq( "com.oracle.database.jdbc" % "ojdbc8" % oracleDriverVersion ) @@ -300,7 +309,8 @@ lazy val doobiemssql = project name := "grackle-doobie-mssql", Test / fork := true, Test / parallelExecution := false, - Test / testOptions += Tests.Setup(_ => runDocker("docker compose up -d --wait --quiet-pull mssql")), + Test / testOptions += Tests + .Setup(_ => runDocker("docker compose up -d --wait --quiet-pull mssql")), libraryDependencies ++= Seq( "com.microsoft.sqlserver" % "mssql-jdbc" % mssqlDriverVersion ) @@ -317,14 +327,15 @@ lazy val skunk = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "grackle-skunk", Test / parallelExecution := false, libraryDependencies ++= Seq( - "org.tpolecat" %%% "skunk-core" % skunkVersion, - "org.tpolecat" %%% "skunk-circe" % skunkVersion, - "org.typelevel" %% "log4cats-core" % log4catsVersion + "org.tpolecat" %%% "skunk-core" % skunkVersion, + "org.tpolecat" %%% "skunk-circe" % skunkVersion, + "org.typelevel" %% "log4cats-core" % log4catsVersion ) ) .jvmSettings( Test / fork := true, - Test / testOptions += Tests.Setup(_ => runDocker("docker compose up -d --wait --quiet-pull postgres")), + Test / testOptions += Tests.Setup(_ => + runDocker("docker compose up -d --wait --quiet-pull postgres")), libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % logbackVersion % "test" ) @@ -343,16 +354,13 @@ lazy val generic = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings(commonSettings) .settings( name := "grackle-generic", - libraryDependencies += ( - scalaVersion.value match { - case Scala3 => "org.typelevel" %%% "shapeless3-deriving" % shapeless3Version - case Scala2 => "com.chuusai" %%% "shapeless" % shapeless2Version - }) + libraryDependencies += (scalaVersion.value match { + case Scala3 => "org.typelevel" %%% "shapeless3-deriving" % shapeless3Version + case Scala2 => "com.chuusai" %%% "shapeless" % shapeless2Version + }) ) .nativeSettings(nativeSettings) -import spray.revolver.Actions._ - lazy val demo = project .in(file("demo")) .enablePlugins(NoPublishPlugin, AutomateHeaderPlugin) @@ -362,18 +370,19 @@ lazy val demo = project name := "grackle-demo", coverageEnabled := false, libraryDependencies ++= Seq( - "org.typelevel" %% "log4cats-slf4j" % log4catsVersion, - "ch.qos.logback" % "logback-classic" % logbackVersion, - "org.tpolecat" %% "doobie-core" % doobieVersion, - "org.tpolecat" %% "doobie-postgres" % doobieVersion, - "org.tpolecat" %% "doobie-hikari" % doobieVersion, - "org.http4s" %% "http4s-ember-server" % http4sVersion, - "org.http4s" %% "http4s-ember-client" % http4sVersion, - "org.http4s" %% "http4s-circe" % http4sVersion, - "org.http4s" %% "http4s-dsl" % http4sVersion + "org.typelevel" %% "log4cats-slf4j" % log4catsVersion, + "ch.qos.logback" % "logback-classic" % logbackVersion, + "org.tpolecat" %% "doobie-core" % doobieVersion, + "org.tpolecat" %% "doobie-postgres" % doobieVersion, + "org.tpolecat" %% "doobie-hikari" % doobieVersion, + "org.http4s" %% "http4s-ember-server" % http4sVersion, + "org.http4s" %% "http4s-ember-client" % http4sVersion, + "org.http4s" %% "http4s-circe" % http4sVersion, + "org.http4s" %% "http4s-dsl" % http4sVersion ), reStart := // Redefine reStart to depend on pgUp - Def.inputTask(reStart.evaluated) + Def + .inputTask(reStart.evaluated) .dependsOn(Compile / products) .dependsOn(ThisBuild / pgUp) .evaluated @@ -385,7 +394,7 @@ lazy val benchmarks = project .enablePlugins(NoPublishPlugin, AutomateHeaderPlugin, JmhPlugin) .settings(commonSettings) .settings( - coverageEnabled := false, + coverageEnabled := false ) lazy val profile = project @@ -403,11 +412,11 @@ lazy val profile = project disk = true.some, dumpOnExit = true.some, duration = 30.seconds.some, - pathToGcRoots = true.some, + pathToGcRoots = true.some ) ), fork := true, - coverageEnabled := false, + coverageEnabled := false ) lazy val docs = project @@ -419,7 +428,7 @@ lazy val docs = project libraryDependencies ++= Seq( "org.typelevel" %% "cats-effect" % catsEffectVersion ), - coverageEnabled := false, + coverageEnabled := false ) // Run repoDocs / mdoc manually to generated README.md from docs/index.md and header.md @@ -430,12 +439,12 @@ lazy val repoDocs = project .settings( mdocVariables := Map( - "VERSION" -> tlLatestVersion.value.getOrElse(version.value), + "VERSION" -> tlLatestVersion.value.getOrElse(version.value), "headerVariant" -> "repo" - ), - mdocIn := file("docs/index.md"), + ), + mdocIn := file("docs/index.md"), mdocOut := file("README.md"), - coverageEnabled := false, + coverageEnabled := false ) lazy val unidocs = project @@ -454,6 +463,6 @@ lazy val unidocs = project doobieoracle, doobiemssql, skunk.jvm, - generic.jvm, + generic.jvm ) ) diff --git a/demo/src/main/scala/demo/DemoServer.scala b/demo/src/main/scala/demo/DemoServer.scala index 0e4b7a3e..872f85ba 100644 --- a/demo/src/main/scala/demo/DemoServer.scala +++ b/demo/src/main/scala/demo/DemoServer.scala @@ -15,8 +15,7 @@ package demo -import cats.effect.IO -import cats.effect.Resource +import cats.effect.{IO, Resource} import cats.syntax.all._ import com.comcast.ip4s._ import org.http4s.{HttpApp, HttpRoutes} @@ -30,27 +29,31 @@ object DemoServer { val httpApp0 = ( // Routes for static resources, i.e. GraphQL Playground resourceServiceBuilder[IO]("/assets").toRoutes <+> - // GraphQL routes - graphQLRoutes + // GraphQL routes + graphQLRoutes ).orNotFound val httpApp = Logger.httpApp(true, false)(httpApp0) - val withErrorLogging: HttpApp[IO] = ErrorHandling.Recover.total( - ErrorAction.log( - httpApp, - messageFailureLogAction = errorHandler, - serviceErrorLogAction = errorHandler)) + val withErrorLogging: HttpApp[IO] = ErrorHandling + .Recover + .total( + ErrorAction.log( + httpApp, + messageFailureLogAction = errorHandler, + serviceErrorLogAction = errorHandler)) // Spin up the server ... - EmberServerBuilder.default[IO] + EmberServerBuilder + .default[IO] .withHost(ip"0.0.0.0") .withPort(port"8080") .withHttpApp(withErrorLogging) - .build.void + .build + .void } - def errorHandler(t: Throwable, msg: => String) : IO[Unit] = + def errorHandler(t: Throwable, msg: => String): IO[Unit] = IO.println(msg) >> IO.println(t) >> IO.println(t.printStackTrace()) } // #server diff --git a/demo/src/main/scala/demo/GraphQLService.scala b/demo/src/main/scala/demo/GraphQLService.scala index b82fa249..34464eb5 100644 --- a/demo/src/main/scala/demo/GraphQLService.scala +++ b/demo/src/main/scala/demo/GraphQLService.scala @@ -17,16 +17,17 @@ package demo import cats.effect.Concurrent import cats.syntax.all._ -import grackle.Mapping -import io.circe.{Json, ParsingFailure, parser} +import io.circe.{parser, Json, ParsingFailure} +import org.http4s.{HttpRoutes, InvalidMessageBodyFailure, ParseFailure, QueryParamDecoder} import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -import org.http4s.{HttpRoutes, InvalidMessageBodyFailure, ParseFailure, QueryParamDecoder} + +import grackle.Mapping // #service object GraphQLService { def mkRoutes[F[_]: Concurrent](prefix: String)(mapping: Mapping[F]): HttpRoutes[F] = { - val dsl = new Http4sDsl[F]{} + val dsl = new Http4sDsl[F] {} import dsl._ implicit val jsonQPDecoder: QueryParamDecoder[Json] = @@ -36,40 +37,45 @@ object GraphQLService { } } - object QueryMatcher - extends QueryParamDecoderMatcher[String]("query") + object QueryMatcher extends QueryParamDecoderMatcher[String]("query") object OperationNameMatcher - extends OptionalQueryParamDecoderMatcher[String]("operationName") + extends OptionalQueryParamDecoderMatcher[String]("operationName") object VariablesMatcher - extends OptionalValidatingQueryParamDecoderMatcher[Json]("variables") + extends OptionalValidatingQueryParamDecoderMatcher[Json]("variables") HttpRoutes.of[F] { // GraphQL query is embedded in the URI query string when queried via GET - case GET -> Root / `prefix` :? - QueryMatcher(query) +& OperationNameMatcher(op) +& VariablesMatcher(vars0) => - vars0.sequence.fold( - errors => BadRequest(errors.map(_.sanitized).mkString_("", ",", "")), - vars => - for { - result <- mapping.compileAndRun(query, op, vars) - resp <- Ok(result) - } yield resp - ) + case GET -> Root / `prefix` :? + QueryMatcher(query) +& OperationNameMatcher(op) +& VariablesMatcher(vars0) => + vars0 + .sequence + .fold( + errors => BadRequest(errors.map(_.sanitized).mkString_("", ",", "")), + vars => + for { + result <- mapping.compileAndRun(query, op, vars) + resp <- Ok(result) + } yield resp + ) // GraphQL query is embedded in a Json request body when queried via POST case req @ POST -> Root / `prefix` => for { - body <- req.as[Json] - obj <- body.asObject.liftTo[F]( - InvalidMessageBodyFailure("Invalid GraphQL query") - ) - query <- obj("query").flatMap(_.asString).liftTo[F]( - InvalidMessageBodyFailure("Missing query field") - ) - op = obj("operationName").flatMap(_.asString) - vars = obj("variables") + body <- req.as[Json] + obj <- body + .asObject + .liftTo[F]( + InvalidMessageBodyFailure("Invalid GraphQL query") + ) + query <- obj("query") + .flatMap(_.asString) + .liftTo[F]( + InvalidMessageBodyFailure("Missing query field") + ) + op = obj("operationName").flatMap(_.asString) + vars = obj("variables") result <- mapping.compileAndRun(query, op, vars) - resp <- Ok(result) + resp <- Ok(result) } yield resp } } diff --git a/demo/src/main/scala/demo/Main.scala b/demo/src/main/scala/demo/Main.scala index 7c9d875b..408637a9 100644 --- a/demo/src/main/scala/demo/Main.scala +++ b/demo/src/main/scala/demo/Main.scala @@ -17,19 +17,19 @@ package demo import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all._ + +import demo.DemoServer.mkServer +import demo.GraphQLService.mkRoutes import demo.starwars.StarWarsMapping import demo.world.WorldMapping -import GraphQLService.mkRoutes -import DemoServer.mkServer - // #main object Main extends IOApp { def run(args: List[String]): IO[ExitCode] = { (for { starWarsRoutes <- StarWarsMapping[IO].map(mkRoutes("starwars")) - worldRoutes <- WorldMapping[IO].map(mkRoutes("world")) - _ <- mkServer(starWarsRoutes <+> worldRoutes) + worldRoutes <- WorldMapping[IO].map(mkRoutes("world")) + _ <- mkServer(starWarsRoutes <+> worldRoutes) } yield ()).useForever } } diff --git a/demo/src/main/scala/demo/starwars/StarWarsMapping.scala b/demo/src/main/scala/demo/starwars/StarWarsMapping.scala index b4f8bea0..a11f329b 100644 --- a/demo/src/main/scala/demo/starwars/StarWarsMapping.scala +++ b/demo/src/main/scala/demo/starwars/StarWarsMapping.scala @@ -16,13 +16,14 @@ package demo.starwars import cats.MonadThrow -import cats.syntax.all._ import cats.effect.Resource +import cats.syntax.all._ + import grackle.Predicate._ import grackle.Query._ import grackle.QueryCompiler._ +import grackle.Result import grackle.Value._ -import grackle._ import grackle.generic._ import grackle.syntax._ @@ -90,11 +91,11 @@ trait StarWarsMapping[F[_]] extends GenericMapping[F] { self: StarWarsData[F] => case (QueryType, "hero", List(Binding("episode", EnumValue(e)))) => for { ep <- Elab.liftR( - Episode.values.find(_.toString == e).toResult(s"Unknown episode '$e'") - ) - _ <- Elab.transformChild { child => - Unique(Filter(Eql(CharacterType / "id", Const(hero(ep).id)), child)) - } + Episode.values.find(_.toString == e).toResult(s"Unknown episode '$e'") + ) + _ <- Elab.transformChild { child => + Unique(Filter(Eql(CharacterType / "id", Const(hero(ep).id)), child)) + } } yield () // The character, human and droid selectors all take a single ID argument and yield a @@ -135,40 +136,38 @@ trait StarWarsData[F[_]] extends GenericMapping[F] { self: StarWarsMapping[F] => } case class Human( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - homePlanet: Option[String] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + homePlanet: Option[String] ) extends Character object Human { implicit val cursorBuilder: CursorBuilder[Human] = - deriveObjectCursorBuilder[Human](HumanType) - .transformField("friends")(resolveFriends) + deriveObjectCursorBuilder[Human](HumanType).transformField("friends")(resolveFriends) } case class Droid( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - primaryFunction: Option[String] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + primaryFunction: Option[String] ) extends Character object Droid { implicit val cursorBuilder: CursorBuilder[Droid] = - deriveObjectCursorBuilder[Droid](DroidType) - .transformField("friends")(resolveFriends) + deriveObjectCursorBuilder[Droid](DroidType).transformField("friends")(resolveFriends) } def resolveFriends(c: Character): Result[Option[List[Character]]] = c.friends match { case None => None.success case Some(ids) => - ids.traverse(id => - characters.find(_.id == id).toResultOrError(s"Bad id '$id'") - ).map(_.some) + ids + .traverse(id => characters.find(_.id == id).toResultOrError(s"Bad id '$id'")) + .map(_.some) } // #model_types @@ -228,15 +227,13 @@ trait StarWarsData[F[_]] extends GenericMapping[F] { self: StarWarsMapping[F] => ) // #model_values - import Episode._ - - val Some(lukeSkywalker) = characters.find(_.id == "1000") : @unchecked - val Some(r2d2) = characters.find(_.id == "2001") : @unchecked + val Some(lukeSkywalker) = characters.find(_.id == "1000"): @unchecked + val Some(r2d2) = characters.find(_.id == "2001"): @unchecked // Mapping from Episode to its hero Character - val hero: Map[Value, Character] = Map( - NEWHOPE -> r2d2, - EMPIRE -> lukeSkywalker, - JEDI -> r2d2 + val hero: Map[Episode.Value, Character] = Map( + Episode.NEWHOPE -> r2d2, + Episode.EMPIRE -> lukeSkywalker, + Episode.JEDI -> r2d2 ) } diff --git a/demo/src/main/scala/demo/world/WorldData.scala b/demo/src/main/scala/demo/world/WorldData.scala index 94cfee05..8cbcccf4 100644 --- a/demo/src/main/scala/demo/world/WorldData.scala +++ b/demo/src/main/scala/demo/world/WorldData.scala @@ -23,7 +23,8 @@ import cats.effect.{Async, Resource} import doobie.hikari.HikariTransactor object WorldData { - def mkTransactor[F[_]: Async](connInfo: PostgresConnectionInfo): Resource[F, HikariTransactor[F]] = { + def mkTransactor[F[_]: Async]( + connInfo: PostgresConnectionInfo): Resource[F, HikariTransactor[F]] = { import connInfo._ HikariTransactor.newHikariTransactor[F]( driverClassName, diff --git a/demo/src/main/scala/demo/world/WorldMapping.scala b/demo/src/main/scala/demo/world/WorldMapping.scala index 14550f3e..ec153d29 100644 --- a/demo/src/main/scala/demo/world/WorldMapping.scala +++ b/demo/src/main/scala/demo/world/WorldMapping.scala @@ -17,46 +17,47 @@ package demo.world import cats.effect.{Async, Resource, Sync} import doobie.{Meta, Transactor} +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger + +import grackle._ import grackle.Predicate._ import grackle.Query._ import grackle.QueryCompiler._ import grackle.Value._ -import grackle._ import grackle.doobie.{DoobieMonitor, LoggedDoobieMappingCompanion} -import grackle.doobie.postgres.{DoobiePgMapping} +import grackle.doobie.postgres.DoobiePgMapping import grackle.sql.Like import grackle.syntax._ -import org.typelevel.log4cats.Logger -import org.typelevel.log4cats.slf4j.Slf4jLogger -import WorldData._ +import demo.world.WorldData._ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { // #db_tables object country extends TableDef("country") { - val code = col("code", Meta[String]) - val name = col("name", Meta[String]) - val continent = col("continent", Meta[String]) - val region = col("region", Meta[String]) - val surfacearea = col("surfacearea", Meta[Float]) - val indepyear = col("indepyear", Meta[Int], nullable = true) - val population = col("population", Meta[Int]) + val code = col("code", Meta[String]) + val name = col("name", Meta[String]) + val continent = col("continent", Meta[String]) + val region = col("region", Meta[String]) + val surfacearea = col("surfacearea", Meta[Float]) + val indepyear = col("indepyear", Meta[Int], nullable = true) + val population = col("population", Meta[Int]) val lifeexpectancy = col("lifeexpectancy", Meta[Float], nullable = true) - val gnp = col("gnp", Meta[BigDecimal], nullable = true) - val gnpold = col("gnpold", Meta[BigDecimal], nullable = true) - val localname = col("localname", Meta[String]) + val gnp = col("gnp", Meta[BigDecimal], nullable = true) + val gnpold = col("gnpold", Meta[BigDecimal], nullable = true) + val localname = col("localname", Meta[String]) val governmentform = col("governmentform", Meta[String]) - val headofstate = col("headofstate", Meta[String], nullable = true) - val capitalId = col("capital", Meta[Int], nullable = true) - val numCities = col("num_cities", Meta[Long]) + val headofstate = col("headofstate", Meta[String], nullable = true) + val capitalId = col("capital", Meta[Int], nullable = true) + val numCities = col("num_cities", Meta[Long]) } object city extends TableDef("city") { - val id = col("id", Meta[Int]) + val id = col("id", Meta[Int]) val countrycode = col("countrycode", Meta[String]) - val name = col("name", Meta[String]) - val district = col("district", Meta[String]) - val population = col("population", Meta[Int]) + val name = col("name", Meta[String]) + val district = col("district", Meta[String]) + val population = col("population", Meta[Int]) } object countrylanguage extends TableDef("countrylanguage") { @@ -112,9 +113,9 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { """ // #schema - val QueryType = schema.ref("Query") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val QueryType = schema.ref("Query") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") val LanguageType = schema.ref("Language") val typeMappings = @@ -129,23 +130,23 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { // #root // #type_mappings ObjectMapping(CountryType)( - SqlField("code", country.code, key = true), - SqlField("name", country.name), - SqlField("continent", country.continent), - SqlField("region", country.region), - SqlField("surfacearea", country.surfacearea), - SqlField("indepyear", country.indepyear), - SqlField("population", country.population), + SqlField("code", country.code, key = true), + SqlField("name", country.name), + SqlField("continent", country.continent), + SqlField("region", country.region), + SqlField("surfacearea", country.surfacearea), + SqlField("indepyear", country.indepyear), + SqlField("population", country.population), SqlField("lifeexpectancy", country.lifeexpectancy), - SqlField("gnp", country.gnp), - SqlField("gnpold", country.gnpold), - SqlField("localname", country.localname), + SqlField("gnp", country.gnp), + SqlField("gnpold", country.gnpold), + SqlField("localname", country.localname), SqlField("governmentform", country.governmentform), - SqlField("headofstate", country.headofstate), - SqlField("capitalId", country.capitalId), - SqlField("numCities", country.numCities), - SqlObject("cities", Join(country.code, city.countrycode)), - SqlObject("languages", Join(country.code, countrylanguage.countrycode)) + SqlField("headofstate", country.headofstate), + SqlField("capitalId", country.capitalId), + SqlField("numCities", country.numCities), + SqlObject("cities", Join(country.code, city.countrycode)), + SqlObject("languages", Join(country.code, countrylanguage.countrycode)) ), ObjectMapping(CityType)( SqlField("id", city.id, key = true), @@ -153,7 +154,7 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { SqlField("name", city.name), SqlField("district", city.district), SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlObject("country", Join(city.countrycode, country.code)) ), ObjectMapping(LanguageType)( SqlField("name", countrylanguage.language, key = true, associative = true), @@ -170,14 +171,16 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { Unique(Filter(Eql(CountryType / "code", Const(code)), child)) } - case (QueryType, "countries", - List( - Binding("maxPopulation", IntValue(max)), - Binding("sortByPopulation", BooleanValue(sortByPop)), - Binding("offset", IntValue(off)), - Binding("limit", IntValue(lim)) - ) - ) => + case ( + QueryType, + "countries", + List( + Binding("maxPopulation", IntValue(max)), + Binding("sortByPopulation", BooleanValue(sortByPop)), + Binding("offset", IntValue(off)), + Binding("limit", IntValue(lim)) + ) + ) => def filter(query: Query): Query = if (max < 0) query else Filter(LtEql(CountryType / "population", Const(max)), query) @@ -212,15 +215,13 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { } case (CountryType, "numCities", List(Binding("namePattern", AbsentValue))) => - Elab.transformChild { _ => - Count(Select("cities", Select("name"))) - } + Elab.transformChild { _ => Count(Select("cities", Select("name"))) } - case (CountryType, "numCities", - List(Binding("namePattern", StringValue(namePattern)))) => + case (CountryType, "numCities", List(Binding("namePattern", StringValue(namePattern)))) => Elab.transformChild { _ => Count( - Select("cities", + Select( + "cities", Filter(Like(CityType / "name", namePattern, true), Select("name")) ) ) @@ -230,7 +231,9 @@ trait WorldMapping[F[_]] extends DoobiePgMapping[F] { } object WorldMapping extends LoggedDoobieMappingCompanion { - def mkMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F]): WorldMapping[F] = + def mkMapping[F[_]: Sync]( + transactor: Transactor[F], + monitor: DoobieMonitor[F]): WorldMapping[F] = new DoobiePgMapping(transactor, monitor) with WorldMapping[F] def mkMappingFromTransactor[F[_]: Sync](transactor: Transactor[F]): WorldMapping[F] = { diff --git a/modules/circe/src/main/scala/circemapping.scala b/modules/circe/src/main/scala/circemapping.scala index b02840c0..0346350e 100644 --- a/modules/circe/src/main/scala/circemapping.scala +++ b/modules/circe/src/main/scala/circemapping.scala @@ -13,55 +13,68 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circe +package grackle.circe import scala.collection.Factory import cats.MonadThrow import cats.implicits._ import fs2.Stream -import io.circe.Json -import io.circe.Encoder +import io.circe.{Encoder, Json} import org.tpolecat.sourcepos.SourcePos -import syntax._ -import Cursor.DeferredCursor -import ScalarType._ +import grackle._ +import grackle.Cursor.DeferredCursor +import grackle.ScalarType._ +import grackle.syntax._ -abstract class CirceMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with CirceMappingLike[F] +abstract class CirceMapping[F[_]](implicit val M: MonadThrow[F]) + extends Mapping[F] + with CirceMappingLike[F] trait CirceMappingLike[F[_]] extends Mapping[F] { // Syntax to allow Circe-specific root effects implicit class CirceMappingRootEffectSyntax(self: RootEffect.type) { - def computeJson(fieldName: String)(effect: (Path, Env) => F[Result[Json]])(implicit pos: SourcePos): RootEffect = + def computeJson(fieldName: String)(effect: (Path, Env) => F[Result[Json]])( + implicit pos: SourcePos): RootEffect = self.computeCursor(fieldName)((p, e) => effect(p, e).map(_.map(circeCursor(p, e, _)))) - def computeEncodable[A](fieldName: String)(effect: (Path, Env) => F[Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootEffect = + def computeEncodable[A](fieldName: String)(effect: (Path, Env) => F[Result[A]])( + implicit pos: SourcePos, + enc: Encoder[A]): RootEffect = computeJson(fieldName)((p, e) => effect(p, e).map(_.map(enc(_)))) } implicit class CirceMappingRootStreamSyntax(self: RootStream.type) { - def computeJson(fieldName: String)(effect: (Path, Env) => Stream[F, Result[Json]])(implicit pos: SourcePos): RootStream = + def computeJson(fieldName: String)(effect: (Path, Env) => Stream[F, Result[Json]])( + implicit pos: SourcePos): RootStream = self.computeCursor(fieldName)((p, e) => effect(p, e).map(_.map(circeCursor(p, e, _)))) - def computeEncodable[A](fieldName: String)(effect: (Path, Env) => Stream[F, Result[A]])(implicit pos: SourcePos, enc: Encoder[A]): RootStream = + def computeEncodable[A](fieldName: String)(effect: (Path, Env) => Stream[F, Result[A]])( + implicit pos: SourcePos, + enc: Encoder[A]): RootStream = computeJson(fieldName)((p, e) => effect(p, e).map(_.map(enc(_)))) } def circeCursor(path: Path, env: Env, value: Json): Cursor = - if(path.isRoot) + if (path.isRoot) CirceCursor(Context(path.rootTpe), value, None, env) else - DeferredCursor(path, (context, parent) => CirceCursor(context, value, Some(parent), env).success) - - override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] = + DeferredCursor( + path, + (context, parent) => CirceCursor(context, value, Some(parent), env).success) + + override def mkCursorForMappedField( + parent: Cursor, + fieldContext: Context, + fm: FieldMapping): Result[Cursor] = (fm, parent.focus) match { case (CirceField(_, json, _), _) => CirceCursor(fieldContext, json, Some(parent), parent.env).success case (CursorFieldJson(_, f, _, _), _) => - f(parent).map(res => CirceCursor(fieldContext, focus = res, parent = Some(parent), env = parent.env)) + f(parent).map(res => + CirceCursor(fieldContext, focus = res, parent = Some(parent), env = parent.env)) case _ => super.mkCursorForMappedField(parent, fieldContext, fm) } @@ -70,17 +83,23 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { def subtree: Boolean = true } - case class CirceField(fieldName: String, value: Json, hidden: Boolean = false)(implicit val pos: SourcePos) extends CirceFieldMapping + case class CirceField(fieldName: String, value: Json, hidden: Boolean = false)( + implicit val pos: SourcePos) + extends CirceFieldMapping - case class CursorFieldJson(fieldName: String, f: Cursor => Result[Json], required: List[String], hidden: Boolean = false)( - implicit val pos: SourcePos + case class CursorFieldJson( + fieldName: String, + f: Cursor => Result[Json], + required: List[String], + hidden: Boolean = false)( + implicit val pos: SourcePos ) extends CirceFieldMapping case class CirceCursor( - context: Context, - focus: Json, - parent: Option[Cursor], - env: Env + context: Context, + focus: Json, + parent: Option[Cursor], + env: Env ) extends Cursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) @@ -90,31 +109,35 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { def isLeaf: Boolean = tpe.dealias match { case BooleanType => focus.isBoolean - case StringType|IDType => focus.isString - case IntType|FloatType => focus.isNumber + case StringType | IDType => focus.isString + case IntType | FloatType => focus.isNumber case _: EnumType => focus.isString case _ => false } def asLeaf: Result[Json] = tpe.dealias match { - case BooleanType if focus.isBoolean => focus.success - case StringType|IDType if focus.isString => focus.success - case IntType if focus.isNumber => - focus.asNumber.flatMap(_.toLong.map(Json.fromLong)) + case BooleanType if focus.isBoolean => focus.success + case StringType | IDType if focus.isString => focus.success + case IntType if focus.isNumber => + focus + .asNumber + .flatMap(_.toLong.map(Json.fromLong)) .toResultOrError(s"Expected Int found ${focus.noSpaces}") - case FloatType if focus.isNumber => focus.success - case e: EnumType if focus.isString => + case FloatType if focus.isNumber => focus.success + case e: EnumType if focus.isString => if (focus.asString.exists(e.hasValue)) focus.success else Result.internalError(s"Expected Enum ${e.name}, found ${focus.noSpaces}") - case _: ScalarType if !focus.isObject => focus.success // custom Scalar; any non-object type is fine + case _: ScalarType if !focus.isObject => + focus.success // custom Scalar; any non-object type is fine case _ => - Result.internalError(s"Expected Scalar type, found $tpe for focus ${focus.noSpaces} at ${context.path.reverse.mkString("/")} ") + Result.internalError( + s"Expected Scalar type, found $tpe for focus ${focus.noSpaces} at ${context.path.reverse.mkString("/")} ") } def preunique: Result[Cursor] = { val listTpe = tpe.nonNull.list - if(focus.isArray) + if (focus.isArray) mkChild(context.asType(listTpe), focus).success else Result.internalError(s"Expected List type, found $focus for ${listTpe}") @@ -124,7 +147,9 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { def asList[C](factory: Factory[Cursor, C]): Result[C] = tpe match { case ListType(elemTpe) if focus.isArray => - focus.asArray.map(_.view.map(e => mkChild(context.asType(elemTpe), e)).to(factory)) + focus + .asArray + .map(_.view.map(e => mkChild(context.asType(elemTpe), e)).to(factory)) .toResultOrError(s"Expected List type, found $tpe for focus ${focus.noSpaces}") case _ => Result.internalError(s"Expected List type, found $tpe for focus ${focus.noSpaces}") @@ -132,7 +157,9 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { def listSize: Result[Int] = tpe match { case ListType(_) if focus.isArray => - focus.asArray.map(_.size) + focus + .asArray + .map(_.size) .toResultOrError(s"Expected List type, found $tpe for focus ${focus.noSpaces}") case _ => Result.internalError(s"Expected List type, found $tpe for focus ${focus.noSpaces}") @@ -156,9 +183,9 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { (subtpe <:< tpe && ((subtpe.dealias, focus.asObject) match { case (nt: TypeWithFields, Some(obj)) => - nt.fields.forall { f => - f.tpe.isNullable || obj.contains(f.name) - } && obj.keys.forall(nt.hasField) + nt.fields.forall { f => f.tpe.isNullable || obj.contains(f.name) } && obj + .keys + .forall(nt.hasField) case _ => false })).success @@ -168,14 +195,15 @@ trait CirceMappingLike[F[_]] extends Mapping[F] { if (n) mkChild(context.asType(subtpe)).success else - Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") + Result.internalError( + s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") } def field(fieldName: String, resultName: Option[String]): Result[Cursor] = { val localField = for { obj <- focus.asObject - f <- obj(fieldName) + f <- obj(fieldName) } yield mkChild(context.forFieldOrAttribute(fieldName, resultName), f) localField.map(_.success).getOrElse(mkCursorForField(this, fieldName, resultName)) diff --git a/modules/circe/src/test/scala/CirceData.scala b/modules/circe/src/test/scala/CirceData.scala index 5e1190ee..2ff09e28 100644 --- a/modules/circe/src/test/scala/CirceData.scala +++ b/modules/circe/src/test/scala/CirceData.scala @@ -13,20 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circetests +package grackle.circetests -import cats.effect.IO import cats.data.OptionT +import cats.effect.IO import io.circe.Json import io.circe.literal._ +import grackle._ +import grackle.Query._ +import grackle.QueryCompiler._ import grackle.circe.CirceMapping import grackle.syntax._ -import Query._ -import QueryCompiler._ - object TestCirceMapping extends CirceMapping[IO] { val schema = schema""" @@ -103,17 +102,15 @@ object TestCirceMapping extends CirceMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - CirceField("root", data), - ) + fieldMappings = List( + CirceField("root", data) + ) ), ObjectMapping( tpe = RootType, - fieldMappings = - List( - CursorField("computed", computeField, List("hidden")) - ) + fieldMappings = List( + CursorField("computed", computeField, List("hidden")) + ) ), LeafMapping[BigDecimal](BigDecimalType) ) @@ -122,7 +119,7 @@ object TestCirceMapping extends CirceMapping[IO] { (for { n <- OptionT(c.fieldAs[Json]("hidden").map(_.asNumber)) i <- OptionT(Result(n.toInt)) - } yield i+1).value + } yield i + 1).value } override val selectElaborator = SelectElaborator { diff --git a/modules/circe/src/test/scala/CirceEffectData.scala b/modules/circe/src/test/scala/CirceEffectData.scala index 991f039a..302244ea 100644 --- a/modules/circe/src/test/scala/CirceEffectData.scala +++ b/modules/circe/src/test/scala/CirceEffectData.scala @@ -13,14 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circetests +package grackle.circetests import cats.effect.Sync import cats.implicits._ import fs2.concurrent.SignallingRef import io.circe.{Encoder, Json} +import grackle._ import grackle.circe.CirceMapping import grackle.syntax._ @@ -52,53 +52,62 @@ class TestCirceEffectMapping[F[_]: Sync](ref: SignallingRef[F, Int]) extends Cir val typeMappings = List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( + fieldMappings = List( + // Compute a CirceCursor + RootEffect.computeCursor("foo")((p, e) => + ref + .update(_ + 1) + .as( + Result( + circeCursor( + p, + e, + Json.obj( + "n" -> Json.fromInt(42), + "s" -> Json.fromString("hi") + ) + )) + )), - // Compute a CirceCursor - RootEffect.computeCursor("foo")((p, e) => - ref.update(_+1).as( - Result(circeCursor(p, e, + // Compute a Json, let the implementation handle the cursor + RootEffect.computeJson("bar")((_, _) => + ref + .update(_ + 1) + .as( + Result( Json.obj( "n" -> Json.fromInt(42), - "s" -> Json.fromString("hi") - ) - )) - ) - ), + "s" -> Json.fromString("ho") + )) + )), - // Compute a Json, let the implementation handle the cursor - RootEffect.computeJson("bar")((_, _) => - ref.update(_+1).as( - Result(Json.obj( - "n" -> Json.fromInt(42), - "s" -> Json.fromString("ho") - )) - ) - ), - - // Compute an encodable value, let the implementation handle json and the cursor - RootEffect.computeEncodable("baz")((_, _) => - ref.update(_+1).as( + // Compute an encodable value, let the implementation handle json and the cursor + RootEffect.computeEncodable("baz")((_, _) => + ref + .update(_ + 1) + .as( Result(Struct(44, "hee")) - ) - ), + )), - // Compute a CirceCursor focussed on the root - RootEffect.computeCursor("qux")((p, e) => - ref.update(_+1).as( - Result(circeCursor(Path.from(p.rootTpe), e, - Json.obj( - "qux" -> - Json.obj( - "n" -> Json.fromInt(42), - "s" -> Json.fromString("hi") - ) - ) - )) - ) - ) - ) + // Compute a CirceCursor focussed on the root + RootEffect.computeCursor("qux")((p, e) => + ref + .update(_ + 1) + .as( + Result( + circeCursor( + Path.from(p.rootTpe), + e, + Json.obj( + "qux" -> + Json.obj( + "n" -> Json.fromInt(42), + "s" -> Json.fromString("hi") + ) + ) + )) + )) + ) ) ) } diff --git a/modules/circe/src/test/scala/CirceEffectSuite.scala b/modules/circe/src/test/scala/CirceEffectSuite.scala index 54c9c323..5a25eb24 100644 --- a/modules/circe/src/test/scala/CirceEffectSuite.scala +++ b/modules/circe/src/test/scala/CirceEffectSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circetests +package grackle.circetests import cats.effect.IO import fs2.concurrent.SignallingRef @@ -46,10 +45,10 @@ final class CirceEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new TestCirceEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new TestCirceEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -78,10 +77,10 @@ final class CirceEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new TestCirceEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new TestCirceEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -110,10 +109,10 @@ final class CirceEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new TestCirceEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new TestCirceEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -142,10 +141,10 @@ final class CirceEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new TestCirceEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new TestCirceEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -190,10 +189,10 @@ final class CirceEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new TestCirceEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new TestCirceEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 3)) diff --git a/modules/circe/src/test/scala/CircePrioritySuite.scala b/modules/circe/src/test/scala/CircePrioritySuite.scala index 07cd204a..efd37d39 100644 --- a/modules/circe/src/test/scala/CircePrioritySuite.scala +++ b/modules/circe/src/test/scala/CircePrioritySuite.scala @@ -13,15 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circetests +package grackle.circetests import cats.effect.IO import io.circe.Json import io.circe.literal._ +import munit.CatsEffectSuite + import grackle.circe.CirceMapping import grackle.syntax._ -import munit.CatsEffectSuite object CircePriorityMapping extends CirceMapping[IO] { @@ -39,30 +39,28 @@ object CircePriorityMapping extends CirceMapping[IO] { } """ - val QueryType = schema.ref("Query") + val QueryType = schema.ref("Query") val MonkeyType = schema.ref("Monkey") val BarrelType = schema.ref("Barrel") - val FooType = schema.ref("Foo") + val FooType = schema.ref("Foo") val typeMappings = List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - CirceField("present", json"""{ "monkey": { "name": "Bob" }}"""), - CirceField("fallback", json"""{ "monkey": {}}"""), - ) + fieldMappings = List( + CirceField("present", json"""{ "monkey": { "name": "Bob" }}"""), + CirceField("fallback", json"""{ "monkey": {}}""") + ) ), ObjectMapping( tpe = MonkeyType, - fieldMappings = - List( - CirceField("name", Json.fromString("Steve")) - ) + fieldMappings = List( + CirceField("name", Json.fromString("Steve")) + ) ), LeafMapping[String](FooType) - ) + ) } diff --git a/modules/circe/src/test/scala/CirceSuite.scala b/modules/circe/src/test/scala/CirceSuite.scala index 2fc236a7..3b681702 100644 --- a/modules/circe/src/test/scala/CirceSuite.scala +++ b/modules/circe/src/test/scala/CirceSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package circetests +package grackle.circetests import io.circe.literal._ import munit.CatsEffectSuite diff --git a/modules/core/src/main/scala-2/syntax2.scala b/modules/core/src/main/scala-2/syntax2.scala index d0edcd8f..715f2021 100644 --- a/modules/core/src/main/scala-2/syntax2.scala +++ b/modules/core/src/main/scala-2/syntax2.scala @@ -18,6 +18,7 @@ package grackle import cats.data.NonEmptyChain import cats.syntax.all._ import org.typelevel.literally.Literally + import grackle.Ast.Document import grackle.Schema @@ -32,25 +33,37 @@ class StringContextOps(val sc: StringContext) extends AnyVal { } private object SchemaLiteral extends Literally[Schema] { - def validate(c: Context)(s: String): Either[String,c.Expr[Schema]] = { + def validate(c: Context)(s: String): Either[String, c.Expr[Schema]] = { import c.universe._ def mkError(err: Either[Throwable, NonEmptyChain[Problem]]) = err.fold( - t => s"Internal error: ${t.getMessage}", - ps => s"Invalid schema: ${ps.toList.distinct.mkString("\n 🐞 ", "\n 🐞 ", "\n")}", + t => s"Internal error: ${t.getMessage}", + ps => s"Invalid schema: ${ps.toList.distinct.mkString("\n 🐞 ", "\n 🐞 ", "\n")}" ) - Schema(s, CompiletimeParsers.schemaParser).toEither.bimap(mkError, _ => c.Expr(q"_root_.grackle.Schema($s, _root_.grackle.CompiletimeParsers.schemaParser).toOption.get")) + Schema(s, CompiletimeParsers.schemaParser) + .toEither + .bimap( + mkError, + _ => + c.Expr( + q"_root_.grackle.Schema($s, _root_.grackle.CompiletimeParsers.schemaParser).toOption.get")) } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Schema] = apply(c)(args: _*) } private object DocumentLiteral extends Literally[Document] { - def validate(c: Context)(s: String): Either[String,c.Expr[Document]] = { + def validate(c: Context)(s: String): Either[String, c.Expr[Document]] = { import c.universe._ - CompiletimeParsers.parser.parseText(s).toEither.bimap( - _.fold(thr => show"Invalid document: ${thr.getMessage}", _.toList.mkString("\n 🐞 ", "\n 🐞 ", "\n")), - _ => c.Expr(q"_root_.grackle.CompiletimeParsers.parser.parseText($s).toOption.get"), - ) + CompiletimeParsers + .parser + .parseText(s) + .toEither + .bimap( + _.fold( + thr => show"Invalid document: ${thr.getMessage}", + _.toList.mkString("\n 🐞 ", "\n 🐞 ", "\n")), + _ => c.Expr(q"_root_.grackle.CompiletimeParsers.parser.parseText($s).toOption.get") + ) } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Document] = apply(c)(args: _*) } diff --git a/modules/core/src/main/scala-3/syntax3.scala b/modules/core/src/main/scala-3/syntax3.scala index f2e09a10..0104b2b4 100644 --- a/modules/core/src/main/scala-3/syntax3.scala +++ b/modules/core/src/main/scala-3/syntax3.scala @@ -17,27 +17,36 @@ package grackle import cats.syntax.all._ import org.typelevel.literally.Literally + import grackle.Ast.Document trait VersionSpecificSyntax: extension (inline ctx: StringContext) - inline def schema(inline args: Any*): Schema = ${SchemaLiteral('ctx, 'args)} - inline def doc(inline args: Any*): Document = ${DocumentLiteral('ctx, 'args) } + inline def schema(inline args: Any*): Schema = ${ SchemaLiteral('ctx, 'args) } + inline def doc(inline args: Any*): Document = ${ DocumentLiteral('ctx, 'args) } object SchemaLiteral extends Literally[Schema]: def validate(s: String)(using Quotes) = - Schema(s, CompiletimeParsers.schemaParser).toEither.bimap( - nec => s"Invalid schema:${nec.toList.distinct.mkString("\n 🐞 ", "\n 🐞 ", "\n")}", - _ => '{Schema(${Expr(s)}, CompiletimeParsers.schemaParser).toOption.get} - ) + Schema(s, CompiletimeParsers.schemaParser) + .toEither + .bimap( + nec => s"Invalid schema:${nec.toList.distinct.mkString("\n 🐞 ", "\n 🐞 ", "\n")}", + _ => '{ Schema(${ Expr(s) }, CompiletimeParsers.schemaParser).toOption.get } + ) object DocumentLiteral extends Literally[Document]: def validate(s: String)(using Quotes) = - CompiletimeParsers.parser.parseText(s).toEither.bimap( - _.fold(thr => show"Invalid document: ${thr.getMessage}", _.toList.mkString("\n 🐞 ", "\n 🐞 ", "\n")), - _ => '{CompiletimeParsers.parser.parseText(${Expr(s)}).toOption.get} - ) + CompiletimeParsers + .parser + .parseText(s) + .toEither + .bimap( + _.fold( + thr => show"Invalid document: ${thr.getMessage}", + _.toList.mkString("\n 🐞 ", "\n 🐞 ", "\n")), + _ => '{ CompiletimeParsers.parser.parseText(${ Expr(s) }).toOption.get } + ) object CompiletimeParsers: val parser: GraphQLParser = GraphQLParser(GraphQLParser.defaultConfig) diff --git a/modules/core/src/main/scala/ast.scala b/modules/core/src/main/scala/ast.scala index cf71b8b5..c0491e8b 100644 --- a/modules/core/src/main/scala/ast.scala +++ b/modules/core/src/main/scala/ast.scala @@ -31,8 +31,8 @@ object Ast { sealed abstract class OperationType(val name: String) object OperationType { - case object Query extends OperationType("query") - case object Mutation extends OperationType("mutation") + case object Query extends OperationType("query") + case object Mutation extends OperationType("mutation") case object Subscription extends OperationType("subscription") } @@ -40,15 +40,15 @@ object Ast { object OperationDefinition { case class QueryShorthand( - selectionSet: List[Selection] + selectionSet: List[Selection] ) extends OperationDefinition case class Operation( - operationType: OperationType, - name: Option[Name], - variables: List[VariableDefinition], - directives: List[Directive], - selectionSet: List[Selection] + operationType: OperationType, + name: Option[Name], + variables: List[VariableDefinition], + directives: List[Directive], + selectionSet: List[Selection] ) extends OperationDefinition } @@ -61,74 +61,74 @@ object Ast { object Selection { case class Field( - alias: Option[Name], - name: Name, - arguments: List[(Name, Value)], - directives: List[Directive], - selectionSet: List[Selection] + alias: Option[Name], + name: Name, + arguments: List[(Name, Value)], + directives: List[Directive], + selectionSet: List[Selection] ) extends Selection case class FragmentSpread( - name: Name, - directives: List[Directive] + name: Name, + directives: List[Directive] ) extends Selection case class InlineFragment( - typeCondition: Option[Type], - directives: List[Directive], - selectionSet: List[Selection] + typeCondition: Option[Type], + directives: List[Directive], + selectionSet: List[Selection] ) extends Selection } case class FragmentDefinition( - name: Name, - typeCondition: Type, - directives: List[Directive], - selectionSet: List[Selection] + name: Name, + typeCondition: Type, + directives: List[Directive], + selectionSet: List[Selection] ) extends ExecutableDefinition case class VariableDefinition( - name: Name, - tpe: Type, - defaultValue: Option[Value], - directives: List[Directive] - ) + name: Name, + tpe: Type, + defaultValue: Option[Value], + directives: List[Directive] + ) sealed trait Value object Value { - case class Variable(name: Name) extends Value - case class IntValue(value: Int) extends Value - case class FloatValue(value: Double) extends Value - case class StringValue(value: String) extends Value - case class BooleanValue(value: Boolean) extends Value - case object NullValue extends Value - case class EnumValue(name: Name) extends Value - case class ListValue(values: List[Value]) extends Value - case class ObjectValue(fields: List[(Name, Value)]) extends Value + case class Variable(name: Name) extends Value + case class IntValue(value: Int) extends Value + case class FloatValue(value: Double) extends Value + case class StringValue(value: String) extends Value + case class BooleanValue(value: Boolean) extends Value + case object NullValue extends Value + case class EnumValue(name: Name) extends Value + case class ListValue(values: List[Value]) extends Value + case class ObjectValue(fields: List[(Name, Value)]) extends Value } sealed abstract class Type(val name: String) object Type { - case class Named(astName: Name) extends Type(astName.value) - case class List(ofType: Type) extends Type(s"[${ofType.name}]") + case class Named(astName: Name) extends Type(astName.value) + case class List(ofType: Type) extends Type(s"[${ofType.name}]") case class NonNull(of: Either[Named, List]) extends Type(s"${of.merge.name}!") } case class SchemaDefinition( - rootOperationTypes: List[RootOperationTypeDefinition], - directives: List[Directive] + rootOperationTypes: List[RootOperationTypeDefinition], + directives: List[Directive] ) extends TypeSystemDefinition case class SchemaExtension( - rootOperationTypes: List[RootOperationTypeDefinition], - directives: List[Directive] + rootOperationTypes: List[RootOperationTypeDefinition], + directives: List[Directive] ) extends TypeSystemExtension case class RootOperationTypeDefinition( - operationType: OperationType, - tpe: Type.Named, - directives: List[Directive] + operationType: OperationType, + tpe: Type.Named, + directives: List[Directive] ) sealed trait TypeDefinition extends TypeSystemDefinition with Product with Serializable { @@ -138,136 +138,136 @@ object Ast { } case class ScalarTypeDefinition( - name: Name, - description: Option[String], - directives: List[Directive] + name: Name, + description: Option[String], + directives: List[Directive] ) extends TypeDefinition case class ObjectTypeDefinition( - name: Name, - description: Option[String], - fields: List[FieldDefinition], - interfaces: List[Type.Named], - directives: List[Directive] + name: Name, + description: Option[String], + fields: List[FieldDefinition], + interfaces: List[Type.Named], + directives: List[Directive] ) extends TypeDefinition case class InterfaceTypeDefinition( - name: Name, - description: Option[String], - fields: List[FieldDefinition], - interfaces: List[Type.Named], - directives: List[Directive] + name: Name, + description: Option[String], + fields: List[FieldDefinition], + interfaces: List[Type.Named], + directives: List[Directive] ) extends TypeDefinition case class UnionTypeDefinition( - name: Name, - description: Option[String], - directives: List[Directive], - members: List[Type.Named] + name: Name, + description: Option[String], + directives: List[Directive], + members: List[Type.Named] ) extends TypeDefinition case class EnumTypeDefinition( - name: Name, - description: Option[String], - directives: List[Directive], - values: List[EnumValueDefinition] + name: Name, + description: Option[String], + directives: List[Directive], + values: List[EnumValueDefinition] ) extends TypeDefinition case class FieldDefinition( - name: Name, - description: Option[String], - args: List[InputValueDefinition], - tpe: Type, - directives: List[Directive] + name: Name, + description: Option[String], + args: List[InputValueDefinition], + tpe: Type, + directives: List[Directive] ) case class EnumValueDefinition( - name: Name, - description: Option[String], - directives: List[Directive], + name: Name, + description: Option[String], + directives: List[Directive] ) case class InputValueDefinition( - name: Name, - description: Option[String], - tpe: Type, - defaultValue: Option[Value], - directives: List[Directive] + name: Name, + description: Option[String], + tpe: Type, + defaultValue: Option[Value], + directives: List[Directive] ) case class InputObjectTypeDefinition( - name: Name, - description: Option[String], - fields: List[InputValueDefinition], - directives: List[Directive] + name: Name, + description: Option[String], + fields: List[InputValueDefinition], + directives: List[Directive] ) extends TypeDefinition case class DirectiveDefinition( - name: Name, - description: Option[String], - args: List[InputValueDefinition], - repeatable: Boolean, - locations: List[DirectiveLocation] + name: Name, + description: Option[String], + args: List[InputValueDefinition], + repeatable: Boolean, + locations: List[DirectiveLocation] ) extends TypeSystemDefinition case class ScalarTypeExtension( - baseType: Type.Named, - directives: List[Directive] + baseType: Type.Named, + directives: List[Directive] ) extends TypeExtension case class ObjectTypeExtension( - baseType: Type.Named, - fields: List[FieldDefinition], - interfaces: List[Type.Named], - directives: List[Directive] + baseType: Type.Named, + fields: List[FieldDefinition], + interfaces: List[Type.Named], + directives: List[Directive] ) extends TypeExtension case class InterfaceTypeExtension( - baseType: Type.Named, - fields: List[FieldDefinition], - interfaces: List[Type.Named], - directives: List[Directive] + baseType: Type.Named, + fields: List[FieldDefinition], + interfaces: List[Type.Named], + directives: List[Directive] ) extends TypeExtension case class UnionTypeExtension( - baseType: Type.Named, - directives: List[Directive], - members: List[Type.Named] + baseType: Type.Named, + directives: List[Directive], + members: List[Type.Named] ) extends TypeExtension case class EnumTypeExtension( - baseType: Type.Named, - directives: List[Directive], - values: List[EnumValueDefinition] + baseType: Type.Named, + directives: List[Directive], + values: List[EnumValueDefinition] ) extends TypeExtension case class InputObjectTypeExtension( - baseType: Type.Named, - directives: List[Directive], - fields: List[InputValueDefinition], + baseType: Type.Named, + directives: List[Directive], + fields: List[InputValueDefinition] ) extends TypeExtension sealed trait DirectiveLocation object DirectiveLocation { - case object QUERY extends DirectiveLocation - case object MUTATION extends DirectiveLocation - case object SUBSCRIPTION extends DirectiveLocation - case object FIELD extends DirectiveLocation - case object FRAGMENT_DEFINITION extends DirectiveLocation - case object FRAGMENT_SPREAD extends DirectiveLocation - case object INLINE_FRAGMENT extends DirectiveLocation - case object VARIABLE_DEFINITION extends DirectiveLocation - - case object SCHEMA extends DirectiveLocation - case object SCALAR extends DirectiveLocation - case object OBJECT extends DirectiveLocation - case object FIELD_DEFINITION extends DirectiveLocation - case object ARGUMENT_DEFINITION extends DirectiveLocation - case object INTERFACE extends DirectiveLocation - case object UNION extends DirectiveLocation - case object ENUM extends DirectiveLocation - case object ENUM_VALUE extends DirectiveLocation - case object INPUT_OBJECT extends DirectiveLocation + case object QUERY extends DirectiveLocation + case object MUTATION extends DirectiveLocation + case object SUBSCRIPTION extends DirectiveLocation + case object FIELD extends DirectiveLocation + case object FRAGMENT_DEFINITION extends DirectiveLocation + case object FRAGMENT_SPREAD extends DirectiveLocation + case object INLINE_FRAGMENT extends DirectiveLocation + case object VARIABLE_DEFINITION extends DirectiveLocation + + case object SCHEMA extends DirectiveLocation + case object SCALAR extends DirectiveLocation + case object OBJECT extends DirectiveLocation + case object FIELD_DEFINITION extends DirectiveLocation + case object ARGUMENT_DEFINITION extends DirectiveLocation + case object INTERFACE extends DirectiveLocation + case object UNION extends DirectiveLocation + case object ENUM extends DirectiveLocation + case object ENUM_VALUE extends DirectiveLocation + case object INPUT_OBJECT extends DirectiveLocation case object INPUT_FIELD_DEFINITION extends DirectiveLocation } } diff --git a/modules/core/src/main/scala/compiler.scala b/modules/core/src/main/scala/compiler.scala index 95607b4e..e86f00f1 100644 --- a/modules/core/src/main/scala/compiler.scala +++ b/modules/core/src/main/scala/compiler.scala @@ -21,28 +21,32 @@ import scala.reflect.ClassTag import cats.data.StateT import cats.implicits._ import io.circe.Json -import org.tpolecat.typename.{ TypeName, typeName } +import org.tpolecat.typename.{typeName, TypeName} -import syntax._ -import Query._, Predicate._, Value._, UntypedOperation._ -import QueryCompiler._ -import ScalarType._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.ScalarType._ +import grackle.UntypedOperation._ +import grackle.Value._ +import grackle.syntax._ /** * GraphQL query parser */ trait QueryParser { + /** - * Parse a String to query algebra operations and fragments. + * Parse a String to query algebra operations and fragments. * - * GraphQL errors and warnings are accumulated in the result. + * GraphQL errors and warnings are accumulated in the result. */ def parseText(text: String): Result[(List[UntypedOperation], List[UntypedFragment])] /** - * Parse a document AST to query algebra operations and fragments. + * Parse a document AST to query algebra operations and fragments. * - * GraphQL errors and warnings are accumulated in the result. + * GraphQL errors and warnings are accumulated in the result. */ def parseDocument(doc: Ast.Document): Result[(List[UntypedOperation], List[UntypedFragment])] } @@ -52,54 +56,57 @@ object QueryParser { new Impl(parser) private final class Impl(parser: GraphQLParser) extends QueryParser { - import Ast.{ Directive => _, Type => _, Value => _, _ }, OperationDefinition._, Selection._ + import Ast.{Directive => _, Type => _, Value => _, _} + import OperationDefinition._ + import Selection._ /** - * Parse a String to query algebra operations and fragments. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse a String to query algebra operations and fragments. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseText(text: String): Result[(List[UntypedOperation], List[UntypedFragment])] = for { doc <- parser.parseText(text) res <- parseDocument(doc) - _ <- Result.failure("At least one operation required").whenA(res._1.isEmpty) + _ <- Result.failure("At least one operation required").whenA(res._1.isEmpty) } yield res /** - * Parse a document AST to query algebra operations and fragments. - * - * GraphQL errors and warnings are accumulated in the result. - */ - def parseDocument(doc: Document): Result[(List[UntypedOperation], List[UntypedFragment])] = { + * Parse a document AST to query algebra operations and fragments. + * + * GraphQL errors and warnings are accumulated in the result. + */ + def parseDocument( + doc: Document): Result[(List[UntypedOperation], List[UntypedFragment])] = { val ops0 = doc.collect { case op: OperationDefinition => op } val fragments0 = doc.collect { case frag: FragmentDefinition => frag } for { - ops <- ops0.traverse { - case op: Operation => parseOperation(op) - case qs: QueryShorthand => parseQueryShorthand(qs) - } - frags <- fragments0.traverse { frag => - val tpnme = frag.typeCondition.name - for { - sels <- parseSelections(frag.selectionSet) - dirs <- parseDirectives(frag.directives) - } yield UntypedFragment(frag.name.value, tpnme, dirs, sels) - } + ops <- ops0.traverse { + case op: Operation => parseOperation(op) + case qs: QueryShorthand => parseQueryShorthand(qs) + } + frags <- fragments0.traverse { frag => + val tpnme = frag.typeCondition.name + for { + sels <- parseSelections(frag.selectionSet) + dirs <- parseDirectives(frag.directives) + } yield UntypedFragment(frag.name.value, tpnme, dirs, sels) + } } yield (ops, frags) } /** - * Parse an operation AST to a query algebra operation. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse an operation AST to a query algebra operation. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseOperation(op: Operation): Result[UntypedOperation] = { val Operation(opType, name, vds, dirs0, sels) = op for { - vs <- parseVariableDefinitions(vds) - q <- parseSelections(sels) + vs <- parseVariableDefinitions(vds) + q <- parseSelections(sels) dirs <- parseDirectives(dirs0) } yield { val name0 = name.map(_.value) @@ -112,48 +119,48 @@ object QueryParser { } /** - * Parse variable definition ASTs to query algebra variable definitions. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse variable definition ASTs to query algebra variable definitions. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseVariableDefinitions(vds: List[VariableDefinition]): Result[List[UntypedVarDef]] = vds.traverse { case VariableDefinition(Name(nme), tpe, dv0, dirs0) => for { - dv <- dv0.traverse(Value.fromAst) + dv <- dv0.traverse(Value.fromAst) dirs <- parseDirectives(dirs0) } yield UntypedVarDef(nme, tpe, dv, dirs) } /** - * Parse a query shorthand AST to query algebra operation. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse a query shorthand AST to query algebra operation. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseQueryShorthand(qs: QueryShorthand): Result[UntypedOperation] = parseSelections(qs.selectionSet).map(q => UntypedQuery(None, q, Nil, Nil)) /** - * Parse selection ASTs to query algebra terms. - * - * GraphQL errors and warnings are accumulated in the result - */ + * Parse selection ASTs to query algebra terms. + * + * GraphQL errors and warnings are accumulated in the result + */ def parseSelections(sels: List[Selection]): Result[Query] = sels.traverse(parseSelection).map { sels0 => if (sels0.sizeCompare(1) == 0) sels0.head else Group(sels0) } /** - * Parse a selection AST to a query algebra term. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse a selection AST to a query algebra term. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseSelection(sel: Selection): Result[Query] = sel match { case Field(alias, name, args, directives, sels) => for { args0 <- parseArgs(args) sels0 <- parseSelections(sels) - dirs <- parseDirectives(directives) + dirs <- parseDirectives(directives) } yield { val nme = name.value val alias0 = alias.map(_.value).flatMap(n => if (n == nme) None else Some(n)) @@ -168,32 +175,32 @@ object QueryParser { case InlineFragment(typeCondition, directives, sels) => for { - dirs <- parseDirectives(directives) + dirs <- parseDirectives(directives) sels0 <- parseSelections(sels) } yield UntypedInlineFragment(typeCondition.map(_.name), dirs, sels0) } /** - * Parse directive ASTs to query algebra directives. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse directive ASTs to query algebra directives. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseDirectives(directives: List[Ast.Directive]): Result[List[Directive]] = directives.traverse(Directive.fromAst) /** - * Parse argument ASTs to query algebra bindings. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse argument ASTs to query algebra bindings. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseArgs(args: List[(Name, Ast.Value)]): Result[List[Binding]] = args.traverse((parseArg _).tupled) /** - * Parse an argument AST to a query algebra binding. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Parse an argument AST to a query algebra binding. + * + * GraphQL errors and warnings are accumulated in the result. + */ def parseArg(name: Name, value: Ast.Value): Result[Binding] = Value.fromAst(value).map(v => Binding(name.value, v)) } @@ -202,87 +209,102 @@ object QueryParser { /** * GraphQL query compiler. * - * A QueryCompiler parses GraphQL queries to query algebra terms, then - * applies a collection of transformation phases in sequence, yielding a - * query algebra term which can be directly interpreted. + * A QueryCompiler parses GraphQL queries to query algebra terms, then applies a collection of + * transformation phases in sequence, yielding a query algebra term which can be directly + * interpreted. */ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { import IntrospectionLevel._ /** - * Compiles the GraphQL query `text` to a query algebra term which - * can be directly executed. + * Compiles the GraphQL query `text` to a query algebra term which can be directly executed. * * GraphQL errors and warnings are accumulated in the result. */ - def compile(text: String, name: Option[String] = None, untypedVars: Option[Json] = None, introspectionLevel: IntrospectionLevel = Full, reportUnused: Boolean = true, env: Env = Env.empty): Result[Operation] = - parser.parseText(text).flatMap { case (ops, frags) => - for { - _ <- Result.fromProblems(validateVariablesAndFragments(ops, frags, reportUnused)) - _ <- Result.fromProblems(validateFieldMergeability(ops, frags)) - ops0 <- ops.traverse(op => compileOperation(op, untypedVars, frags, introspectionLevel, env).map(op0 => (op.name, op0))) - res <- (ops0, name) match { - case (List((_, op)), None) => - op.success - case (Nil, _) => - Result.failure("At least one operation required") - case (_, None) => - Result.failure("Operation name required to select unique operation") - case (ops, _) if ops.lengthCompare(1) > 0 && ops.exists(_._1.isEmpty) => - Result.failure("Query shorthand cannot be combined with multiple operations") - case (ops, on@Some(name)) => - ops.filter(_._1 == on) match { - case List((_, op)) => - op.success - case Nil => - Result.failure(s"No operation named '$name'") - case _ => - Result.failure(s"Multiple operations named '$name'") - } - } - } yield res + def compile( + text: String, + name: Option[String] = None, + untypedVars: Option[Json] = None, + introspectionLevel: IntrospectionLevel = Full, + reportUnused: Boolean = true, + env: Env = Env.empty): Result[Operation] = + parser.parseText(text).flatMap { + case (ops, frags) => + for { + _ <- Result.fromProblems(validateVariablesAndFragments(ops, frags, reportUnused)) + _ <- Result.fromProblems(validateFieldMergeability(ops, frags)) + ops0 <- ops.traverse(op => + compileOperation(op, untypedVars, frags, introspectionLevel, env) + .map(op0 => (op.name, op0))) + res <- (ops0, name) match { + case (List((_, op)), None) => + op.success + case (Nil, _) => + Result.failure("At least one operation required") + case (_, None) => + Result.failure("Operation name required to select unique operation") + case (ops, _) if ops.lengthCompare(1) > 0 && ops.exists(_._1.isEmpty) => + Result.failure("Query shorthand cannot be combined with multiple operations") + case (ops, on @ Some(name)) => + ops.filter(_._1 == on) match { + case List((_, op)) => + op.success + case Nil => + Result.failure(s"No operation named '$name'") + case _ => + Result.failure(s"Multiple operations named '$name'") + } + } + } yield res } /** - * Compiles the provided operation AST to a query algebra term - * which can be directly executed. - * - * GraphQL errors and warnings are accumulated in the result. - */ - def compileOperation(op: UntypedOperation, untypedVars: Option[Json], frags: List[UntypedFragment], introspectionLevel: IntrospectionLevel = Full, env: Env = Env.empty): Result[Operation] = { + * Compiles the provided operation AST to a query algebra term which can be directly executed. + * + * GraphQL errors and warnings are accumulated in the result. + */ + def compileOperation( + op: UntypedOperation, + untypedVars: Option[Json], + frags: List[UntypedFragment], + introspectionLevel: IntrospectionLevel = Full, + env: Env = Env.empty): Result[Operation] = { val allPhases = - IntrospectionElaborator(introspectionLevel).toList ++ (VariablesSkipAndFragmentElaborator :: MergeFields :: phases) + IntrospectionElaborator( + introspectionLevel).toList ++ (VariablesSkipAndFragmentElaborator :: MergeFields :: phases) for { varDefs <- compileVarDefs(op.variables) - vars <- compileVars(varDefs, untypedVars) - _ <- Directive.validateDirectivesForQuery(schema, op, frags, vars) + vars <- compileVars(varDefs, untypedVars) + _ <- Directive.validateDirectivesForQuery(schema, op, frags, vars) rootTpe <- op.rootTpe(schema) - res <- ( - for { - query <- allPhases.foldLeftM(op.query) { (acc, phase) => phase.transformFragments *> phase.transform(acc) } - } yield Operation(query, rootTpe, op.directives) - ).runA( - ElabState( - None, - schema, - Context(rootTpe), - vars, - frags.map(f => (f.name, f)).toMap, - op.query, - env, - List.empty, - Elab.pure - ) - ) + res <- ( + for { + query <- allPhases.foldLeftM(op.query) { (acc, phase) => + phase.transformFragments *> phase.transform(acc) + } + } yield Operation(query, rootTpe, op.directives) + ).runA( + ElabState( + None, + schema, + Context(rootTpe), + vars, + frags.map(f => (f.name, f)).toMap, + op.query, + env, + List.empty, + Elab.pure + ) + ) } yield res } /** - * Compiles variable definition ASTs to variable definitions for the target schema. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Compiles variable definition ASTs to variable definitions for the target schema. + * + * GraphQL errors and warnings are accumulated in the result. + */ def compileVarDefs(untypedVarDefs: UntypedVarDefs): Result[VarDefs] = untypedVarDefs.traverse { case UntypedVarDef(name, untypedTpe, default, dirs) => @@ -290,10 +312,10 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { } /** - * Compiles raw query variables to variables for the target schema. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Compiles raw query variables to variables for the target schema. + * + * GraphQL errors and warnings are accumulated in the result. + */ def compileVars(varDefs: VarDefs, untypedVars: Option[Json]): Result[Vars] = untypedVars match { case None => Map.empty.success @@ -302,70 +324,94 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { case None => Result.failure(s"Variables must be represented as a Json object") case Some(obj) => - varDefs.traverse(iv => checkVarValue(iv, obj(iv.name), "variable values").map(v => (iv.name, (iv.tpe, v)))).map(_.toMap) + varDefs + .traverse(iv => + checkVarValue(iv, obj(iv.name), "variable values").map(v => + (iv.name, (iv.tpe, v)))) + .map(_.toMap) } } /** - * Compiles a type AST to a type in the target schema. - * - * GraphQL errors and warnings are accumulated in the result. - */ + * Compiles a type AST to a type in the target schema. + * + * GraphQL errors and warnings are accumulated in the result. + */ def compileType(tpe: Ast.Type): Result[Type] = { def loop(tpe: Ast.Type, nonNull: Boolean): Result[Type] = tpe match { case Ast.Type.NonNull(Left(named)) => loop(named, true) case Ast.Type.NonNull(Right(list)) => loop(list, true) - case Ast.Type.List(elem) => loop(elem, false).map(e => if (nonNull) ListType(e) else NullableType(ListType(e))) - case Ast.Type.Named(name) => schema.definition(name.value) match { - case None => Result.failure(s"Undefined type '${name.value}'") - case Some(tpe) => (if (nonNull) tpe else NullableType(tpe)).success - } + case Ast.Type.List(elem) => + loop(elem, false).map(e => if (nonNull) ListType(e) else NullableType(ListType(e))) + case Ast.Type.Named(name) => + schema.definition(name.value) match { + case None => Result.failure(s"Undefined type '${name.value}'") + case Some(tpe) => (if (nonNull) tpe else NullableType(tpe)).success + } } loop(tpe, false) } - def validateVariablesAndFragments(ops: List[UntypedOperation], frags: List[UntypedFragment], reportUnused: Boolean): List[Problem] = { - val (uniqueFrags, duplicateFrags) = frags.map(_.name).foldLeft((Set.empty[String], Set.empty[String])) { - case ((unique, duplicate), nme) => - if (unique.contains(nme)) (unique, duplicate + nme) - else (unique + nme, duplicate) - } + def validateVariablesAndFragments( + ops: List[UntypedOperation], + frags: List[UntypedFragment], + reportUnused: Boolean): List[Problem] = { + val (uniqueFrags, duplicateFrags) = + frags.map(_.name).foldLeft((Set.empty[String], Set.empty[String])) { + case ((unique, duplicate), nme) => + if (unique.contains(nme)) (unique, duplicate + nme) + else (unique + nme, duplicate) + } if (duplicateFrags.nonEmpty) duplicateFrags.toList.map(nme => Problem(s"Fragment '$nme' is defined more than once")) else { def collectQueryRefs(query: Query): (Set[String], Set[String]) = { @tailrec - def loop(queries: Iterator[Query], vars: Set[String], frags: Set[String]): (Set[String], Set[String]) = + def loop( + queries: Iterator[Query], + vars: Set[String], + frags: Set[String]): (Set[String], Set[String]) = if (!queries.hasNext) (vars, frags) else queries.next() match { case UntypedSelect(_, _, args, dirs, child) => val v0 = args.iterator.flatMap(arg => collectValueRefs(arg.value)).toSet - val v1 = dirs.iterator.flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))).toSet + val v1 = dirs + .iterator + .flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))) + .toSet loop(Iterator.single(child) ++ queries, vars ++ v0 ++ v1, frags) case UntypedFragmentSpread(nme, dirs) => - val v0 = dirs.iterator.flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))).toSet + val v0 = dirs + .iterator + .flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))) + .toSet loop(queries, vars ++ v0, frags + nme) case UntypedInlineFragment(_, dirs, child) => - val v0 = dirs.iterator.flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))).toSet + val v0 = dirs + .iterator + .flatMap(dir => dir.args.iterator.flatMap(arg => collectValueRefs(arg.value))) + .toSet loop(Iterator.single(child) ++ queries, vars ++ v0, frags) case Group(children) => loop(children.iterator ++ queries, vars, frags) - case Select(_, _, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Narrow(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Unique(child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Filter(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Limit(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Offset(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case OrderBy(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Introspect(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Environment(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Component(_, _, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Effect(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case TransformCursor(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) - case Count(_) => loop(queries, vars, frags) - case Empty => loop(queries, vars, frags) + case Select(_, _, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Narrow(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Unique(child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Filter(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Limit(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Offset(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case OrderBy(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Introspect(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Environment(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case Component(_, _, child) => + loop(Iterator.single(child) ++ queries, vars, frags) + case Effect(_, child) => loop(Iterator.single(child) ++ queries, vars, frags) + case TransformCursor(_, child) => + loop(Iterator.single(child) ++ queries, vars, frags) + case Count(_) => loop(queries, vars, frags) + case Empty => loop(queries, vars, frags) } loop(Iterator.single(query), Set.empty[String], Set.empty[String]) @@ -390,9 +436,7 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { } val fragRefs: Map[String, (Set[String], Set[String])] = - frags.map { frag => - (frag.name, collectQueryRefs(frag.child)) - }.toMap + frags.map { frag => (frag.name, collectQueryRefs(frag.child)) }.toMap @tailrec def checkCycle(pendingFrags: Set[String], seen: Set[String]): Option[Set[String]] = { @@ -400,14 +444,17 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { else { val hd = pendingFrags.head if (seen.contains(hd)) None - else checkCycle(fragRefs.get(hd).map(_._2).getOrElse(Set.empty) ++ pendingFrags.tail, seen + hd) + else + checkCycle( + fragRefs.get(hd).map(_._2).getOrElse(Set.empty) ++ pendingFrags.tail, + seen + hd) } } def findCycle: Option[String] = { @tailrec def loop(pendingFrags: Set[String]): Either[Set[String], String] = { - if(pendingFrags.isEmpty) Left(Set.empty[String]) + if (pendingFrags.isEmpty) Left(Set.empty[String]) else { val hd = pendingFrags.head checkCycle(Set(hd), Set.empty[String]) match { @@ -424,12 +471,17 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { findCycle match { case Some(from) => List(Problem(s"Fragment cycle starting from '$from'")) case _ => - def validateOp(op: UntypedOperation, pendingFrags: Set[String]): (List[Problem], Set[String]) = { + def validateOp( + op: UntypedOperation, + pendingFrags: Set[String]): (List[Problem], Set[String]) = { val pendingVars = op.variables.map(_.name).toSet val (dqv, dqf) = collectQueryRefs(op.query) @tailrec - def closeRefs(pendingFrags: List[String], seenVars: Set[String], seenFrags: Set[String]): (Set[String], Set[String]) = + def closeRefs( + pendingFrags: List[String], + seenVars: Set[String], + seenFrags: Set[String]): (Set[String], Set[String]) = pendingFrags match { case Nil => (seenVars, seenFrags) case hd :: tl => @@ -450,11 +502,17 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { if (qv == pendingVars) Nil else { val undefinedProblems = - qv.diff(pendingVars).toList.map(nme => Problem(s"Variable '$nme' is undefined")) + qv.diff(pendingVars) + .toList + .map(nme => Problem(s"Variable '$nme' is undefined")) val unusedProblems = if (!reportUnused) Nil - else pendingVars.diff(qv).toList.map(nme => Problem(s"Variable '$nme' is unused")) + else + pendingVars + .diff(qv) + .toList + .map(nme => Problem(s"Variable '$nme' is unused")) undefinedProblems ++ unusedProblems } @@ -463,7 +521,8 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { if (qf.subsetOf(uniqueFrags)) Nil else { val undefined = qf.diff(uniqueFrags) - val undefinedProblems = undefined.toList.map(nme => Problem(s"Fragment '$nme' is undefined")) + val undefinedProblems = + undefined.toList.map(nme => Problem(s"Fragment '$nme' is undefined")) undefinedProblems } @@ -475,7 +534,7 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { case ((acc, pendingFrags), op) => val (problems, pendingFrags0) = validateOp(op, pendingFrags) (acc ++ problems, pendingFrags0) - } + } val unreferencedFragProblems = if (!reportUnused) Nil @@ -487,15 +546,19 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { } /** - * Validates that field mergeability rules are satisfied for the supplied operations and fragments. - * - * Returns a list of problems encountered. - */ - def validateFieldMergeability(ops: List[UntypedOperation], frags: List[UntypedFragment]): List[Problem] = { + * Validates that field mergeability rules are satisfied for the supplied operations and + * fragments. + * + * Returns a list of problems encountered. + */ + def validateFieldMergeability( + ops: List[UntypedOperation], + frags: List[UntypedFragment]): List[Problem] = { // Validates field mergeability for a single operation def validateOp(op: UntypedOperation): List[Problem] = { // Collects all top level selects of the supplied queries, ungrouping and inlining fragments where necessary - def collectSelects(queries: List[(NamedType, Query)]): List[(NamedType, UntypedSelect)] = { + def collectSelects( + queries: List[(NamedType, Query)]): List[(NamedType, UntypedSelect)] = { queries.flatMap { case (tpe, g: Group) => collectSelects(g.queries.map(q => (tpe, q))) case (tpe, s: UntypedSelect) => List((tpe, s)) @@ -516,24 +579,32 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { } // Checks that the supplied field types are compatible - def checkShapes(tpes: List[Type], resultName: String): Either[List[Problem], List[NamedType]] = { - if(tpes.sizeCompare(1) <= 0) Right(tpes.map(_.underlyingNamed)) + def checkShapes( + tpes: List[Type], + resultName: String): Either[List[Problem], List[NamedType]] = { + if (tpes.sizeCompare(1) <= 0) Right(tpes.map(_.underlyingNamed)) else { def stripNull(tpes: List[Type]): Either[List[Problem], List[NamedType]] = { if (tpes.forall(!_.isNullable)) stripList(tpes) - else if(tpes.forall(_.isNullable)) stripList(tpes.map(_.nonNull)) - else Left(List(Problem(s"Cannot merge fields named '$resultName' of both nullable and non-nullable types"))) + else if (tpes.forall(_.isNullable)) stripList(tpes.map(_.nonNull)) + else + Left(List(Problem( + s"Cannot merge fields named '$resultName' of both nullable and non-nullable types"))) } def stripList(tpes: List[Type]): Either[List[Problem], List[NamedType]] = { if (tpes.forall(!_.isList)) Right(tpes.map(_.underlyingNamed)) - else if(tpes.forall(_.isList)) stripNull(tpes.collect { case ListType(elem) => elem }) - else Left(List(Problem(s"Cannot merge fields named '$resultName' of both list and non-list types"))) + else if (tpes.forall(_.isList)) + stripNull(tpes.collect { case ListType(elem) => elem }) + else + Left( + List(Problem( + s"Cannot merge fields named '$resultName' of both list and non-list types"))) } stripNull(tpes) match { - case l@Left(_) => l - case r@Right(tpes) => + case l @ Left(_) => l + case r @ Right(tpes) => if (tpes.forall(!_.isLeaf)) r else { val first = tpes.head @@ -544,10 +615,14 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { val nonLeafNames = nonLeaf0.map(_.name).distinct val leafErrors = if (leafNames.sizeCompare(1) <= 0) Nil - else List(Problem(s"Cannot merge fields named '$resultName' of distinct leaf types ${leafNames.mkString(", ")}")) + else + List(Problem( + s"Cannot merge fields named '$resultName' of distinct leaf types ${leafNames.mkString(", ")}")) val nonLeafErrors = if (nonLeafNames.isEmpty) Nil - else List(Problem(s"Cannot merge fields named '$resultName' of leaf types ${leafNames.mkString(", ")} and non-leaf types ${nonLeafNames.mkString(", ")}")) + else + List(Problem( + s"Cannot merge fields named '$resultName' of leaf types ${leafNames.mkString(", ")} and non-leaf types ${nonLeafNames.mkString(", ")}")) Left(leafErrors ::: nonLeafErrors) } } @@ -568,7 +643,9 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { else { val allTypes = sels.map(_._1.dealias).distinct allTypes.flatMap { tpe => - val conflictSet = sels.collect { case (ntpe, sel) if ntpe <:< tpe || tpe <:< ntpe => sel } + val conflictSet = sels.collect { + case (ntpe, sel) if ntpe <:< tpe || tpe <:< ntpe => sel + } if (conflictSet.sizeCompare(1) <= 0) Nil else { val first = conflictSet.head @@ -578,10 +655,14 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { else { val nameProblems = if (noNameConflicts) Nil - else List(Problem(s"Cannot merge fields with alias '$resultName' and names ${conflictSet.map(s => s"'${s.name}'").distinct.mkString(", ")}")) + else + List(Problem( + s"Cannot merge fields with alias '$resultName' and names ${conflictSet.map(s => s"'${s.name}'").distinct.mkString(", ")}")) val argProblems = if (noArgConflicts) Nil - else List(Problem(s"Cannot merge fields named '$resultName' with different arguments")) + else + List(Problem( + s"Cannot merge fields named '$resultName' with different arguments")) nameProblems ::: argProblems } } @@ -594,11 +675,11 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { grouped.flatMap { case (resultName, sels) => val children = sels.flatMap { - case ((tpe, q)) => + case (tpe, q) => q.name match { case "__typename" => Nil - case "__type" => List((Introspection.__TypeType, q.child)) - case "__schema" => List((Introspection.__SchemaType, q.child)) + case "__type" => List((Introspection.__TypeType, q.child)) + case "__schema" => List((Introspection.__SchemaType, q.child)) case _ => (for { ctpe <- tpe.field(q.name) @@ -616,9 +697,9 @@ class QueryCompiler(parser: QueryParser, schema: Schema, phases: List[Phase]) { } op.rootTpe(schema) match { - case Result.Success(tpe) => validateQueries(List((tpe, op.query))) + case Result.Success(tpe) => validateQueries(List((tpe, op.query))) case Result.Warning(ps, tpe) => ps.toList ++ validateQueries(List((tpe, op.query))) - case Result.Failure(ps) => ps.toList + case Result.Failure(ps) => ps.toList case Result.InternalError(_) => Nil // This will be reported elsewhere } } @@ -638,133 +719,260 @@ object QueryCompiler { import IntrospectionLevel._ /** - * Elaboration monad. - * - * Supports threading of state through the elaboration of a query. Provides, - * + access to the schema, context, variables and fragments of a query. - * + ability to transform the children of Selects to supply semantics for field arguments. - * + ability to add contextual data to the resulting query both to support propagation of - * context to the elaboration of children, and to to drive run time behaviour. - * + ability to add selects for additional attributes to the resulting query. - * + ability to test existence and properties of neighbour nodes of the node being - * elaborated. - * + ability to report errors and warnings during elaboration. - */ + * Elaboration monad. + * + * Supports threading of state through the elaboration of a query. Provides, + access to the + * schema, context, variables and fragments of a query. + ability to transform the children of + * Selects to supply semantics for field arguments. + ability to add contextual data to the + * resulting query both to support propagation of context to the elaboration of children, and + * to to drive run time behaviour. + ability to add selects for additional attributes to the + * resulting query. + ability to test existence and properties of neighbour nodes of the node + * being elaborated. + ability to report errors and warnings during elaboration. + */ type Elab[T] = StateT[Result, ElabState, T] object Elab { def unit: Elab[Unit] = StateT.pure(()) def pure[T](t: T): Elab[T] = StateT.pure(t) def liftR[T](rt: Result[T]): Elab[T] = StateT.liftF(rt) - /** The scheam of the query being elaborated */ + /** + * The scheam of the query being elaborated + */ def schema: Elab[Schema] = StateT.inspect(_.schema) - /** The context of the node currently being elaborated */ + + /** + * The context of the node currently being elaborated + */ def context: Elab[Context] = StateT.inspect(_.context) - /** The variables of the query being elaborated */ + + /** + * The variables of the query being elaborated + */ def vars: Elab[Vars] = StateT.inspect(_.vars) - /** The fragments of the query being elaborated */ + + /** + * The fragments of the query being elaborated + */ def fragments: Elab[Map[String, UntypedFragment]] = StateT.inspect(_.fragments) - /** The fragment with the supplied name, if defined, failing otherwise */ + + /** + * The fragment with the supplied name, if defined, failing otherwise + */ def fragment(nme: String): Elab[UntypedFragment] = StateT.inspectF(_.fragments.get(nme).toResult(s"Fragment '$nme' is not defined")) - def transformFragments(f: Map[String, UntypedFragment] => Elab[Map[String, UntypedFragment]]): Elab[Unit] = + def transformFragments( + f: Map[String, UntypedFragment] => Elab[Map[String, UntypedFragment]]): Elab[Unit] = for { - fs <- fragments + fs <- fragments fs0 <- f(fs) - _ <- StateT.modify(_.copy(fragments = fs0)): Elab[Unit] + _ <- StateT.modify(_.copy(fragments = fs0)): Elab[Unit] } yield () - /** `true` if the node currently being elaborated has a child with the supplied name */ + + /** + * `true` if the node currently being elaborated has a child with the supplied name + */ def hasField(name: String): Elab[Boolean] = StateT.inspect(_.hasField(name)) - /** The alias, if any, of the child with the supplied name */ + + /** + * The alias, if any, of the child with the supplied name + */ def fieldAlias(name: String): Elab[Option[String]] = StateT.inspect(_.fieldAlias(name)) - /** `true` if the node currently being elaborated has a sibling with the supplied name */ + + /** + * `true` if the node currently being elaborated has a sibling with the supplied name + */ def hasSibling(name: String): Elab[Boolean] = StateT.inspect(_.hasSibling(name)) - /** The result name of the node currently being elaborated */ + + /** + * The result name of the node currently being elaborated + */ def resultName: Elab[Option[String]] = StateT.inspect(_.resultName) - /** Binds the supplied value to the supplied name in the elaboration environment */ + /** + * Binds the supplied value to the supplied name in the elaboration environment + */ def env(nme: String, value: Any): Elab[Unit] = env(List(nme -> value)) - /** Binds the supplied names and values in the elaboration environment */ + + /** + * Binds the supplied names and values in the elaboration environment + */ def env(kv: (String, Any), kvs: (String, Any)*): Elab[Unit] = env(kv +: kvs.toSeq) - /** Binds the supplied names and values in the elaboration environment */ + + /** + * Binds the supplied names and values in the elaboration environment + */ def env(kvs: Seq[(String, Any)]): Elab[Unit] = StateT.modify(_.env(kvs)) - /** Adds all the bindings of the supplied environment to the elaboration environment */ + + /** + * Adds all the bindings of the supplied environment to the elaboration environment + */ def env(other: Env): Elab[Unit] = StateT.modify(_.env(other)) - /** The value bound to the supplied name in the elaboration environment, if any */ + + /** + * The value bound to the supplied name in the elaboration environment, if any + */ def env[T: ClassTag](nme: String): Elab[Option[T]] = StateT.inspect(_.env[T](nme)) - /** The value bound to the supplied name in the elaboration environment, if any, failing otherwise */ + + /** + * The value bound to the supplied name in the elaboration environment, if any, failing + * otherwise + */ def envE[T: ClassTag: TypeName](nme: String): Elab[T] = - env(nme).flatMap(v => Elab.liftR(v.toResultOrError(s"Key '$nme' of type ${typeName[T]} was not found in $this"))) - /** The subset of the elaboration environment defined directly at this node */ + env(nme).flatMap(v => + Elab.liftR( + v.toResultOrError(s"Key '$nme' of type ${typeName[T]} was not found in $this"))) + + /** + * The subset of the elaboration environment defined directly at this node + */ def localEnv: Elab[Env] = StateT.inspect(_.localEnv) - /** Applies the supplied transformation to the child of the node currently being elaborated */ - def transformChild(f: Query => Elab[Query]): Elab[Unit] = StateT.modify(_.addChildTransform(f)) - /** Applies the supplied transformation to the child of the node currently being elaborated */ - def transformChild(f: Query => Query)(implicit dummy: DummyImplicit): Elab[Unit] = transformChild(q => Elab.pure(f(q))) - /** Applies the supplied transformation to the child of the node currently being elaborated */ - def transformChild(f: Query => Result[Query])(implicit dummy1: DummyImplicit, dummy2: DummyImplicit): Elab[Unit] = transformChild(q => Elab.liftR(f(q))) - /** The transformation to be applied to the child of the node currently being elaborated */ + /** + * Applies the supplied transformation to the child of the node currently being elaborated + */ + def transformChild(f: Query => Elab[Query]): Elab[Unit] = + StateT.modify(_.addChildTransform(f)) + + /** + * Applies the supplied transformation to the child of the node currently being elaborated + */ + def transformChild(f: Query => Query)(implicit dummy: DummyImplicit): Elab[Unit] = + transformChild(q => Elab.pure(f(q))) + + /** + * Applies the supplied transformation to the child of the node currently being elaborated + */ + def transformChild(f: Query => Result[Query])( + implicit dummy1: DummyImplicit, + dummy2: DummyImplicit): Elab[Unit] = transformChild(q => Elab.liftR(f(q))) + + /** + * The transformation to be applied to the child of the node currently being elaborated + */ def transform: Elab[Query => Elab[Query]] = StateT.inspect(_.childTransform) - /** Add the supplied attributed and corresponding query, if any, to the query being elaborated */ - def addAttribute(name: String, query: Query = Empty): Elab[Unit] = StateT.modify(_.addAttribute(name, query)) - /** The attributes which have been added to the query being elaborated */ + + /** + * Add the supplied attributed and corresponding query, if any, to the query being + * elaborated + */ + def addAttribute(name: String, query: Query = Empty): Elab[Unit] = + StateT.modify(_.addAttribute(name, query)) + + /** + * The attributes which have been added to the query being elaborated + */ def attributes: Elab[List[(String, Query)]] = StateT.inspect(_.attributes) - /** Report the supplied GraphQL warning during elaboration */ - def warning(msg: String): Elab[Unit] = StateT(s => Result.warning[(ElabState, Unit)](msg, (s, ()))) - /** Report the supplied GraphQL warning during elaboration */ - def warning(err: Problem): Elab[Unit] = StateT(s => Result.warning[(ElabState, Unit)](err, (s, ()))) - /** Report the supplied GraphQL error during elaboration */ + /** + * Report the supplied GraphQL warning during elaboration + */ + def warning(msg: String): Elab[Unit] = + StateT(s => Result.warning[(ElabState, Unit)](msg, (s, ()))) + + /** + * Report the supplied GraphQL warning during elaboration + */ + def warning(err: Problem): Elab[Unit] = + StateT(s => Result.warning[(ElabState, Unit)](err, (s, ()))) + + /** + * Report the supplied GraphQL error during elaboration + */ def failure[T](msg: String): Elab[T] = StateT(_ => Result.failure[(ElabState, T)](msg)) - /** Report the supplied GraphQL error during elaboration */ + + /** + * Report the supplied GraphQL error during elaboration + */ def failure[T](err: Problem): Elab[T] = StateT(_ => Result.failure[(ElabState, T)](err)) - /** Report the supplied internal error during elaboration */ - def internalError[T](msg: String): Elab[T] = StateT(_ => Result.internalError[(ElabState, T)](msg)) - /** Report the supplied internal error during elaboration */ - def internalError[T](err: Throwable): Elab[T] = StateT(_ => Result.internalError[(ElabState, T)](err)) - /** Save the current elaboration state */ + /** + * Report the supplied internal error during elaboration + */ + def internalError[T](msg: String): Elab[T] = + StateT(_ => Result.internalError[(ElabState, T)](msg)) + + /** + * Report the supplied internal error during elaboration + */ + def internalError[T](err: Throwable): Elab[T] = + StateT(_ => Result.internalError[(ElabState, T)](err)) + + /** + * Save the current elaboration state + */ def push: Elab[Unit] = StateT.modify(_.push) - /** Save the current elaboration state and switch to the supplied context and query */ + + /** + * Save the current elaboration state and switch to the supplied context and query + */ def push(context: Context, query: Query): Elab[Unit] = StateT.modify(_.push(context, query)) - /** Save the current elaboration state and switch to the supplied schema, context and query */ - def push(schema: Schema, context: Context, query: Query): Elab[Unit] = StateT.modify(_.push(schema, context, query)) - /** Restore the previous elaboration state */ + + /** + * Save the current elaboration state and switch to the supplied schema, context and query + */ + def push(schema: Schema, context: Context, query: Query): Elab[Unit] = + StateT.modify(_.push(schema, context, query)) + + /** + * Restore the previous elaboration state + */ def pop: Elab[Unit] = StateT.modifyF(s => s.parent.toResultOrError("Cannot pop root state")) } /** - * The state managed by the elaboration monad. - */ + * The state managed by the elaboration monad. + */ case class ElabState( - parent: Option[ElabState], - schema: Schema, - context: Context, - vars: Vars, - fragments: Map[String, UntypedFragment], - query: Query, - localEnv: Env, - attributes: List[(String, Query)], - childTransform: Query => Elab[Query] + parent: Option[ElabState], + schema: Schema, + context: Context, + vars: Vars, + fragments: Map[String, UntypedFragment], + query: Query, + localEnv: Env, + attributes: List[(String, Query)], + childTransform: Query => Elab[Query] ) { def hasField(fieldName: String): Boolean = Query.hasField(query, fieldName) def fieldAlias(fieldName: String): Option[String] = Query.fieldAlias(query, fieldName) - def hasSibling(fieldName: String): Boolean = parent.exists(s => Query.hasField(s.query, fieldName)) + def hasSibling(fieldName: String): Boolean = + parent.exists(s => Query.hasField(s.query, fieldName)) def resultName: Option[String] = Query.ungroup(query).headOption.flatMap(Query.resultName) def env(kvs: Seq[(String, Any)]): ElabState = copy(localEnv = localEnv.add(kvs: _*)) def env(other: Env): ElabState = copy(localEnv = localEnv.add(other)) - def env[T: ClassTag](nme: String): Option[T] = localEnv.get(nme).orElse(parent.flatMap(_.env(nme))) - def addAttribute(name: String, query: Query = Empty): ElabState = copy(attributes = (name, query) :: attributes) - def addChildTransform(f: Query => Elab[Query]): ElabState = copy(childTransform = childTransform.andThen(_.flatMap(f))) - def push: ElabState = copy(parent = Some(this), localEnv = Env.empty, attributes = Nil, childTransform = Elab.pure) + def env[T: ClassTag](nme: String): Option[T] = + localEnv.get(nme).orElse(parent.flatMap(_.env(nme))) + def addAttribute(name: String, query: Query = Empty): ElabState = + copy(attributes = (name, query) :: attributes) + def addChildTransform(f: Query => Elab[Query]): ElabState = + copy(childTransform = childTransform.andThen(_.flatMap(f))) + def push: ElabState = copy( + parent = Some(this), + localEnv = Env.empty, + attributes = Nil, + childTransform = Elab.pure) def push(context: Context, query: Query): ElabState = - copy(parent = Some(this), context = context, query = query, localEnv = Env.empty, attributes = Nil, childTransform = Elab.pure) + copy( + parent = Some(this), + context = context, + query = query, + localEnv = Env.empty, + attributes = Nil, + childTransform = Elab.pure) def push(schema: Schema, context: Context, query: Query): ElabState = - copy(parent = Some(this), schema = schema, context = context, query = query, localEnv = Env.empty, attributes = Nil, childTransform = Elab.pure) + copy( + parent = Some(this), + schema = schema, + context = context, + query = query, + localEnv = Env.empty, + attributes = Nil, + childTransform = Elab.pure) } - /** A QueryCompiler phase. */ + /** + * A QueryCompiler phase. + */ trait Phase { def transformFragments: Elab[Unit] = Elab.unit @@ -773,119 +981,128 @@ object QueryCompiler { */ def transform(query: Query): Elab[Query] = query match { - case s@UntypedSelect(fieldName, alias, _, _, child) => + case s @ UntypedSelect(fieldName, alias, _, _, child) => transformSelect(fieldName, alias, child).map(ec => s.copy(child = ec)) - case s@Select(fieldName, alias, child) => + case s @ Select(fieldName, alias, child) => transformSelect(fieldName, alias, child).map(ec => s.copy(child = ec)) - case n@Narrow(subtpe, child) => + case n @ Narrow(subtpe, child) => for { - c <- Elab.context - _ <- Elab.push(c.asType(subtpe), child) + c <- Elab.context + _ <- Elab.push(c.asType(subtpe), child) ec <- transform(child) - _ <- Elab.pop + _ <- Elab.pop } yield n.copy(child = ec) - case f@UntypedFragmentSpread(_, _) => Elab.pure(f) - case i@UntypedInlineFragment(None, _, child) => + case f @ UntypedFragmentSpread(_, _) => Elab.pure(f) + case i @ UntypedInlineFragment(None, _, child) => transform(child).map(ec => i.copy(child = ec)) - case i@UntypedInlineFragment(Some(tpnme), _, child) => + case i @ UntypedInlineFragment(Some(tpnme), _, child) => for { - s <- Elab.schema - c <- Elab.context - subtpe <- Elab.liftR(Result.fromOption( - s.definition(tpnme).orElse(Introspection.schema.definition(tpnme)), - s"Unknown type '$tpnme' in type condition of inline fragment" - )) - _ <- Elab.push(c.asType(subtpe), child) - ec <- transform(child) - _ <- Elab.pop + s <- Elab.schema + c <- Elab.context + subtpe <- Elab.liftR( + Result.fromOption( + s.definition(tpnme).orElse(Introspection.schema.definition(tpnme)), + s"Unknown type '$tpnme' in type condition of inline fragment" + )) + _ <- Elab.push(c.asType(subtpe), child) + ec <- transform(child) + _ <- Elab.pop } yield i.copy(child = ec) - case i@Introspect(_, child) => + case i @ Introspect(_, child) => for { - s <- Elab.schema - c <- Elab.context - iTpe = if(c.tpe =:= s.queryType) Introspection.schema.queryType else TypenameType - _ <- Elab.push(Introspection.schema, c.asType(iTpe), child) - ec <- transform(child) - _ <- Elab.pop + s <- Elab.schema + c <- Elab.context + iTpe = if (c.tpe =:= s.queryType) Introspection.schema.queryType else TypenameType + _ <- Elab.push(Introspection.schema, c.asType(iTpe), child) + ec <- transform(child) + _ <- Elab.pop } yield i.copy(child = ec) - case u@Unique(child) => + case u @ Unique(child) => for { - c <- Elab.context - _ <- Elab.push(c.asType(c.tpe.nonNull.list), child) + c <- Elab.context + _ <- Elab.push(c.asType(c.tpe.nonNull.list), child) ec <- transform(child) - _ <- Elab.pop + _ <- Elab.pop } yield u.copy(child = ec) - case f@Filter(_, child) => + case f @ Filter(_, child) => for { - c <- Elab.context + c <- Elab.context item <- Elab.liftR(c.tpe.item.toResultOrError(s"Filter of non-List type ${c.tpe}")) - _ <- Elab.push(c.asType(item), child) - ec <- transform(child) - _ <- Elab.pop + _ <- Elab.push(c.asType(item), child) + ec <- transform(child) + _ <- Elab.pop } yield f.copy(child = ec) - case n@Count(child) => + case n @ Count(child) => for { - c <- Elab.context + c <- Elab.context pc <- Elab.liftR(c.parent.toResultOrError(s"Count node has no parent")) - _ <- Elab.push(pc, child) + _ <- Elab.push(pc, child) ec <- transform(child) - _ <- Elab.pop + _ <- Elab.pop } yield n.copy(child = ec) - case g@Group(children) => - children.traverse { c => - for { - _ <- Elab.push - tc <- transform(c) - _ <- Elab.pop - } yield tc - }.map(eqs => g.copy(queries = eqs)) - - case c@Component(_, _, child) => transform(child).map(ec => c.copy(child = ec)) - case e@Effect(_, child) => transform(child).map(ec => e.copy(child = ec)) - case l@Limit(_, child) => transform(child).map(ec => l.copy(child = ec)) - case o@Offset(_, child) => transform(child).map(ec => o.copy(child = ec)) - case o@OrderBy(_, child) => transform(child).map(ec => o.copy(child = ec)) - case e@Environment(_, child) => transform(child).map(ec => e.copy(child = ec)) - case t@TransformCursor(_, child) => transform(child).map(ec => t.copy(child = ec)) - case Empty => Elab.pure(Empty) + case g @ Group(children) => + children + .traverse { c => + for { + _ <- Elab.push + tc <- transform(c) + _ <- Elab.pop + } yield tc + } + .map(eqs => g.copy(queries = eqs)) + + case c @ Component(_, _, child) => transform(child).map(ec => c.copy(child = ec)) + case e @ Effect(_, child) => transform(child).map(ec => e.copy(child = ec)) + case l @ Limit(_, child) => transform(child).map(ec => l.copy(child = ec)) + case o @ Offset(_, child) => transform(child).map(ec => o.copy(child = ec)) + case o @ OrderBy(_, child) => transform(child).map(ec => o.copy(child = ec)) + case e @ Environment(_, child) => transform(child).map(ec => e.copy(child = ec)) + case t @ TransformCursor(_, child) => transform(child).map(ec => t.copy(child = ec)) + case Empty => Elab.pure(Empty) } def transformSelect(fieldName: String, alias: Option[String], child: Query): Elab[Query] = for { - c <- Elab.context - _ <- validateSubselection(fieldName, child) + c <- Elab.context + _ <- validateSubselection(fieldName, child) childCtx <- Elab.liftR(c.forField(fieldName, alias)) - _ <- Elab.push(childCtx, child) - ec <- transform(child) - _ <- Elab.pop + _ <- Elab.push(childCtx, child) + ec <- transform(child) + _ <- Elab.pop } yield ec def validateSubselection(fieldName: String, child: Query): Elab[Unit] = for { - c <- Elab.context + c <- Elab.context childCtx <- Elab.liftR(c.forField(fieldName, None)) - tpe = childCtx.tpe - _ <- { - val isLeaf = tpe.isUnderlyingLeaf - def obj = c.tpe.underlyingNamed - if (isLeaf && child != Empty) - Elab.failure(s"Leaf field '$fieldName' of $obj must have an empty subselection set") - else if (!isLeaf && child == Empty) - Elab.failure(s"Non-leaf field '$fieldName' of $obj must have a non-empty subselection set") - else - Elab.pure(()) - } + tpe = childCtx.tpe + _ <- { + val isLeaf = tpe.isUnderlyingLeaf + def obj = c.tpe.underlyingNamed + if (isLeaf && child != Empty) + Elab.failure(s"Leaf field '$fieldName' of $obj must have an empty subselection set") + else if (!isLeaf && child == Empty) + Elab.failure( + s"Non-leaf field '$fieldName' of $obj must have a non-empty subselection set") + else + Elab.pure(()) + } } yield () - val TypenameType = ObjectType(s"__Typename", None, List(Field("__typename", None, Nil, StringType, Nil)), Nil, Nil) + val TypenameType = ObjectType( + s"__Typename", + None, + List(Field("__typename", None, Nil, StringType, Nil)), + Nil, + Nil) } /** @@ -894,22 +1111,32 @@ object QueryCompiler { class IntrospectionElaborator(level: IntrospectionLevel) extends Phase { override def transformFragments: Elab[Unit] = Elab.transformFragments { fs => - fs.toList.traverse { - case (nme, f@UntypedFragment(_, tpnme, _, child)) => - for { - s <- Elab.schema - c <- Elab.context - tpe <- Elab.liftR(Result.fromOption(s.definition(tpnme).orElse(Introspection.schema.definition(tpnme)), s"Unknown type '$tpnme' in fragment definition")) - _ <- Elab.push(c.asType(tpe), child) - ec <- transform(child) - _ <- Elab.pop - } yield (nme, f.copy(child = ec)) - }.map(_.toMap) + fs.toList + .traverse { + case (nme, f @ UntypedFragment(_, tpnme, _, child)) => + for { + s <- Elab.schema + c <- Elab.context + tpe <- Elab.liftR( + Result.fromOption( + s.definition(tpnme).orElse(Introspection.schema.definition(tpnme)), + s"Unknown type '$tpnme' in fragment definition")) + _ <- Elab.push(c.asType(tpe), child) + ec <- transform(child) + _ <- Elab.pop + } yield (nme, f.copy(child = ec)) + } + .map(_.toMap) } override def transform(query: Query): Elab[Query] = query match { - case s@UntypedSelect(fieldName @ ("__typename" | "__schema" | "__type"), _, _, _, _) => + case s @ UntypedSelect( + fieldName @ ("__typename" | "__schema" | "__type"), + _, + _, + _, + _) => (fieldName, level) match { case ("__typename", Disabled) => Elab.failure("Introspection is disabled") @@ -933,41 +1160,37 @@ object QueryCompiler { } /** - * A phase which elaborates variables, directives, fragment spreads - * and inline fragments. - * - * 1. Query variable values are substituted for all variable - * references. - * - * 2. `skip` and `include` directives are handled during this phase - * and the guarded subqueries are retained or removed as - * appropriate. + * A phase which elaborates variables, directives, fragment spreads and inline fragments. * - * 3. Fragment spread and inline fragments are expanded. - * - * 4. types narrowing coercions by resolving the target type - * against the schema. - * - * 5. verifies that leaves have an empty subselection set and that - * structured types have a non-empty subselection set. + * 1. Query variable values are substituted for all variable references. + * 2. `skip` and `include` directives are handled during this phase and the guarded + * subqueries are retained or removed as appropriate. + * 3. Fragment spread and inline fragments are expanded. + * 4. types narrowing coercions by resolving the target type against the schema. + * 5. verifies that leaves have an empty subselection set and that structured types have a + * non-empty subselection set. */ object VariablesSkipAndFragmentElaborator extends Phase { override def transform(query: Query): Elab[Query] = query match { - case sel@UntypedSelect(fieldName, alias, args, dirs, child) => + case sel @ UntypedSelect(fieldName, alias, args, dirs, child) => isSkipped(dirs).ifM( Elab.pure(Empty), for { - _ <- validateSubselection(fieldName, child) - s <- Elab.schema - c <- Elab.context + _ <- validateSubselection(fieldName, child) + s <- Elab.schema + c <- Elab.context childCtx <- Elab.liftR(c.forField(fieldName, alias)) - vars <- Elab.vars - eArgs <- args.traverse(elaborateBinding(_, vars)) - eDirs <- Elab.liftR(Directive.elaborateDirectives(s, dirs.filterNot(dir => dir.name == "skip" || dir.name == "include"), vars)) - _ <- Elab.push(childCtx, child) - ec <- transform(child) - _ <- Elab.pop + vars <- Elab.vars + eArgs <- args.traverse(elaborateBinding(_, vars)) + eDirs <- Elab.liftR( + Directive.elaborateDirectives( + s, + dirs.filterNot(dir => dir.name == "skip" || dir.name == "include"), + vars)) + _ <- Elab.push(childCtx, child) + ec <- transform(child) + _ <- Elab.pop } yield sel.copy(args = eArgs, directives = eDirs, child = ec) ) @@ -975,15 +1198,19 @@ object QueryCompiler { isSkipped(dirs).ifM( Elab.pure(Empty), for { - s <- Elab.schema - c <- Elab.context - f <- Elab.fragment(nme) - ctpe = c.tpe.underlyingNamed - subtpe <- Elab.liftR(s.definition(f.tpnme).toResult(s"Unknown type '${f.tpnme}' in type condition of fragment '$nme'")) - _ <- Elab.failure(s"Fragment '$nme' is not compatible with type '${c.tpe}'").whenA(!fragmentApplies(s, subtpe, ctpe)) - _ <- Elab.push(c.asType(subtpe), f.child) - ec <- transform(f.child) - _ <- Elab.pop + s <- Elab.schema + c <- Elab.context + f <- Elab.fragment(nme) + ctpe = c.tpe.underlyingNamed + subtpe <- Elab.liftR( + s.definition(f.tpnme) + .toResult(s"Unknown type '${f.tpnme}' in type condition of fragment '$nme'")) + _ <- Elab + .failure(s"Fragment '$nme' is not compatible with type '${c.tpe}'") + .whenA(!fragmentApplies(s, subtpe, ctpe)) + _ <- Elab.push(c.asType(subtpe), f.child) + ec <- transform(f.child) + _ <- Elab.pop } yield if (ctpe <:< subtpe) ec else Narrow(s.uncheckedRef(subtpe), ec) @@ -993,19 +1220,24 @@ object QueryCompiler { isSkipped(dirs).ifM( Elab.pure(Empty), for { - s <- Elab.schema - c <- Elab.context - ctpe = c.tpe.underlyingNamed + s <- Elab.schema + c <- Elab.context + ctpe = c.tpe.underlyingNamed subtpe <- tpnme0 match { - case None => - Elab.pure(ctpe) - case Some(tpnme) => - Elab.liftR(s.definition(tpnme).toResult(s"Unknown type '$tpnme' in type condition of inline fragment")) - } - _ <- Elab.failure(s"Inline fragment with type condition '$subtpe' is not compatible with type '$ctpe'").whenA(!fragmentApplies(s, subtpe, ctpe)) - _ <- Elab.push(c.asType(subtpe), child) - ec <- transform(child) - _ <- Elab.pop + case None => + Elab.pure(ctpe) + case Some(tpnme) => + Elab.liftR( + s.definition(tpnme) + .toResult(s"Unknown type '$tpnme' in type condition of inline fragment")) + } + _ <- Elab + .failure( + s"Inline fragment with type condition '$subtpe' is not compatible with type '$ctpe'") + .whenA(!fragmentApplies(s, subtpe, ctpe)) + _ <- Elab.push(c.asType(subtpe), child) + ec <- transform(child) + _ <- Elab.pop } yield if (ctpe <:< subtpe) ec else Narrow(s.uncheckedRef(subtpe), ec) @@ -1034,7 +1266,8 @@ object QueryCompiler { for { c <- extractCond(value) } yield (nme == "skip" && c) || (nme == "include" && !c) - case List(Directive(nme, _)) => Elab.failure(s"Directive '$nme' must have a single Boolean 'if' argument") + case List(Directive(nme, _)) => + Elab.failure(s"Directive '$nme' must have a single Boolean 'if' argument") case _ => Elab.failure("skip/include directives must be unique") } @@ -1042,12 +1275,13 @@ object QueryCompiler { value match { case VariableRef(varName) => for { - v <- Elab.vars - tv <- Elab.liftR(Result.fromOption(v.get(varName), s"Variable '$varName' is undefined")) - b <- tv match { - case (tpe, BooleanValue(value)) if tpe.nonNull =:= BooleanType => Elab.pure(value) - case _ => Elab.failure(s"Argument of skip/include must be boolean") - } + v <- Elab.vars + tv <- Elab.liftR( + Result.fromOption(v.get(varName), s"Variable '$varName' is undefined")) + b <- tv match { + case (tpe, BooleanValue(value)) if tpe.nonNull =:= BooleanType => Elab.pure(value) + case _ => Elab.failure(s"Argument of skip/include must be boolean") + } } yield b case BooleanValue(value) => Elab.pure(value) case _ => Elab.failure(s"Argument of skip/include must be boolean") @@ -1055,72 +1289,69 @@ object QueryCompiler { } /** - * A compiler phase which applies GraphQL field merge rules to an - * untyped query. - */ + * A compiler phase which applies GraphQL field merge rules to an untyped query. + */ object MergeFields extends Phase { override def transform(query: Query): Elab[Query] = Elab.pure(mergeUntypedQueries(List(query))) } /** - * A compiler phase which translates `Select` nodes to be directly - * interpretable. + * A compiler phase which translates `Select` nodes to be directly interpretable. * * This phase, * - * 1. types bindings according to the schema: - * i) untyped enums are validated and typed according to their - * declared type. - * ii) String and Int bindings are translated to ID bindings - * where appropriate. - * iii) default values are supplied for missing arguments. - * iv) arguments are permuted into the order declared in the - * schema. + * 1. types bindings according to the schema: i) untyped enums are validated and typed + * according to their declared type. ii) String and Int bindings are translated to ID + * bindings where appropriate. iii) default values are supplied for missing arguments. + * iv) arguments are permuted into the order declared in the schema. + * 2. eliminates Select arguments by delegating to a model-specific `PartialFunction` which + * is responsible for translating `Select` nodes into a form which is directly + * interpretable, for example, replacing them with a `Filter` or `Unique` node with a + * `Predicate` which is parameterized by the arguments, ie., * - * 2. eliminates Select arguments by delegating to a model-specific - * `PartialFunction` which is responsible for translating `Select` - * nodes into a form which is directly interpretable, for example, - * replacing them with a `Filter` or `Unique` node with a - * `Predicate` which is parameterized by the arguments, ie., - * - * ``` - * UntypedSelect("character", None, List(IDBinding("id", "1000")), Nil, child) - * ``` - * might be translated to, - * ``` - * Select("character, None, Filter(FieldEquals("id", "1000"), child)) - * ``` - * 3. GraphQL introspection query field arguments are elaborated. + * ``` + * UntypedSelect("character", None, List(IDBinding("id", "1000")), Nil, child) + * ``` + * might be translated to, + * ``` + * Select("character, None, Filter(FieldEquals("id", "1000"), child)) + * ``` + * 3. GraphQL introspection query field arguments are elaborated. */ trait SelectElaborator extends Phase { override def transform(query: Query): Elab[Query] = query match { - case sel@UntypedSelect(fieldName, resultName, args, dirs, child) => + case sel @ UntypedSelect(fieldName, resultName, args, dirs, child) => for { - c <- Elab.context - s <- Elab.schema + c <- Elab.context + s <- Elab.schema childCtx <- Elab.liftR(c.forField(fieldName, resultName)) - obj = c.tpe.underlyingNamed.dealias - field <- obj match { - case twf: TypeWithFields => - Elab.liftR(twf.fieldInfo(fieldName).toResult(s"No field '$fieldName' for type ${obj.underlying}")) - case _ => Elab.failure(s"Type $obj is not an object or interface type") - } - eArgs <- Elab.liftR(elaborateFieldArgs(obj, field, args)) - _ <- if (s eq Introspection.schema) elaborateIntrospection(Introspection.schema.uncheckedRef(obj), fieldName, eArgs) - else select(s.uncheckedRef(obj), fieldName, eArgs, dirs) - elab <- Elab.transform - env <- Elab.localEnv - attrs <- Elab.attributes - _ <- Elab.push(childCtx, child) - ec <- transform(child) - _ <- Elab.pop - e2 <- elab(ec) + obj = c.tpe.underlyingNamed.dealias + field <- obj match { + case twf: TypeWithFields => + Elab.liftR( + twf + .fieldInfo(fieldName) + .toResult(s"No field '$fieldName' for type ${obj.underlying}")) + case _ => Elab.failure(s"Type $obj is not an object or interface type") + } + eArgs <- Elab.liftR(elaborateFieldArgs(obj, field, args)) + _ <- + if (s eq Introspection.schema) + elaborateIntrospection(Introspection.schema.uncheckedRef(obj), fieldName, eArgs) + else select(s.uncheckedRef(obj), fieldName, eArgs, dirs) + elab <- Elab.transform + env <- Elab.localEnv + attrs <- Elab.attributes + _ <- Elab.push(childCtx, child) + ec <- transform(child) + _ <- Elab.pop + e2 <- elab(ec) } yield { val e1 = Select(sel.name, sel.alias, e2) val e0 = - if(attrs.isEmpty) e1 + if (attrs.isEmpty) e1 else mergeQueries(e1 :: attrs.map { case (nme, child) => Select(nme, child) }) if (env.isEmpty) e0 @@ -1130,7 +1361,11 @@ object QueryCompiler { case _ => super.transform(query) } - def select(ref: TypeRef, name: String, args: List[Binding], directives: List[Directive]): Elab[Unit] + def select( + ref: TypeRef, + name: String, + args: List[Binding], + directives: List[Directive]): Elab[Unit] val QueryTypeRef = Introspection.QueryType val TypeTypeRef = Introspection.__TypeType @@ -1140,95 +1375,138 @@ object QueryCompiler { def elaborateIntrospection(ref: TypeRef, name: String, args: List[Binding]): Elab[Unit] = (ref, name, args) match { case (QueryTypeRef, "__type", List(Binding("name", StringValue(name)))) => - Elab.transformChild(child => Unique(Filter(Eql(TypeTypeRef / "name", Const(Option(name))), child))) - - case (TypeTypeRef, "fields", List(Binding("includeDeprecated", BooleanValue(include)))) => - Elab.transformChild(child => if (include) child else Filter(Eql(FieldTypeRef / "isDeprecated", Const(false)), child)) - case (TypeTypeRef, "enumValues", List(Binding("includeDeprecated", BooleanValue(include)))) => - Elab.transformChild(child => if (include) child else Filter(Eql(EnumValueTypeRef / "isDeprecated", Const(false)), child)) - case (TypeTypeRef, "inputFields", List(Binding("includeDeprecated", BooleanValue(include)))) => - Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) - case (FieldTypeRef, "args", List(Binding("includeDeprecated", BooleanValue(include)))) => - Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) - case (Introspection.__DirectiveType, "args", List(Binding("includeDeprecated", BooleanValue(include)))) => - Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) + Elab.transformChild(child => + Unique(Filter(Eql(TypeTypeRef / "name", Const(Option(name))), child))) + + case ( + TypeTypeRef, + "fields", + List(Binding("includeDeprecated", BooleanValue(include)))) => + Elab.transformChild(child => + if (include) child + else Filter(Eql(FieldTypeRef / "isDeprecated", Const(false)), child)) + case ( + TypeTypeRef, + "enumValues", + List(Binding("includeDeprecated", BooleanValue(include)))) => + Elab.transformChild(child => + if (include) child + else Filter(Eql(EnumValueTypeRef / "isDeprecated", Const(false)), child)) + case ( + TypeTypeRef, + "inputFields", + List(Binding("includeDeprecated", BooleanValue(include)))) => + Elab.transformChild(child => + if (include) child + else + Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) + case ( + FieldTypeRef, + "args", + List(Binding("includeDeprecated", BooleanValue(include)))) => + Elab.transformChild(child => + if (include) child + else + Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) + case ( + Introspection.__DirectiveType, + "args", + List(Binding("includeDeprecated", BooleanValue(include)))) => + Elab.transformChild(child => + if (include) child + else + Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child)) case _ => Elab.unit } - def elaborateFieldArgs(tpe: NamedType, field: Field, args: List[Binding]): Result[List[Binding]] = { + def elaborateFieldArgs( + tpe: NamedType, + field: Field, + args: List[Binding]): Result[List[Binding]] = { val infos = field.args val unknownArgs = args.filterNot(arg => infos.exists(_.name == arg.name)) if (unknownArgs.nonEmpty) - Result.failure(s"Unknown argument(s) ${unknownArgs.map(s => s"'${s.name}'").mkString("", ", ", "")} in field ${field.name} of type ${tpe.name}") + Result.failure( + s"Unknown argument(s) ${unknownArgs.map(s => s"'${s.name}'").mkString("", ", ", "")} in field ${field.name} of type ${tpe.name}") else { val argMap = args.groupMapReduce(_.name)(_.value)((x, _) => x) - infos.traverse(info => checkValue(info, argMap.get(info.name), s"field '${field.name}' of type '$tpe'").map(v => Binding(info.name, v))) + infos.traverse(info => + checkValue(info, argMap.get(info.name), s"field '${field.name}' of type '$tpe'").map( + v => Binding(info.name, v))) } } } object SelectElaborator { + /** - * Construct a `SelectElaborator` given a partial function which is called for each - * Select` node in the query. + * Construct a `SelectElaborator` given a partial function which is called for each Select` + * node in the query. */ - def apply(sel: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]]): SelectElaborator = + def apply( + sel: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]]): SelectElaborator = new SelectElaborator { - def select(ref: TypeRef, name: String, args: List[Binding], directives: List[Directive]): Elab[Unit] = - if(sel.isDefinedAt((ref, name, args))) sel((ref, name, args)) + def select( + ref: TypeRef, + name: String, + args: List[Binding], + directives: List[Directive]): Elab[Unit] = + if (sel.isDefinedAt((ref, name, args))) sel((ref, name, args)) else Elab.unit } - /** A select elaborator which discards all field arguments */ + /** + * A select elaborator which discards all field arguments + */ def identity: SelectElaborator = SelectElaborator(_ => Elab.unit) } /** - * A compiler phase which partitions a query for execution by multiple - * composed mappings. + * A compiler phase which partitions a query for execution by multiple composed mappings. * - * This phase transforms the input query by assigning subtrees to component - * mappings as specified by the supplied `cmapping`. + * This phase transforms the input query by assigning subtrees to component mappings as + * specified by the supplied `cmapping`. * - * The mapping has `Type` and field name pairs as keys and mapping and - * join function pairs as values. When the traversal of the input query - * visits a `Select` node with type `Type.field name` it will replace the - * `Select` with a `Component` node comprising, + * The mapping has `Type` and field name pairs as keys and mapping and join function pairs as + * values. When the traversal of the input query visits a `Select` node with type + * `Type.field name` it will replace the `Select` with a `Component` node comprising, * - * 1. the mapping which will be responsible for evaluating the subquery. - * 2. A join function which will be called during interpretation with, + * 1. the mapping which will be responsible for evaluating the subquery. + * 2. A join function which will be called during interpretation with, * - * i) The deferred subquery. - * ii) the cursor at that point in evaluation. + * i) The deferred subquery. ii) the cursor at that point in evaluation. * - * This join function is responsible for computing the continuation - * query which will be evaluated by the responsible interpreter. + * This join function is responsible for computing the continuation query which will be + * evaluated by the responsible interpreter. * - * Because the join is provided with the cursor of the parent - * interpreter the subquery can be parameterised with values derived - * from the parent query. + * Because the join is provided with the cursor of the parent interpreter the subquery can be + * parameterised with values derived from the parent query. */ - class ComponentElaborator[F[_]] private (cmapping: Map[(Type, String), (Mapping[F], (Query, Cursor) => Result[Query])]) extends Phase { + class ComponentElaborator[F[_]] private ( + cmapping: Map[(Type, String), (Mapping[F], (Query, Cursor) => Result[Query])]) + extends Phase { override def transform(query: Query): Elab[Query] = query match { - case s@Select(fieldName, resultName, child) => + case s @ Select(fieldName, resultName, child) => for { - c <- Elab.context - obj <- Elab.liftR(c.tpe.underlyingObject.toResultOrError(s"Type ${c.tpe} is not an object or interface type")) - childCtx = c.forFieldOrAttribute(fieldName, resultName) - _ <- Elab.push(childCtx, child) - ec <- transform(child) - _ <- Elab.pop - schema <- Elab.schema - ref = schema.uncheckedRef(obj) - } yield - cmapping.get((ref, fieldName)) match { - case Some((component, join)) => - Component(component, join, s.copy(child = ec)) - case None => - s.copy(child = ec) - } + c <- Elab.context + obj <- Elab.liftR( + c.tpe + .underlyingObject + .toResultOrError(s"Type ${c.tpe} is not an object or interface type")) + childCtx = c.forFieldOrAttribute(fieldName, resultName) + _ <- Elab.push(childCtx, child) + ec <- transform(child) + _ <- Elab.pop + schema <- Elab.schema + ref = schema.uncheckedRef(obj) + } yield cmapping.get((ref, fieldName)) match { + case Some((component, join)) => + Component(component, join, s.copy(child = ec)) + case None => + s.copy(child = ec) + } case _ => super.transform(query) } @@ -1237,46 +1515,50 @@ object QueryCompiler { object ComponentElaborator { val TrivialJoin = (q: Query, _: Cursor) => q.success - case class ComponentMapping[F[_]](tpe: TypeRef, fieldName: String, mapping: Mapping[F], join: (Query, Cursor) => Result[Query] = TrivialJoin) + case class ComponentMapping[F[_]]( + tpe: TypeRef, + fieldName: String, + mapping: Mapping[F], + join: (Query, Cursor) => Result[Query] = TrivialJoin) def apply[F[_]](mappings: Seq[ComponentMapping[F]]): ComponentElaborator[F] = - new ComponentElaborator(mappings.map(m => ((m.tpe, m.fieldName), (m.mapping, m.join))).toMap) + new ComponentElaborator( + mappings.map(m => ((m.tpe, m.fieldName), (m.mapping, m.join))).toMap) } /** - * A compiler phase which partitions a query for execution which may invoke - * multiple effect handlers. + * A compiler phase which partitions a query for execution which may invoke multiple effect + * handlers. * - * This phase transforms the input query by assigning subtrees to effect - * handlers as specified by the supplied `effects`, which takes a `Context` and - * fieldName, retuning an `EffectHandler` (if any). + * This phase transforms the input query by assigning subtrees to effect handlers as specified + * by the supplied `effects`, which takes a `Context` and fieldName, retuning an + * `EffectHandler` (if any). * - * The mapping has `Type` and field name pairs as keys and effect handlers - * as values. When the traversal of the input query visits a `Select` node - * with type `Type.field name` it will replace the - * `Select` with an `Effect` node comprising, + * The mapping has `Type` and field name pairs as keys and effect handlers as values. When the + * traversal of the input query visits a `Select` node with type `Type.field name` it will + * replace the `Select` with an `Effect` node comprising, * - * 1. the effect handler which will be responsible for running the effect - * and evaluating the subquery against its result. - * 2. the subquery which will be evaluated by the effect handler. + * 1. the effect handler which will be responsible for running the effect and evaluating the + * subquery against its result. + * 2. the subquery which will be evaluated by the effect handler. */ - class EffectElaborator[F[_]] private (effects: (Context, String) => Option[EffectHandler[F]]) extends Phase { + class EffectElaborator[F[_]] private (effects: (Context, String) => Option[EffectHandler[F]]) + extends Phase { override def transform(query: Query): Elab[Query] = query match { - case s@Select(fieldName, resultName, child) => + case s @ Select(fieldName, resultName, child) => for { - c <- Elab.context - childCtx = c.forFieldOrAttribute(fieldName, resultName) - _ <- Elab.push(childCtx, child) - ec <- transform(child) - _ <- Elab.pop - } yield - effects(c, fieldName) match { - case Some(handler) => - Select(fieldName, resultName, Effect(handler, s.copy(child = ec))) - case None => - s.copy(child = ec) - } + c <- Elab.context + childCtx = c.forFieldOrAttribute(fieldName, resultName) + _ <- Elab.push(childCtx, child) + ec <- transform(child) + _ <- Elab.pop + } yield effects(c, fieldName) match { + case Some(handler) => + Select(fieldName, resultName, Effect(handler, s.copy(child = ec))) + case None => + s.copy(child = ec) + } case _ => super.transform(query) } @@ -1285,22 +1567,26 @@ object QueryCompiler { object EffectElaborator { case class EffectMapping[F[_]](tpe: TypeRef, fieldName: String, handler: EffectHandler[F]) - def apply[F[_]](effects: (Context, String) => Option[EffectHandler[F]]): EffectElaborator[F] = + def apply[F[_]]( + effects: (Context, String) => Option[EffectHandler[F]]): EffectElaborator[F] = new EffectElaborator(effects) } /** - * A compiler phase which estimates the size of a query and applies width - * and depth limits. - */ + * A compiler phase which estimates the size of a query and applies width and depth limits. + */ class QuerySizeValidator(maxDepth: Int, maxWidth: Int) extends Phase { override def transform(query: Query): Elab[Query] = Elab.fragments.flatMap { frags => querySize(query, frags) match { - case (depth, _) if depth > maxDepth => Elab.failure(s"Query is too deep: depth is $depth levels, maximum is $maxDepth") - case (_, width) if width > maxWidth => Elab.failure(s"Query is too wide: width is $width leaves, maximum is $maxWidth") - case (depth, width) if depth > maxDepth && width > maxWidth => Elab.failure(s"Query is too complex: width/depth is $width/$depth leaves/levels, maximum is $maxWidth/$maxDepth") + case (depth, _) if depth > maxDepth => + Elab.failure(s"Query is too deep: depth is $depth levels, maximum is $maxDepth") + case (_, width) if width > maxWidth => + Elab.failure(s"Query is too wide: width is $width leaves, maximum is $maxWidth") + case (depth, width) if depth > maxDepth && width > maxWidth => + Elab.failure( + s"Query is too complex: width/depth is $width/$depth leaves/levels, maximum is $maxWidth/$maxDepth") case (_, _) => Elab.pure(query) } } diff --git a/modules/core/src/main/scala/composedmapping.scala b/modules/core/src/main/scala/composedmapping.scala index 9d6ee67a..a68c104c 100644 --- a/modules/core/src/main/scala/composedmapping.scala +++ b/modules/core/src/main/scala/composedmapping.scala @@ -17,11 +17,14 @@ package grackle import cats.MonadThrow -import Cursor.AbstractCursor -import syntax._ +import grackle.Cursor.AbstractCursor +import grackle.syntax._ abstract class ComposedMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] { - override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] = + override def mkCursorForMappedField( + parent: Cursor, + fieldContext: Context, + fm: FieldMapping): Result[Cursor] = ComposedCursor(fieldContext, parent.env).success case class ComposedCursor(context: Context, env: Env) extends AbstractCursor { diff --git a/modules/core/src/main/scala/cursor.scala b/modules/core/src/main/scala/cursor.scala index 387bf698..61dd6d8c 100644 --- a/modules/core/src/main/scala/cursor.scala +++ b/modules/core/src/main/scala/cursor.scala @@ -20,138 +20,168 @@ import scala.reflect.{classTag, ClassTag} import cats.implicits._ import io.circe.Json -import org.tpolecat.typename.{TypeName, typeName} +import org.tpolecat.typename.{typeName, TypeName} -import syntax._ +import grackle.syntax._ /** - * Indicates a position within an abstract data model during the interpretation - * of a GraphQL query. + * Indicates a position within an abstract data model during the interpretation of a GraphQL + * query. */ trait Cursor { - /** The parent of this `Cursor` */ + + /** + * The parent of this `Cursor` + */ def parent: Option[Cursor] - /** The value at the position represented by this `Cursor`. */ + /** + * The value at the position represented by this `Cursor`. + */ def focus: Any - /** The `Context` associated with this `Cursor`. */ + /** + * The `Context` associated with this `Cursor`. + */ def context: Context - /** The selection path from the root */ + /** + * The selection path from the root + */ def path: List[String] = context.path - /** The selection path from the root modified by query aliases. */ + /** + * The selection path from the root modified by query aliases. + */ def resultPath: List[String] = context.resultPath - /** The GraphQL type of the value at the position represented by this `Cursor`. */ + /** + * The GraphQL type of the value at the position represented by this `Cursor`. + */ def tpe: Type = context.tpe - /** Yields a copy of this `Cursor` with the supplied additional environment values. */ + /** + * Yields a copy of this `Cursor` with the supplied additional environment values. + */ def withEnv(env: Env): Cursor private[grackle] def env: Env - /** Yields the value of the supplied environment key, if any. */ + /** + * Yields the value of the supplied environment key, if any. + */ def env[T: ClassTag](nme: String): Option[T] = env.get(nme).orElse(parent.flatMap(_.env(nme))) - /** Yields the value of the supplied environment key, if any, or an error if none. */ + /** + * Yields the value of the supplied environment key, if any, or an error if none. + */ def envR[T: ClassTag: TypeName](nme: String): Result[T] = env(nme).toResultOrError(s"Key '$nme' of type ${typeName[T]} was not found in $this") - /** Yields the cumulative environment defined at this `Cursor`. */ + /** + * Yields the cumulative environment defined at this `Cursor`. + */ def fullEnv: Env = parent.map(_.fullEnv).getOrElse(Env.empty).add(env) - /** Does the environment at this `Cursor` contain a value for the supplied key? */ + /** + * Does the environment at this `Cursor` contain a value for the supplied key? + */ def envContains(nme: String): Boolean = env.contains(nme) || parent.exists(_.envContains(nme)) /** - * Yield the value at this `Cursor` as a value of type `T` if possible, - * an error or the left hand side otherwise. + * Yield the value at this `Cursor` as a value of type `T` if possible, an error or the left + * hand side otherwise. */ def as[T: ClassTag: TypeName]: Result[T] = - classTag[T].unapply(focus).toResultOrError(s"Expected value of type ${typeName[T]} for focus of type $tpe at path $path, found $focus") + classTag[T] + .unapply(focus) + .toResultOrError( + s"Expected value of type ${typeName[T]} for focus of type $tpe at path $path, found $focus") - /** Is the value at this `Cursor` of a scalar or enum type? */ + /** + * Is the value at this `Cursor` of a scalar or enum type? + */ def isLeaf: Boolean /** - * Yield the value at this `Cursor` rendered as Json if it is of a scalar or - * enum type, an error or the left hand side otherwise. + * Yield the value at this `Cursor` rendered as Json if it is of a scalar or enum type, an + * error or the left hand side otherwise. */ def asLeaf: Result[Json] /** - * Yield a `Cursor` which can be used to evaluate the antecedant of a `Unique` - * operation. - */ + * Yield a `Cursor` which can be used to evaluate the antecedant of a `Unique` operation. + */ def preunique: Result[Cursor] - /** Is the value at this `Cursor` of a list type? */ + /** + * Is the value at this `Cursor` of a list type? + */ def isList: Boolean /** - * Yield a list of `Cursor`s corresponding to the elements of the value at - * this `Cursor` if it is of a list type, or an error or the left hand side - * otherwise. + * Yield a list of `Cursor`s corresponding to the elements of the value at this `Cursor` if it + * is of a list type, or an error or the left hand side otherwise. */ final def asList: Result[List[Cursor]] = asList(List) /** - * Yield a collection of `Cursor`s corresponding to the elements of the value at - * this `Cursor` if it is of a list type, or an error or the left hand side - * otherwise. + * Yield a collection of `Cursor`s corresponding to the elements of the value at this `Cursor` + * if it is of a list type, or an error or the left hand side otherwise. */ def asList[C](factory: Factory[Cursor, C]): Result[C] /** - * Yields the number of elements of this `Cursor` if it is of a list type, or an - * error otherwise. - */ + * Yields the number of elements of this `Cursor` if it is of a list type, or an error + * otherwise. + */ def listSize: Result[Int] - /** Is the value at this `Cursor` of a nullable type? */ + /** + * Is the value at this `Cursor` of a nullable type? + */ def isNullable: Boolean /** - * Yield an optional `Cursor`s corresponding to the value at this `Cursor` if - * it is of a nullable type, or an error on the left hand side otherwise. The - * resulting `Cursor` will be present iff the current value is present in the - * model. + * Yield an optional `Cursor`s corresponding to the value at this `Cursor` if it is of a + * nullable type, or an error on the left hand side otherwise. The resulting `Cursor` will be + * present iff the current value is present in the model. */ def asNullable: Result[Option[Cursor]] /** - * Yields whether or not this `Cursor` is defined if it is of a nullable type, - * or an error otherwise. - */ + * Yields whether or not this `Cursor` is defined if it is of a nullable type, or an error + * otherwise. + */ def isDefined: Result[Boolean] - /** Is the value at this `Cursor` narrowable to `subtpe`? */ + /** + * Is the value at this `Cursor` narrowable to `subtpe`? + */ def narrowsTo(subtpe: TypeRef): Result[Boolean] /** - * Yield a `Cursor` corresponding to the value at this `Cursor` narrowed to - * type `subtpe`, or an error on the left hand side if such a narrowing is not - * possible. + * Yield a `Cursor` corresponding to the value at this `Cursor` narrowed to type `subtpe`, or + * an error on the left hand side if such a narrowing is not possible. */ def narrow(subtpe: TypeRef): Result[Cursor] /** - * Yield a `Cursor` corresponding to the value of the field `fieldName` of the - * value at this `Cursor`, or an error on the left hand side if there is no - * such field. + * Yield a `Cursor` corresponding to the value of the field `fieldName` of the value at this + * `Cursor`, or an error on the left hand side if there is no such field. */ def field(fieldName: String, resultName: Option[String]): Result[Cursor] /** - * Yield the value of the field `fieldName` of this `Cursor` as a value of - * type `T` if possible, an error or the left hand side otherwise. + * Yield the value of the field `fieldName` of this `Cursor` as a value of type `T` if + * possible, an error or the left hand side otherwise. */ - def fieldAs[T: ClassTag : TypeName](fieldName: String): Result[T] = + def fieldAs[T: ClassTag: TypeName](fieldName: String): Result[T] = field(fieldName, None).flatMap(_.as[T]) - /** True if this cursor is nullable and null, false otherwise. */ + /** + * True if this cursor is nullable and null, false otherwise. + */ def isNull: Boolean = isNullable && (asNullable match { case Result.Success(None) => true @@ -159,9 +189,8 @@ trait Cursor { }) /** - * Yield a `Cursor` corresponding to the value of the possibly nullable field - * `fieldName` of the value at this `Cursor`, or an error on the left hand - * side if there is no such field. + * Yield a `Cursor` corresponding to the value of the possibly nullable field `fieldName` of + * the value at this `Cursor`, or an error on the left hand side if there is no such field. */ def nullableField(fieldName: String): Result[Cursor] = if (isNullable) @@ -172,9 +201,9 @@ trait Cursor { else field(fieldName, None) /** - * Yield a `Cursor` corresponding to the value of the field identified by path - * `fns` starting from the value at this `Cursor`, or an error on the left - * hand side if there is no such field. + * Yield a `Cursor` corresponding to the value of the field identified by path `fns` starting + * from the value at this `Cursor`, or an error on the left hand side if there is no such + * field. */ def path(fns: List[String]): Result[Cursor] = fns match { case Nil => this.success @@ -186,9 +215,9 @@ trait Cursor { } /** - * Yield a list of `Cursor`s corresponding to the values generated by - * following the path `fns` from the value at this `Cursor`, or an error on - * the left hand side if there is no such path. + * Yield a list of `Cursor`s corresponding to the values generated by following the path `fns` + * from the value at this `Cursor`, or an error on the left hand side if there is no such + * path. */ def listPath(fns: List[String]): Result[List[Cursor]] = fns match { case Nil => List(this).success @@ -205,32 +234,35 @@ trait Cursor { } /** - * Yield a list of `Cursor`s corresponding to the values generated by - * following the path `fns` from the value at this `Cursor`, or an error on - * the left hand side if there is no such path. If the field at the end - * of the path is a list then yield the concatenation of the lists of - * cursors corresponding to the field elements. + * Yield a list of `Cursor`s corresponding to the values generated by following the path `fns` + * from the value at this `Cursor`, or an error on the left hand side if there is no such + * path. If the field at the end of the path is a list then yield the concatenation of the + * lists of cursors corresponding to the field elements. */ def flatListPath(fns: List[String]): Result[List[Cursor]] = - listPath(fns).flatMap(cs => cs.flatTraverse(c => if (c.isList) c.asList else List(c).success)) + listPath(fns).flatMap(cs => + cs.flatTraverse(c => if (c.isList) c.asList else List(c).success)) } object Cursor { def flatten(c: Cursor): Result[List[Cursor]] = - if(c.isList) c.asList.flatMap(flatten) - else if(c.isNullable) c.asNullable.flatMap(oc => flatten(oc.toList)) + if (c.isList) c.asList.flatMap(flatten) + else if (c.isNullable) c.asNullable.flatMap(oc => flatten(oc.toList)) else List(c).success def flatten(cs: List[Cursor]): Result[List[Cursor]] = cs.flatTraverse(flatten) - /** Abstract `Cursor` providing default implementation of most methods. */ + /** + * Abstract `Cursor` providing default implementation of most methods. + */ abstract class AbstractCursor extends Cursor { def isLeaf: Boolean = false def asLeaf: Result[Json] = - Result.internalError(s"Expected Scalar type, found $tpe for focus ${focus} at ${context.path.reverse.mkString("/")}") + Result.internalError( + s"Expected Scalar type, found $tpe for focus ${focus} at ${context.path.reverse.mkString("/")}") def preunique: Result[Cursor] = Result.internalError(s"Expected List type, found $focus for ${tpe.nonNull.list}") @@ -260,7 +292,9 @@ object Cursor { Result.internalError(s"No field '$fieldName' for type ${tpe.underlying}") } - /** Proxy `Cursor` which delegates most methods to an underlying `Cursor`.. */ + /** + * Proxy `Cursor` which delegates most methods to an underlying `Cursor`.. + */ class ProxyCursor(underlying: Cursor) extends Cursor { def context: Context = underlying.context @@ -294,11 +328,15 @@ object Cursor { def narrow(subtpe: TypeRef): Result[Cursor] = underlying.narrow(subtpe) - def field(fieldName: String, resultName: Option[String]): Result[Cursor] = underlying.field(fieldName, resultName) + def field(fieldName: String, resultName: Option[String]): Result[Cursor] = + underlying.field(fieldName, resultName) } - /** Empty `Cursor` with no content */ - case class EmptyCursor(context: Context, parent: Option[Cursor], env: Env) extends AbstractCursor { + /** + * Empty `Cursor` with no content + */ + case class EmptyCursor(context: Context, parent: Option[Cursor], env: Env) + extends AbstractCursor { def focus: Any = Result.internalError(s"Empty cursor has no focus") def withEnv(env0: Env): EmptyCursor = copy(env = env.add(env0)) } @@ -308,38 +346,53 @@ object Cursor { * * Typically used as the result of a `TransformCursor` operation */ - case class ListTransformCursor(underlying: Cursor, newSize: Int, newElems: Seq[Cursor]) extends ProxyCursor(underlying) { - override def withEnv(env: Env): Cursor = new ListTransformCursor(underlying.withEnv(env), newSize, newElems) + case class ListTransformCursor(underlying: Cursor, newSize: Int, newElems: Seq[Cursor]) + extends ProxyCursor(underlying) { + override def withEnv(env: Env): Cursor = + new ListTransformCursor(underlying.withEnv(env), newSize, newElems) override lazy val listSize: Result[Int] = newSize.success override def asList[C](factory: Factory[Cursor, C]): Result[C] = factory.fromSpecific(newElems).success } - /** Proxy `Cursor` which always yields a `NullCursor` for fields of the underlying cursor */ + /** + * Proxy `Cursor` which always yields a `NullCursor` for fields of the underlying cursor + */ case class NullFieldCursor(underlying: Cursor) extends ProxyCursor(underlying) { override def withEnv(env: Env): Cursor = new NullFieldCursor(underlying.withEnv(env)) override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = underlying.field(fieldName, resultName).map(NullCursor(_)) } - /** Proxy `Cursor` which always yields null */ + /** + * Proxy `Cursor` which always yields null + */ case class NullCursor(underlying: Cursor) extends ProxyCursor(underlying) { override def withEnv(env: Env): Cursor = new NullCursor(underlying.withEnv(env)) override def isDefined: Result[Boolean] = false.success override def asNullable: Result[Option[Cursor]] = None.success } - case class DeferredCursor(context: Context, parent: Option[Cursor], env: Env, deferredPath: List[String], mkCursor: (Context, Cursor) => Result[Cursor]) extends AbstractCursor { + case class DeferredCursor( + context: Context, + parent: Option[Cursor], + env: Env, + deferredPath: List[String], + mkCursor: (Context, Cursor) => Result[Cursor]) + extends AbstractCursor { def focus: Any = Result.internalError(s"Empty cursor has no focus") def withEnv(env0: Env): DeferredCursor = copy(env = env.add(env0)) override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = - if(fieldName != deferredPath.head) Result.internalError(s"No field '$fieldName' for type ${tpe.underlying}") + if (fieldName != deferredPath.head) + Result.internalError(s"No field '$fieldName' for type ${tpe.underlying}") else for { fieldContext <- context.forField(fieldName, resultName) - cursor <- if(path.sizeCompare(1) > 0) DeferredCursor(fieldContext, Some(this), env, deferredPath.tail, mkCursor).success - else mkCursor(fieldContext, this) + cursor <- + if (path.sizeCompare(1) > 0) + DeferredCursor(fieldContext, Some(this), env, deferredPath.tail, mkCursor).success + else mkCursor(fieldContext, this) } yield cursor } @@ -350,16 +403,15 @@ object Cursor { } /** - * Context represents a position in the output tree in terms of, - * 1) the path through the schema to the position - * 2) the path through the schema with query aliases applied - * 3) the type of the element at the position - */ + * Context represents a position in the output tree in terms of, 1) the path through the schema + * to the position 2) the path through the schema with query aliases applied 3) the type of the + * element at the position + */ case class Context( - rootTpe: Type, - path: List[String], - resultPath: List[String], - typePath: List[Type] + rootTpe: Type, + path: List[String], + resultPath: List[String], + typePath: List[Type] ) { lazy val tpe: Type = typePath.headOption.getOrElse(rootTpe) @@ -373,18 +425,30 @@ case class Context( def isRoot: Boolean = path.isEmpty def parent: Option[Context] = - if(path.isEmpty) None + if (path.isEmpty) None else Some(copy(path = path.tail, resultPath = resultPath.tail, typePath = typePath.tail)) def forField(fieldName: String, resultName: String): Result[Context] = - tpe.underlyingField(fieldName).map { fieldTpe => - copy(path = fieldName :: path, resultPath = resultName :: resultPath, typePath = fieldTpe :: typePath) - }.toResult(s"No field '$fieldName' for type ${tpe.underlying}") + tpe + .underlyingField(fieldName) + .map { fieldTpe => + copy( + path = fieldName :: path, + resultPath = resultName :: resultPath, + typePath = fieldTpe :: typePath) + } + .toResult(s"No field '$fieldName' for type ${tpe.underlying}") def forField(fieldName: String, resultName: Option[String]): Result[Context] = - tpe.underlyingField(fieldName).map { fieldTpe => - copy(path = fieldName :: path, resultPath = resultName.getOrElse(fieldName) :: resultPath, typePath = fieldTpe :: typePath) - }.toResult(s"No field '$fieldName' for type ${tpe.underlying}") + tpe + .underlyingField(fieldName) + .map { fieldTpe => + copy( + path = fieldName :: path, + resultPath = resultName.getOrElse(fieldName) :: resultPath, + typePath = fieldTpe :: typePath) + } + .toResult(s"No field '$fieldName' for type ${tpe.underlying}") def forPath(path1: List[String]): Result[Context] = path1 match { @@ -394,7 +458,10 @@ case class Context( def forFieldOrAttribute(fieldName: String, resultName: Option[String]): Context = { val fieldTpe = tpe.underlyingField(fieldName).getOrElse(ScalarType.AttributeType) - copy(path = fieldName :: path, resultPath = resultName.getOrElse(fieldName) :: resultPath, typePath = fieldTpe :: typePath) + copy( + path = fieldName :: path, + resultPath = resultName.getOrElse(fieldName) :: resultPath, + typePath = fieldTpe :: typePath) } def forUnderlyingNamed: Context = @@ -417,14 +484,19 @@ object Context { def apply(rootTpe: Type, fieldName: String, resultName: Option[String]): Option[Context] = { for { tpe <- rootTpe.underlyingField(fieldName) - } yield new Context(rootTpe, List(fieldName), List(resultName.getOrElse(fieldName)), List(tpe)) + } yield new Context( + rootTpe, + List(fieldName), + List(resultName.getOrElse(fieldName)), + List(tpe)) } def apply(rootTpe: Type): Context = Context(rootTpe, Nil, Nil, Nil) def apply(path: Path): Result[Context] = - path.path.foldLeftM(Context(path.rootTpe, Nil, Nil, Nil)) { case (acc, elem) => - acc.forField(elem, None) + path.path.foldLeftM(Context(path.rootTpe, Nil, Nil, Nil)) { + case (acc, elem) => + acc.forField(elem, None) } } @@ -463,10 +535,10 @@ object Env { } case class NonEmptyEnv(elems: Map[String, Any]) extends Env { - def add[T](items: (String, T)*): Env = NonEmptyEnv(elems++items) + def add[T](items: (String, T)*): Env = NonEmptyEnv(elems ++ items) def add(other: Env): Env = other match { case EmptyEnv => this - case NonEmptyEnv(elems0) => NonEmptyEnv(elems++elems0) + case NonEmptyEnv(elems0) => NonEmptyEnv(elems ++ elems0) } def contains(name: String): Boolean = elems.contains(name) def get[T: ClassTag](name: String): Option[T] = diff --git a/modules/core/src/main/scala/introspection.scala b/modules/core/src/main/scala/introspection.scala index dc9695c7..18fc1102 100644 --- a/modules/core/src/main/scala/introspection.scala +++ b/modules/core/src/main/scala/introspection.scala @@ -17,7 +17,7 @@ package grackle import io.circe.Encoder -import ScalarType._ +import grackle.ScalarType._ object Introspection { val schema = @@ -137,13 +137,13 @@ object Introspection { val QueryType = schema.uncheckedRef(schema.queryType) val __SchemaType = schema.ref("__Schema") - val __TypeType = schema.ref("__Type") - val __FieldType = schema.ref("__Field") - val __InputValueType = schema.ref("__InputValue") - val __EnumValueType = schema.ref("__EnumValue") - val __DirectiveType = schema.ref("__Directive") - val __TypeKindType = schema.ref("__TypeKind") - val __DirectiveLocationType = schema.ref("__DirectiveLocation") + val __TypeType = schema.ref("__Type") + val __FieldType = schema.ref("__Field") + val __InputValueType = schema.ref("__InputValue") + val __EnumValueType = schema.ref("__EnumValue") + val __DirectiveType = schema.ref("__Directive") + val __TypeKindType = schema.ref("__TypeKind") + val __DirectiveLocationType = schema.ref("__DirectiveLocation") object TypeKind extends Enumeration { val SCALAR, OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT, LIST, NON_NULL = Value @@ -154,25 +154,25 @@ object Introspection { import Ast.DirectiveLocation._ Encoder[String].contramap { - case QUERY => "QUERY" - case MUTATION => "MUTATION" - case SUBSCRIPTION => "SUBSCRIPTION" - case FIELD => "FIELD" - case FRAGMENT_DEFINITION => "FRAGMENT_DEFINITION" - case FRAGMENT_SPREAD => "FRAGMENT_SPREAD" - case INLINE_FRAGMENT => "INLINE_FRAGMENT" - case VARIABLE_DEFINITION => "VARIABLE_DEFINITION" - - case SCHEMA => "SCHEMA" - case SCALAR => "SCALAR" - case OBJECT => "OBJECT" - case FIELD_DEFINITION => "FIELD_DEFINITION" - case ARGUMENT_DEFINITION => "ARGUMENT_DEFINITION" - case INTERFACE => "INTERFACE" - case UNION => "UNION" - case ENUM => "ENUM" - case ENUM_VALUE => "ENUM_VALUE" - case INPUT_OBJECT => "INPUT_OBJECT" + case QUERY => "QUERY" + case MUTATION => "MUTATION" + case SUBSCRIPTION => "SUBSCRIPTION" + case FIELD => "FIELD" + case FRAGMENT_DEFINITION => "FRAGMENT_DEFINITION" + case FRAGMENT_SPREAD => "FRAGMENT_SPREAD" + case INLINE_FRAGMENT => "INLINE_FRAGMENT" + case VARIABLE_DEFINITION => "VARIABLE_DEFINITION" + + case SCHEMA => "SCHEMA" + case SCALAR => "SCALAR" + case OBJECT => "OBJECT" + case FIELD_DEFINITION => "FIELD_DEFINITION" + case ARGUMENT_DEFINITION => "ARGUMENT_DEFINITION" + case INTERFACE => "INTERFACE" + case UNION => "UNION" + case ENUM => "ENUM" + case ENUM_VALUE => "ENUM_VALUE" + case INPUT_OBJECT => "INPUT_OBJECT" case INPUT_FIELD_DEFINITION => "INPUT_FIELD_DEFINITION" } } @@ -181,12 +181,12 @@ object Introspection { val flipNullityDealias: PartialFunction[Type, Any] = { case NullableType(tpe) => tpe.dealias - case tpe => NonNullType(tpe) + case tpe => NonNullType(tpe) } val defaultTypes = schema.types.filterNot(_ =:= schema.queryType) ++ - List(BooleanType, IntType, FloatType, StringType, IDType) + List(BooleanType, IntType, FloatType, StringType, IDType) def interpreter(targetSchema: Schema): QueryInterpreter[Either[Throwable, *]] = new IntrospectionMapping(targetSchema).interpreter @@ -210,58 +210,82 @@ object Introspection { ValueField("directives", _.directives) ), ValueObjectMapping(__TypeType).on[Type]( - ValueField("kind", flipNullityDealias andThen { - case _: ScalarType => TypeKind.SCALAR - case _: ObjectType => TypeKind.OBJECT - case _: UnionType => TypeKind.UNION - case _: InterfaceType => TypeKind.INTERFACE - case _: EnumType => TypeKind.ENUM - case _: InputObjectType => TypeKind.INPUT_OBJECT - case _: ListType => TypeKind.LIST - case _: NonNullType => TypeKind.NON_NULL - }), - ValueField("name", flipNullityDealias andThen { - case nt: NamedType => Some(nt.name) - case _ => None - }), - ValueField("description", flipNullityDealias andThen { - case nt: NamedType => nt.description - case _ => None - }), - ValueField("specifiedByURL", flipNullityDealias andThen { - case s: ScalarType => s.specifiedByURL - case _ => None - }), - ValueField("fields", flipNullityDealias andThen { - case tf: TypeWithFields => Some(tf.fields) - case _ => None - }), - ValueField("interfaces", flipNullityDealias andThen { - case tf: TypeWithFields => Some(tf.interfaces.map(_.nullable)) - case _ => None - }), - ValueField("possibleTypes", flipNullityDealias andThen { - case u: UnionType => Some(u.members.map(_.nullable)) - case i: InterfaceType => Some(targetSchema.implementations(i).map(_.nullable)) - case _ => None - }), - ValueField("enumValues", flipNullityDealias andThen { - case e: EnumType => Some(e.enumValues) - case _ => None - }), - ValueField("inputFields", flipNullityDealias andThen { - case i: InputObjectType => Some(i.inputFields) - case _ => None - }), - ValueField("ofType", flipNullityDealias andThen { - case l: ListType => Some(l.ofType) - case NonNullType(t) => Some(NullableType(t)) - case _ => None - }), - ValueField("isOneOf", flipNullityDealias andThen { - case i: InputObjectType => Some(i.isOneOf) - case _ => None - }), + ValueField( + "kind", + flipNullityDealias andThen { + case _: ScalarType => TypeKind.SCALAR + case _: ObjectType => TypeKind.OBJECT + case _: UnionType => TypeKind.UNION + case _: InterfaceType => TypeKind.INTERFACE + case _: EnumType => TypeKind.ENUM + case _: InputObjectType => TypeKind.INPUT_OBJECT + case _: ListType => TypeKind.LIST + case _: NonNullType => TypeKind.NON_NULL + } + ), + ValueField( + "name", + flipNullityDealias andThen { + case nt: NamedType => Some(nt.name) + case _ => None + }), + ValueField( + "description", + flipNullityDealias andThen { + case nt: NamedType => nt.description + case _ => None + }), + ValueField( + "specifiedByURL", + flipNullityDealias andThen { + case s: ScalarType => s.specifiedByURL + case _ => None + }), + ValueField( + "fields", + flipNullityDealias andThen { + case tf: TypeWithFields => Some(tf.fields) + case _ => None + }), + ValueField( + "interfaces", + flipNullityDealias andThen { + case tf: TypeWithFields => Some(tf.interfaces.map(_.nullable)) + case _ => None + }), + ValueField( + "possibleTypes", + flipNullityDealias andThen { + case u: UnionType => Some(u.members.map(_.nullable)) + case i: InterfaceType => Some(targetSchema.implementations(i).map(_.nullable)) + case _ => None + } + ), + ValueField( + "enumValues", + flipNullityDealias andThen { + case e: EnumType => Some(e.enumValues) + case _ => None + }), + ValueField( + "inputFields", + flipNullityDealias andThen { + case i: InputObjectType => Some(i.inputFields) + case _ => None + }), + ValueField( + "ofType", + flipNullityDealias andThen { + case l: ListType => Some(l.ofType) + case NonNullType(t) => Some(NullableType(t)) + case _ => None + }), + ValueField( + "isOneOf", + flipNullityDealias andThen { + case i: InputObjectType => Some(i.isOneOf) + case _ => None + }) ), ValueObjectMapping(__FieldType).on[Field]( ValueField("name", _.name), diff --git a/modules/core/src/main/scala/jsonextractors.scala b/modules/core/src/main/scala/jsonextractors.scala index 5c4f1ff6..4727500d 100644 --- a/modules/core/src/main/scala/jsonextractors.scala +++ b/modules/core/src/main/scala/jsonextractors.scala @@ -15,8 +15,7 @@ package grackle -import io.circe.Json -import io.circe.JsonObject +import io.circe.{Json, JsonObject} object JsonExtractor { diff --git a/modules/core/src/main/scala/mapping.scala b/modules/core/src/main/scala/mapping.scala index 371cd71e..d0ecbe1d 100644 --- a/modules/core/src/main/scala/mapping.scala +++ b/modules/core/src/main/scala/mapping.scala @@ -16,26 +16,31 @@ package grackle import scala.collection.Factory -import scala.collection.mutable.{ Map => MMap } +import scala.collection.mutable.{Map => MMap} import scala.reflect.ClassTag import cats.{ApplicativeError, Id, MonadThrow} import cats.data.{Chain, NonEmptyList, StateT} import cats.implicits._ -import fs2.{ Stream, Compiler } +import fs2.{Compiler, Stream} import io.circe.{Encoder, Json} import io.circe.syntax._ import org.tpolecat.sourcepos.SourcePos import org.tpolecat.typename._ import org.typelevel.scalaccompat.annotation._ -import syntax._ -import Cursor.{AbstractCursor, ProxyCursor} -import Query.EffectHandler -import QueryCompiler.{ComponentElaborator, EffectElaborator, IntrospectionLevel, SelectElaborator} -import QueryInterpreter.ProtoJson -import IntrospectionLevel._ -import ValidationFailure.Severity +import grackle.Cursor.{AbstractCursor, ProxyCursor} +import grackle.Query.EffectHandler +import grackle.QueryCompiler.{ + ComponentElaborator, + EffectElaborator, + IntrospectionLevel, + SelectElaborator +} +import grackle.QueryCompiler.IntrospectionLevel._ +import grackle.QueryInterpreter.ProtoJson +import grackle.ValidationFailure.Severity +import grackle.syntax._ /** * Represents a mapping between a GraphQL schema and an underlying abstract data source. @@ -46,51 +51,80 @@ abstract class Mapping[F[_]] { val typeMappings: TypeMappings /** - * Compile and run a single GraphQL query or mutation. - * - * Yields a JSON response containing the result of the query or mutation. - */ - def compileAndRun(text: String, name: Option[String] = None, untypedVars: Option[Json] = None, introspectionLevel: IntrospectionLevel = Full, reportUnused: Boolean = true, env: Env = Env.empty)( - implicit sc: Compiler[F,F] + * Compile and run a single GraphQL query or mutation. + * + * Yields a JSON response containing the result of the query or mutation. + */ + def compileAndRun( + text: String, + name: Option[String] = None, + untypedVars: Option[Json] = None, + introspectionLevel: IntrospectionLevel = Full, + reportUnused: Boolean = true, + env: Env = Env.empty)( + implicit sc: Compiler[F, F] ): F[Json] = - compileAndRunSubscription(text, name, untypedVars, introspectionLevel, reportUnused, env).compile.toList.flatMap { - case List(j) => j.pure[F] - case Nil => M.raiseError(new IllegalStateException("Result stream was empty.")) - case js => M.raiseError(new IllegalStateException(s"Result stream contained ${js.length} results; expected exactly one.")) - } + compileAndRunSubscription(text, name, untypedVars, introspectionLevel, reportUnused, env) + .compile + .toList + .flatMap { + case List(j) => j.pure[F] + case Nil => M.raiseError(new IllegalStateException("Result stream was empty.")) + case js => + M.raiseError( + new IllegalStateException( + s"Result stream contained ${js.length} results; expected exactly one.")) + } /** * Compile and run a GraphQL subscription. * * Yields a stream of JSON responses containing the results of the subscription. */ - def compileAndRunSubscription(text: String, name: Option[String] = None, untypedVars: Option[Json] = None, introspectionLevel: IntrospectionLevel = Full, reportUnused: Boolean = true, env: Env = Env.empty): Stream[F,Json] = { - val compiled = compiler.compile(text, name, untypedVars, introspectionLevel, reportUnused, env) - Stream.eval(compiled.pure[F]).flatMap(_.flatTraverse(op => interpreter.run(op.query, op.rootTpe, env))).evalMap(mkResponse) + def compileAndRunSubscription( + text: String, + name: Option[String] = None, + untypedVars: Option[Json] = None, + introspectionLevel: IntrospectionLevel = Full, + reportUnused: Boolean = true, + env: Env = Env.empty): Stream[F, Json] = { + val compiled = + compiler.compile(text, name, untypedVars, introspectionLevel, reportUnused, env) + Stream + .eval(compiled.pure[F]) + .flatMap(_.flatTraverse(op => interpreter.run(op.query, op.rootTpe, env))) + .evalMap(mkResponse) } - /** Combine and execute multiple queries. + /** + * Combine and execute multiple queries. * - * Each query is interpreted in the context of the Cursor it is - * paired with. The result list is aligned with the argument - * query list. For each query at most one stage will be run and the - * corresponding result may contain deferred components. + * Each query is interpreted in the context of the Cursor it is paired with. The result list + * is aligned with the argument query list. For each query at most one stage will be run and + * the corresponding result may contain deferred components. * - * Errors are aggregated across all the argument queries and are - * accumulated on the `Left` of the result. + * Errors are aggregated across all the argument queries and are accumulated on the `Left` of + * the result. * - * This method is typically called at the end of a stage to evaluate - * deferred subqueries in the result of that stage. These will be - * grouped by and passed jointly to the responsible mapping in - * the next stage using this method. Maappongs which are able - * to benefit from combining queries may do so by overriding this - * method to implement their specific combinging logic. + * This method is typically called at the end of a stage to evaluate deferred subqueries in + * the result of that stage. These will be grouped by and passed jointly to the responsible + * mapping in the next stage using this method. Maappongs which are able to benefit from + * combining queries may do so by overriding this method to implement their specific + * combinging logic. */ def combineAndRun(queries: List[(Query, Cursor)]): F[Result[List[ProtoJson]]] = - queries.map { case (q, c) => (q, schema.queryType, c) }.traverse((interpreter.runOneShot _).tupled).map(ProtoJson.combineResults) + queries + .map { case (q, c) => (q, schema.queryType, c) } + .traverse((interpreter.runOneShot _).tupled) + .map(ProtoJson.combineResults) - /** Yields a `Cursor` focused on the top level operation type of the query */ - def defaultRootCursor(query: Query, tpe: Type, parentCursor: Option[Cursor]): F[Result[(Query, Cursor)]] = + /** + * Yields a `Cursor` focused on the top level operation type of the query + */ + def defaultRootCursor( + query: Query, + tpe: Type, + parentCursor: Option[Cursor]): F[Result[(Query, Cursor)]] = Result((query, RootCursor(Context(tpe), parentCursor, Env.empty))).pure[F].widen /** @@ -99,7 +133,8 @@ abstract class Mapping[F[_]] { * Construction of mapping-specific cursors is handled by delegation to * `mkCursorForField which is typically overridden in `Mapping` subtypes. */ - case class RootCursor(context: Context, parent: Option[Cursor], env: Env) extends AbstractCursor { + case class RootCursor(context: Context, parent: Option[Cursor], env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) def focus: Any = () @@ -109,59 +144,70 @@ abstract class Mapping[F[_]] { } /** - * Yields a `Cursor` suitable for traversing the query result corresponding to - * the `fieldName` child of `parent`, and with the given field `Context` and - * `FieldMapping`. - * - * This method is typically overridden in and delegated to by `Mapping` subtypes. - */ - protected def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] = { + * Yields a `Cursor` suitable for traversing the query result corresponding to the `fieldName` + * child of `parent`, and with the given field `Context` and `FieldMapping`. + * + * This method is typically overridden in and delegated to by `Mapping` subtypes. + */ + protected def mkCursorForMappedField( + parent: Cursor, + fieldContext: Context, + fm: FieldMapping): Result[Cursor] = { def mkLeafCursor(focus: Any): Result[Cursor] = LeafCursor(fieldContext, focus, Some(parent), parent.env).success fm match { - case _ : EffectMapping => + case _: EffectMapping => mkLeafCursor(parent.focus) case CursorField(_, f, _, _, _) => f(parent).flatMap(res => mkLeafCursor(res)) case _ => - Result.internalError(s"Unhandled mapping of type ${fm.getClass.getName} for field '${fieldContext.path.head}' for type ${parent.tpe}") + Result.internalError( + s"Unhandled mapping of type ${fm.getClass.getName} for field '${fieldContext.path.head}' for type ${parent.tpe}") } } /** - * Yields a `Cursor` suitable for traversing the query result corresponding to - * the `fieldName` child of `parent`. - */ - protected final def mkCursorForField(parent: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = { - typeMappings.fieldMapping(parent, fieldName). - flatMap(_.toResultOrError(s"No mapping for field '$fieldName' for type ${parent.tpe}")). - flatMap { - case (np, fm) => - val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName) - mkCursorForMappedField(np, fieldContext, fm) - } + * Yields a `Cursor` suitable for traversing the query result corresponding to the `fieldName` + * child of `parent`. + */ + protected final def mkCursorForField( + parent: Cursor, + fieldName: String, + resultName: Option[String]): Result[Cursor] = { + typeMappings + .fieldMapping(parent, fieldName) + .flatMap(_.toResultOrError(s"No mapping for field '$fieldName' for type ${parent.tpe}")) + .flatMap { + case (np, fm) => + val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName) + mkCursorForMappedField(np, fieldContext, fm) + } } final class TypeMappings private ( - val mappings: Seq[TypeMapping], - typeIndex: MMap[String, TypeMapping], - predicatedTypeIndex: MMap[String, Seq[TypeMapping]], - typeFieldIndex: MMap[String, MMap[String, FieldMapping]], - predicatedTypeFieldIndex: MMap[String, Seq[(ObjectMapping, MMap[String, FieldMapping])]], - unchecked: Boolean + val mappings: Seq[TypeMapping], + typeIndex: MMap[String, TypeMapping], + predicatedTypeIndex: MMap[String, Seq[TypeMapping]], + typeFieldIndex: MMap[String, MMap[String, FieldMapping]], + predicatedTypeFieldIndex: MMap[String, Seq[(ObjectMapping, MMap[String, FieldMapping])]], + unchecked: Boolean ) { import TypeMappings.{InheritedFieldMapping, PolymorphicFieldMapping} - /** Yields the `TypeMapping` associated with the provided context, if any. */ + /** + * Yields the `TypeMapping` associated with the provided context, if any. + */ def typeMapping(context: Context): Option[TypeMapping] = { val nt = context.tpe.underlyingNamed val nme = nt.name typeIndex.get(nme).orElse { val nc = context.asType(nt) - predicatedTypeIndex.getOrElse(nme, Nil).mapFilter { tm => - tm.predicate(nc).map(prio => (prio, tm)) - }.maxByOption(_._1).map(_._2) + predicatedTypeIndex + .getOrElse(nme, Nil) + .mapFilter { tm => tm.predicate(nc).map(prio => (prio, tm)) } + .maxByOption(_._1) + .map(_._2) } } @@ -170,23 +216,29 @@ abstract class Mapping[F[_]] { val nme = nt.name typeFieldIndex.get(nme).orElse { val nc = context.asType(nt) - predicatedTypeFieldIndex.getOrElse(nme, Nil).mapFilter { tm => - tm._1.predicate(nc).map(prio => (prio, tm)) - }.maxByOption(_._1).map(_._2._2) + predicatedTypeFieldIndex + .getOrElse(nme, Nil) + .mapFilter { tm => tm._1.predicate(nc).map(prio => (prio, tm)) } + .maxByOption(_._1) + .map(_._2._2) } } - /** Yields the `ObjectMapping` associated with the provided context, if any. */ + /** + * Yields the `ObjectMapping` associated with the provided context, if any. + */ def objectMapping(context: Context): Option[ObjectMapping] = - typeMapping(context).collect { - case om: ObjectMapping => om - } + typeMapping(context).collect { case om: ObjectMapping => om } - /** Yields the unexpanded `FieldMapping` associated with `fieldName` in `context`, if any. */ + /** + * Yields the unexpanded `FieldMapping` associated with `fieldName` in `context`, if any. + */ def rawFieldMapping(context: Context, fieldName: String): Option[FieldMapping] = fieldIndex(context).flatMap(_.get(fieldName)) - /** Yields the `FieldMapping` associated with `fieldName` in `context`, if any. */ + /** + * Yields the `FieldMapping` associated with `fieldName` in `context`, if any. + */ def fieldMapping(context: Context, fieldName: String): Option[FieldMapping] = rawFieldMapping(context, fieldName).flatMap { case ifm: InheritedFieldMapping => @@ -198,10 +250,12 @@ abstract class Mapping[F[_]] { } /** - * Yields the `FieldMapping` associated with `fieldName` in the runtime context - * determined by the given `Cursor`, if any. + * Yields the `FieldMapping` associated with `fieldName` in the runtime context determined + * by the given `Cursor`, if any. */ - def fieldMapping(parent: Cursor, fieldName: String): Result[Option[(Cursor, FieldMapping)]] = { + def fieldMapping( + parent: Cursor, + fieldName: String): Result[Option[(Cursor, FieldMapping)]] = { val context = parent.context fieldIndex(context).flatMap(_.get(fieldName)) match { case Some(ifm: InheritedFieldMapping) => @@ -220,52 +274,70 @@ abstract class Mapping[F[_]] { case _ => false } - /** Yields the `FieldMapping` directly or ancestrally associated with `fieldName` in `context`, if any. */ + /** + * Yields the `FieldMapping` directly or ancestrally associated with `fieldName` in + * `context`, if any. + */ def ancestralFieldMapping(context: Context, fieldName: String): Option[FieldMapping] = fieldMapping(context, fieldName).orElse { for { parent <- context.parent - fm <- ancestralFieldMapping(parent, context.path.head) + fm <- ancestralFieldMapping(parent, context.path.head) if fm.subtree } yield fm } /** - * Validatate this Mapping, yielding a list of `ValidationFailure`s of severity equal to or greater than the - * specified `Severity`. - */ + * Validatate this Mapping, yielding a list of `ValidationFailure`s of severity equal to or + * greater than the specified `Severity`. + */ def validate(severity: Severity = Severity.Warning): List[ValidationFailure] = { val queryType = schema.schemaType.field("query").flatMap(_.nonNull.asNamed) - val topLevelContexts = (queryType.toList ::: schema.mutationType.toList ::: schema.subscriptionType.toList).map(Context(_)) + val topLevelContexts = + (queryType.toList ::: schema.mutationType.toList ::: schema.subscriptionType.toList) + .map(Context(_)) validateRoots(topLevelContexts).filter(_.severity >= severity) } /** - * Validate this mapping, raising a `ValidationException` in `F` if there are any failures of - * severity equal to or greater than the specified `Severity`. - */ + * Validate this mapping, raising a `ValidationException` in `F` if there are any failures + * of severity equal to or greater than the specified `Severity`. + */ def validateInto[G[_]](severity: Severity = Severity.Warning)( - implicit ev: ApplicativeError[G, Throwable] + implicit ev: ApplicativeError[G, Throwable] ): G[Unit] = - NonEmptyList.fromList(validate(severity)).foldMapA(nec => ev.raiseError(ValidationException(nec))) + NonEmptyList + .fromList(validate(severity)) + .foldMapA(nec => ev.raiseError(ValidationException(nec))) /** - * Validate this Mapping, throwing a `ValidationException` if there are any failures of severity equal - * to or greater than the specified `Severity`. - */ + * Validate this Mapping, throwing a `ValidationException` if there are any failures of + * severity equal to or greater than the specified `Severity`. + */ def unsafeValidate(severity: Severity = Severity.Warning): Unit = validateInto[Either[Throwable, *]](severity).fold(throw _, _ => ()) private[Mapping] def unsafeValidateIfChecked(): Unit = - if(!unchecked) unsafeValidate() + if (!unchecked) unsafeValidate() - /** Validates these type mappings against an unfolding of the schema */ + /** + * Validates these type mappings against an unfolding of the schema + */ @nowarn3 private def validateRoots(rootCtxts: List[Context]): List[ValidationFailure] = { - import TypeMappings.{MappingValidator => MV} // Bogus unused import warning with Scala 3.3.3 + import TypeMappings.{ + MappingValidator => MV + } // Bogus unused import warning with Scala 3.3.3 import MV.{ - initialState, addSeenType, addSeenTypeMapping, addSeenFieldMapping, addProblem, - addProblems, seenType, seenTypeMappings, seenFieldMappings + initialState, + addSeenType, + addSeenTypeMapping, + addSeenFieldMapping, + addProblem, + addProblems, + seenType, + seenTypeMappings, + seenFieldMappings } def allTypeMappings(context: Context): Seq[TypeMapping] = { @@ -278,9 +350,7 @@ abstract class Mapping[F[_]] { case None => Nil case Some(tms) => val nc = context.asType(nt) - tms.mapFilter { tm => - tm.predicate(nc).map(prio => (prio, tm)) - } match { + tms.mapFilter { tm => tm.predicate(nc).map(prio => (prio, tm)) } match { case Seq() => Nil case Seq((_, tm)) => List(tm) case ptms => @@ -299,57 +369,60 @@ abstract class Mapping[F[_]] { def step(context: Context): MV[List[Context]] = { lazy val hasEnclosingSubtreeFieldMapping = if (context.path.isEmpty) false - else ancestralFieldMapping(context.parent.get, context.path.head).map(_.subtree).getOrElse(false) + else + ancestralFieldMapping(context.parent.get, context.path.head) + .map(_.subtree) + .getOrElse(false) def typeChecks(om: ObjectMapping): List[ValidationFailure] = validateTypeMapping(this, context, om) ++ om.fieldMappings.reverse.flatMap(validateFieldMapping(this, context, om, _)) (context.tpe.underlyingNamed.dealias, allTypeMappings(context)) match { - case (lt@((_: ScalarType) | (_: EnumType)), List(lm: LeafMapping[_])) => + case (lt @ (_: ScalarType | _: EnumType), List(lm: LeafMapping[_])) => addSeenType(lt) *> - addSeenTypeMapping(lm) *> - MV.pure(Nil) + addSeenTypeMapping(lm) *> + MV.pure(Nil) - case (lt@((_: ScalarType) | (_: EnumType)), Nil) if hasEnclosingSubtreeFieldMapping => + case (lt @ (_: ScalarType | _: EnumType), Nil) if hasEnclosingSubtreeFieldMapping => addSeenType(lt) *> - MV.pure(Nil) + MV.pure(Nil) - case (lt@((_: ScalarType) | (_: EnumType)), List(om: ObjectMapping)) => + case (lt @ (_: ScalarType | _: EnumType), List(om: ObjectMapping)) => addSeenType(lt) *> - addSeenTypeMapping(om) *> - addProblem(ObjectTypeExpected(om)) *> - MV.pure(Nil) + addSeenTypeMapping(om) *> + addProblem(ObjectTypeExpected(om)) *> + MV.pure(Nil) case (ut: UnionType, List(om: ObjectMapping)) => val vs = ut.members.map(context.asType) addSeenType(ut) *> - addSeenTypeMapping(om) *> - addProblems(typeChecks(om)) *> - MV.pure(vs) + addSeenTypeMapping(om) *> + addProblems(typeChecks(om)) *> + MV.pure(vs) case (ut: UnionType, List(lm: LeafMapping[_])) => val vs = ut.members.map(context.asType) addSeenType(ut) *> - addSeenTypeMapping(lm) *> - addProblem(LeafTypeExpected(lm)) *> - MV.pure(vs) + addSeenTypeMapping(lm) *> + addProblem(LeafTypeExpected(lm)) *> + MV.pure(vs) case (ut: UnionType, Nil) => val vs = ut.members.map(context.asType) addSeenType(ut) *> - MV.pure(vs) + MV.pure(vs) case (twf: TypeWithFields, tms) if tms.sizeCompare(1) <= 0 => def objectCheck(seen: Boolean) = (twf, tms) match { case (_, List(om: ObjectMapping)) => addSeenTypeMapping(om) *> - addProblems(typeChecks(om)).whenA(!seen) + addProblems(typeChecks(om)).whenA(!seen) case (_, List(lm: LeafMapping[_])) => addSeenTypeMapping(lm) *> - addProblem(LeafTypeExpected(lm)) + addProblem(LeafTypeExpected(lm)) case (_: InterfaceType, Nil) => MV.unit @@ -372,7 +445,7 @@ abstract class Mapping[F[_]] { val v = context.asType(it) for { seen <- seenType(v.tpe.underlyingNamed) - } yield if(seen) None else Some(v) + } yield if (seen) None else Some(v) } val fieldNames = twf.fields.map(_.name) @@ -381,33 +454,34 @@ abstract class Mapping[F[_]] { val fctx = context.forFieldOrAttribute(fieldName, None).forUnderlyingNamed lazy val allImplsHaveFieldMapping = implCtxts.nonEmpty && - implCtxts.forall { implCtxt => - objectMapping(implCtxt).exists { om => - om.fieldMapping(fieldName).isDefined + implCtxts.forall { implCtxt => + objectMapping(implCtxt).exists { om => + om.fieldMapping(fieldName).isDefined + } } - } ((ancestralFieldMapping(context, fieldName), tms) match { case (Some(fm), List(om: ObjectMapping)) if !allImplsHaveFieldMapping => addProblem(DeclaredFieldMappingIsHidden(om, fm)).whenA(fm.hidden) *> - addSeenFieldMapping(om, fm) + addSeenFieldMapping(om, fm) - case (None, List(om: ObjectMapping)) if !(hasEnclosingSubtreeFieldMapping || allImplsHaveFieldMapping) => + case (None, List(om: ObjectMapping)) + if !(hasEnclosingSubtreeFieldMapping || allImplsHaveFieldMapping) => val field = context.tpe.fieldInfo(fieldName).get addProblem(MissingFieldMapping(om, field)) case _ => // Other errors will have been reported earlier MV.unit }) *> - seenType(fctx.tpe).map(if(_) None else Some(fctx)) + seenType(fctx.tpe).map(if (_) None else Some(fctx)) } for { - seen <- seenType(twf) - _ <- addSeenType(twf) - _ <- objectCheck(seen) + seen <- seenType(twf) + _ <- addSeenType(twf) + _ <- objectCheck(seen) ifCtxts <- twf.interfaces.traverseFilter(interfaceContext) - fCtxts <- fieldNames.traverseFilter(fieldCheck) + fCtxts <- fieldNames.traverseFilter(fieldCheck) pfCtxts <- MV.pure(if (!seen) allPrefixedMatchContexts(context) else Nil) } yield implCtxts ++ ifCtxts ++ fCtxts ++ pfCtxts @@ -416,7 +490,7 @@ abstract class Mapping[F[_]] { case (_, Nil) => addProblem(MissingTypeMapping(context)) *> - MV.pure(Nil) + MV.pure(Nil) case (_, tms) => (tms.traverse_ { // Suppress false positive follow on errors @@ -428,8 +502,8 @@ abstract class Mapping[F[_]] { case tm => addSeenTypeMapping(tm) }) *> - addProblem(AmbiguousTypeMappings(context, tms)) *> - MV.pure(Nil) + addProblem(AmbiguousTypeMappings(context, tms)) *> + MV.pure(Nil) } } @@ -445,46 +519,51 @@ abstract class Mapping[F[_]] { def unseenTypeMappings(seen: Set[TypeMapping]): Seq[TypeMapping] = { @annotation.tailrec def loop(pending: Seq[TypeMapping], acc: Seq[TypeMapping]): Seq[TypeMapping] = - pending match { - case Seq(om: ObjectMapping, tail @ _*) => - if (seen(om) || om.fieldMappings.forall { case _: Delegate => true ; case _ => false }) + pending match { + case Seq(om: ObjectMapping, tail @ _*) => + if (seen(om) || om.fieldMappings.forall { + case _: Delegate => true; case _ => false + }) + loop(tail, acc) + else + loop(tail, om +: acc) + + case Seq(tm, tail @ _*) if seen(tm) => loop(tail, acc) - else - loop(tail, om +: acc) - case Seq(tm, tail @ _*) if seen(tm) => - loop(tail, acc) + case Seq(tm, tail @ _*) => + loop(tail, tm +: acc) - case Seq(tm, tail @ _*) => - loop(tail, tm +: acc) - - case _ => acc.reverse - } + case _ => acc.reverse + } loop(mappings, Nil) } def refChecks(tm: TypeMapping): MV[Unit] = { addProblem(ReferencedTypeDoesNotExist(tm)).whenA(!tm.tpe.exists) *> - (tm match { - case om: ObjectMapping if !om.tpe.dealias.isUnion => - for { - sfms <- seenFieldMappings(om) - usfms = om.fieldMappings.filterNot { case (_: Delegate) => true ; case fm => fm.hidden || sfms(fm) } - _ <- usfms.traverse_(fm => addProblem(UnusedFieldMapping(om, fm))) - } yield () - case _ => - MV.unit - }) + (tm match { + case om: ObjectMapping if !om.tpe.dealias.isUnion => + for { + sfms <- seenFieldMappings(om) + usfms = om.fieldMappings.filterNot { + case _: Delegate => true; case fm => fm.hidden || sfms(fm) + } + _ <- usfms.traverse_(fm => addProblem(UnusedFieldMapping(om, fm))) + } yield () + case _ => + MV.unit + }) } val res = for { - _ <- addProblem(MissingTypeMapping(Context(schema.uncheckedRef("Query")))).whenA(schema.schemaType.field("query").isEmpty) - _ <- validateAll(rootCtxts) + _ <- addProblem(MissingTypeMapping(Context(schema.uncheckedRef("Query")))) + .whenA(schema.schemaType.field("query").isEmpty) + _ <- validateAll(rootCtxts) seen <- seenTypeMappings - _ <- unseenTypeMappings(seen).traverse_(tm => addProblem(UnusedTypeMapping(tm))) - _ <- mappings.traverse_(refChecks) + _ <- unseenTypeMappings(seen).traverse_(tm => addProblem(UnusedTypeMapping(tm))) + _ <- mappings.traverse_(refChecks) } yield () res.runS(initialState).problems.reverse @@ -512,30 +591,45 @@ abstract class Mapping[F[_]] { Seq( LeafMapping[String](ScalarType.StringType), - LeafMapping.DefaultLeafMapping[Any](MappingPredicate.TypeMatch(ScalarType.IntType), intTypeEncoder, typeName[Int]), - LeafMapping.DefaultLeafMapping[Any](MappingPredicate.TypeMatch(ScalarType.FloatType), floatTypeEncoder, typeName[Float]), + LeafMapping.DefaultLeafMapping[Any]( + MappingPredicate.TypeMatch(ScalarType.IntType), + intTypeEncoder, + typeName[Int]), + LeafMapping.DefaultLeafMapping[Any]( + MappingPredicate.TypeMatch(ScalarType.FloatType), + floatTypeEncoder, + typeName[Float]), LeafMapping[Boolean](ScalarType.BooleanType), LeafMapping[String](ScalarType.IDType) ) } - /** A synthetic field mapping representing a field mapped in an implemented interface */ - case class InheritedFieldMapping(candidates: Seq[(MappingPredicate, FieldMapping)])(implicit val pos: SourcePos) extends FieldMapping { + /** + * A synthetic field mapping representing a field mapped in an implemented interface + */ + case class InheritedFieldMapping(candidates: Seq[(MappingPredicate, FieldMapping)])( + implicit val pos: SourcePos) + extends FieldMapping { def fieldName: String = candidates.head._2.fieldName def hidden: Boolean = false def subtree: Boolean = false def select(context: Context): Option[FieldMapping] = { val applicable = - candidates.mapFilter { case (pred, fm) => - pred(context.asType(pred.tpe)).map(prio => (prio, fm)) + candidates.mapFilter { + case (pred, fm) => + pred(context.asType(pred.tpe)).map(prio => (prio, fm)) } applicable.maxByOption(_._1).map(_._2) } } - /** A synthetic field mapping representing a field mapped by one or more implementing types */ - case class PolymorphicFieldMapping(candidates: Seq[(MappingPredicate, FieldMapping)])(implicit val pos: SourcePos) extends FieldMapping { + /** + * A synthetic field mapping representing a field mapped by one or more implementing types + */ + case class PolymorphicFieldMapping(candidates: Seq[(MappingPredicate, FieldMapping)])( + implicit val pos: SourcePos) + extends FieldMapping { def fieldName: String = candidates.head._2.fieldName def hidden: Boolean = false def subtree: Boolean = false @@ -547,7 +641,7 @@ abstract class Mapping[F[_]] { cursor.narrowsTo(schema.uncheckedRef(pred.tpe)).flatMap { narrows => if (narrows) for { - nc <- cursor.narrow(schema.uncheckedRef(pred.tpe)) + nc <- cursor.narrow(schema.uncheckedRef(pred.tpe)) } yield pred(nc.context).map(prio => (prio, (nc, fm))) else None.success @@ -558,15 +652,17 @@ abstract class Mapping[F[_]] { def select(context: Context): Option[FieldMapping] = { val applicable = - candidates.mapFilter { case (pred, fm) => - pred(context).map(prio => (prio, fm)) + candidates.mapFilter { + case (pred, fm) => + pred(context).map(prio => (prio, fm)) } applicable.maxByOption(_._1).map(_._2) } } private def mkMappings(mappings: Seq[TypeMapping], unchecked: Boolean): TypeMappings = { - val groupedMappings = (mappings ++ builtinLeafMappings).groupBy(_.tpe.underlyingNamed.name) + val groupedMappings = + (mappings ++ builtinLeafMappings).groupBy(_.tpe.underlyingNamed.name) val (typeIndex0, predicatedTypeIndex0) = groupedMappings.partitionMap { case (nme, tms) if tms.sizeCompare(1) == 0 => Left(nme -> tms.head) @@ -577,76 +673,89 @@ abstract class Mapping[F[_]] { val predicatedTypeIndex = MMap.from(predicatedTypeIndex0) val interfaceMappingIndex = - MMap.from(mappings.collect { case om: ObjectMapping if om.tpe.dealias.isInterface => om.tpe.name -> om }) - - val objectFieldIndices = mappings.collect { - case om: ObjectMapping if om.tpe.dealias.isObject => - om.tpe.underlying match { - case o: ObjectType if o.interfaces.nonEmpty => - val ims = o.interfaces.flatMap { i => interfaceMappingIndex.get(i.name).collect { case im: ObjectMapping => im } } - val allFieldsAndAttrs = (om.fieldMappings.map(_.fieldName) ++ ims.flatMap(_.fieldMappings.map(_.fieldName)) ++ o.fields.map(_.name)).distinct - val newFields = - allFieldsAndAttrs.flatMap { fnme => - om.fieldMapping(fnme).map(Seq(_)).getOrElse { - val candidates = - ims.flatMap { im => - im.fieldMapping(fnme).map { fm => (im.predicate, fm) } - } + MMap.from(mappings.collect { + case om: ObjectMapping if om.tpe.dealias.isInterface => om.tpe.name -> om + }) - if (candidates.isEmpty) Seq.empty - else Seq(InheritedFieldMapping(candidates)) - } + val objectFieldIndices = mappings + .collect { + case om: ObjectMapping if om.tpe.dealias.isObject => + om.tpe.underlying match { + case o: ObjectType if o.interfaces.nonEmpty => + val ims = o.interfaces.flatMap { i => + interfaceMappingIndex.get(i.name).collect { case im: ObjectMapping => im } } - val index = MMap.from((newFields).map(fm => fm.fieldName -> fm)) - (om, index) - - case _ => - val index = MMap.from(om.fieldMappings.map(fm => fm.fieldName -> fm)) - (om, index) - } - }.groupBy(_._1.tpe.underlyingNamed.name) - - val nonObjectFieldIndices = - mappings.collect { - case im: ObjectMapping if !im.tpe.dealias.isObject => - im.tpe.underlying match { - case i: InterfaceType => - val impls = schema.implementations(i) - val ms = impls.flatMap(impl => objectFieldIndices.getOrElse(impl.name, Seq.empty)) - val allFieldsAndAttrs = (im.fieldMappings.map(_.fieldName) ++ i.fields.map(_.name)).distinct + val allFieldsAndAttrs = (om.fieldMappings.map(_.fieldName) ++ ims.flatMap( + _.fieldMappings.map(_.fieldName)) ++ o.fields.map(_.name)).distinct val newFields = allFieldsAndAttrs.flatMap { fnme => - val candidates: Seq[(MappingPredicate, FieldMapping)] = - ms.flatMap { case (om, ofi) => - ofi.get(fnme).toSeq.flatMap { - case InheritedFieldMapping(ifms) => ifms.filterNot { case (p, _) => p.tpe =:= i } - case fm => Seq((om.predicate, fm)) + om.fieldMapping(fnme).map(Seq(_)).getOrElse { + val candidates = + ims.flatMap { im => + im.fieldMapping(fnme).map { fm => (im.predicate, fm) } } - } - val dfm = im.fieldMapping(fnme).toSeq - if (candidates.isEmpty) dfm - else { - val dfm0 = dfm.map(ifm => (im.predicate, ifm)) - val (tps, ifs) = - (dfm0 ++ candidates).partitionMap { - case pfm@(p, _) => - if (p.tpe.dealias.isInterface) Right(pfm) - else Left(pfm) - } - val cands = tps ++ ifs - Seq(PolymorphicFieldMapping(cands)) + if (candidates.isEmpty) Seq.empty + else Seq(InheritedFieldMapping(candidates)) } } - - val index = MMap.from((newFields).map(fm => fm.fieldName -> fm)) - (im, index) + val index = MMap.from(newFields.map(fm => fm.fieldName -> fm)) + (om, index) case _ => - val index = MMap.from(im.fieldMappings.map(fm => fm.fieldName -> fm)) - (im, index) + val index = MMap.from(om.fieldMappings.map(fm => fm.fieldName -> fm)) + (om, index) } - }.groupBy(_._1.tpe.underlyingNamed.name) + } + .groupBy(_._1.tpe.underlyingNamed.name) + + val nonObjectFieldIndices = + mappings + .collect { + case im: ObjectMapping if !im.tpe.dealias.isObject => + im.tpe.underlying match { + case i: InterfaceType => + val impls = schema.implementations(i) + val ms = + impls.flatMap(impl => objectFieldIndices.getOrElse(impl.name, Seq.empty)) + val allFieldsAndAttrs = + (im.fieldMappings.map(_.fieldName) ++ i.fields.map(_.name)).distinct + val newFields = + allFieldsAndAttrs.flatMap { fnme => + val candidates: Seq[(MappingPredicate, FieldMapping)] = + ms.flatMap { + case (om, ofi) => + ofi.get(fnme).toSeq.flatMap { + case InheritedFieldMapping(ifms) => + ifms.filterNot { case (p, _) => p.tpe =:= i } + case fm => Seq((om.predicate, fm)) + } + } + + val dfm = im.fieldMapping(fnme).toSeq + if (candidates.isEmpty) dfm + else { + val dfm0 = dfm.map(ifm => (im.predicate, ifm)) + val (tps, ifs) = + (dfm0 ++ candidates).partitionMap { + case pfm @ (p, _) => + if (p.tpe.dealias.isInterface) Right(pfm) + else Left(pfm) + } + val cands = tps ++ ifs + Seq(PolymorphicFieldMapping(cands)) + } + } + + val index = MMap.from(newFields.map(fm => fm.fieldName -> fm)) + (im, index) + + case _ => + val index = MMap.from(im.fieldMappings.map(fm => fm.fieldName -> fm)) + (im, index) + } + } + .groupBy(_._1.tpe.underlyingNamed.name) val (typeFieldIndex0, predicatedTypeFieldIndex0) = (objectFieldIndices ++ nonObjectFieldIndices).partitionMap { @@ -657,7 +766,13 @@ abstract class Mapping[F[_]] { val typeFieldIndex = MMap.from(typeFieldIndex0) val predicatedTypeFieldIndex = MMap.from(predicatedTypeFieldIndex0) - new TypeMappings(mappings, typeIndex, predicatedTypeIndex, typeFieldIndex, predicatedTypeFieldIndex, unchecked) + new TypeMappings( + mappings, + typeIndex, + predicatedTypeIndex, + typeFieldIndex, + predicatedTypeFieldIndex, + unchecked) } def apply(mappings: Seq[TypeMapping]): TypeMappings = @@ -680,7 +795,8 @@ abstract class Mapping[F[_]] { def unsafe(mappings: TypeMapping*)(implicit dummy: DummyImplicit): TypeMappings = unchecked(mappings) - implicit def fromList(mappings: List[TypeMappingCompat]): TypeMappings = TypeMappings(mappings.flatMap(_.unwrap)) + implicit def fromList(mappings: List[TypeMappingCompat]): TypeMappings = TypeMappings( + mappings.flatMap(_.unwrap)) val empty: TypeMappings = unchecked(Nil) @@ -693,16 +809,19 @@ abstract class Mapping[F[_]] { def addProblems(ps: List[ValidationFailure]): MV[Unit] = StateT.modify(_.addProblems(ps)) def seenType(tpe: Type): MV[Boolean] = StateT.inspect(_.seenType(tpe)) def addSeenType(tpe: Type): MV[Unit] = StateT.modify(_.addSeenType(tpe)) - def addSeenTypeMapping(tm: TypeMapping): MV[Unit] = StateT.modify(_.addSeenTypeMapping(tm)) + def addSeenTypeMapping(tm: TypeMapping): MV[Unit] = + StateT.modify(_.addSeenTypeMapping(tm)) def seenTypeMappings: MV[Set[TypeMapping]] = StateT.inspect(_.seenTypeMappings) - def addSeenFieldMapping(om: ObjectMapping, fm: FieldMapping): MV[Unit] = StateT.modify(_.addSeenFieldMapping(om, fm)) - def seenFieldMappings(om: ObjectMapping): MV[Set[FieldMapping]] = StateT.inspect(_.seenFieldMappings(om)) + def addSeenFieldMapping(om: ObjectMapping, fm: FieldMapping): MV[Unit] = + StateT.modify(_.addSeenFieldMapping(om, fm)) + def seenFieldMappings(om: ObjectMapping): MV[Set[FieldMapping]] = + StateT.inspect(_.seenFieldMappings(om)) case class State( - seenTypes: Set[String], - seenTypeMappings: Set[TypeMapping], - seenFieldMappings0: Map[ObjectMapping, Set[FieldMapping]], - problems: List[ValidationFailure] + seenTypes: Set[String], + seenTypeMappings: Set[TypeMapping], + seenFieldMappings0: Map[ObjectMapping, Set[FieldMapping]], + problems: List[ValidationFailure] ) { def addProblem(p: ValidationFailure): State = copy(problems = p :: problems) @@ -723,7 +842,10 @@ abstract class Mapping[F[_]] { copy(seenTypeMappings = seenTypeMappings + tm) def addSeenFieldMapping(om: ObjectMapping, fm: FieldMapping): State = - copy(seenTypeMappings = seenTypeMappings + om, seenFieldMappings0 = seenFieldMappings0.updatedWith(om)(_.map(_ + fm).orElse(Set(fm).some))) + copy( + seenTypeMappings = seenTypeMappings + om, + seenFieldMappings0 = + seenFieldMappings0.updatedWith(om)(_.map(_ + fm).orElse(Set(fm).some))) def seenFieldMappings(om: ObjectMapping): Set[FieldMapping] = seenFieldMappings0.getOrElse(om, Set.empty) @@ -734,16 +856,26 @@ abstract class Mapping[F[_]] { } } + /** + * Check Mapping specific TypeMapping validity + */ + protected def validateTypeMapping( + mappings: TypeMappings, + context: Context, + tm: TypeMapping): List[ValidationFailure] = Nil - /** Check Mapping specific TypeMapping validity */ - protected def validateTypeMapping(mappings: TypeMappings, context: Context, tm: TypeMapping): List[ValidationFailure] = Nil - - /** Check Mapping specific FieldMapping validity */ - protected def validateFieldMapping(mappings: TypeMappings, context: Context, om: ObjectMapping, fm: FieldMapping): List[ValidationFailure] = Nil + /** + * Check Mapping specific FieldMapping validity + */ + protected def validateFieldMapping( + mappings: TypeMappings, + context: Context, + om: ObjectMapping, + fm: FieldMapping): List[ValidationFailure] = Nil /** - * Validatate this Mapping, yielding a chain of `Failure`s of severity equal to or greater than the - * specified `Severity`. + * Validatate this Mapping, yielding a chain of `Failure`s of severity equal to or greater + * than the specified `Severity`. */ def validate(severity: Severity = Severity.Warning): List[ValidationFailure] = typeMappings.validate(severity) @@ -753,29 +885,31 @@ abstract class Mapping[F[_]] { * severity equal to or greater than the specified `Severity`. */ def validateInto[G[_]](severity: Severity = Severity.Warning)( - implicit ev: ApplicativeError[G, Throwable] + implicit ev: ApplicativeError[G, Throwable] ): G[Unit] = typeMappings.validateInto(severity)(ev) /** - * Validate this Mapping, throwing a `ValidationException` if there are any failures of severity equal - * to or greater than the specified `Severity`. + * Validate this Mapping, throwing a `ValidationException` if there are any failures of + * severity equal to or greater than the specified `Severity`. */ def unsafeValidate(severity: Severity = Severity.Warning): Unit = typeMappings.unsafeValidate(severity) - /** Yields the `RootEffect`, if any, associated with `fieldName`. */ + /** + * Yields the `RootEffect`, if any, associated with `fieldName`. + */ def rootEffect(context: Context, fieldName: String): Option[RootEffect] = - typeMappings.fieldMapping(context, fieldName).collect { - case re: RootEffect => re - } + typeMappings.fieldMapping(context, fieldName).collect { case re: RootEffect => re } - /** Yields the `RootStream`, if any, associated with `fieldName`. */ + /** + * Yields the `RootStream`, if any, associated with `fieldName`. + */ def rootStream(context: Context, fieldName: String): Option[RootStream] = - typeMappings.fieldMapping(context, fieldName).collect { - case rs: RootStream => rs - } + typeMappings.fieldMapping(context, fieldName).collect { case rs: RootStream => rs } - /** Yields the `Encoder` associated with the provided leaf context, if any. */ + /** + * Yields the `Encoder` associated with the provided leaf context, if any. + */ def encoderForLeaf(context: Context): Option[Encoder[Any]] = typeMappings.typeMapping(context).collect { case lm: LeafMapping[_] => lm.encoder.asInstanceOf[Encoder[Any]] @@ -800,8 +934,11 @@ abstract class Mapping[F[_]] { case other => other } - /** Backwards compatible constructor for legacy `PrefixedMapping` */ - def PrefixedMapping(tpe: NamedType, mappings: List[(List[String], ObjectMapping)]): TypeMappingCompat.PrefixedMappingCompat = { + /** + * Backwards compatible constructor for legacy `PrefixedMapping` + */ + def PrefixedMapping(tpe: NamedType, mappings: List[(List[String], ObjectMapping)]) + : TypeMappingCompat.PrefixedMappingCompat = { if (!mappings.forall(_._2.predicate.tpe =:= tpe)) throw new IllegalArgumentException("All prefixed mappings must have the same type") @@ -819,15 +956,21 @@ abstract class Mapping[F[_]] { def showMappingType: String = productPrefix } - /** A predicate determining the applicability of a `TypeMapping` in a given context */ + /** + * A predicate determining the applicability of a `TypeMapping` in a given context + */ trait MappingPredicate { - /** The type to which this predicate applies */ + + /** + * The type to which this predicate applies + */ def tpe: NamedType + /** * Does this predicate apply to the given context? * - * Yields the priority of the corresponding `TypeMapping` if the predicate applies, - * or `None` otherwise. + * Yields the priority of the corresponding `TypeMapping` if the predicate applies, or + * `None` otherwise. */ def apply(ctx: Context): Option[Int] @@ -839,12 +982,13 @@ abstract class Mapping[F[_]] { } object MappingPredicate { + /** * Extend the given context by the given path, if possible, navigating through interfaces * and unions as necessary. */ def extendContext(ctx: Context, path: List[String]): Option[Context] = - if(path.isEmpty) Some(ctx) + if (path.isEmpty) Some(ctx) else ctx.tpe.underlyingNamed.dealias match { case ot: ObjectType => @@ -859,7 +1003,9 @@ abstract class Mapping[F[_]] { case Some(_) => extendContext(ctx.forFieldOrAttribute(path.head, None), path.tail) case None => - schema.implementations(it).collectFirstSome(tpe => extendContext(ctx.asType(tpe), path)) + schema + .implementations(it) + .collectFirstSome(tpe => extendContext(ctx.asType(tpe), path)) } case ut: UnionType => ut.members.collectFirstSome(tpe => extendContext(ctx.asType(tpe), path)) @@ -868,7 +1014,9 @@ abstract class Mapping[F[_]] { None } - /** A predicate that matches a specific type in any context */ + /** + * A predicate that matches a specific type in any context + */ case class TypeMatch(tpe: NamedType) extends MappingPredicate { def apply(ctx: Context): Option[Int] = if (ctx.tpe.underlyingNamed =:= tpe) @@ -888,15 +1036,13 @@ abstract class Mapping[F[_]] { /** * A predicate that matches a specific type with a given path prefix. * - * This predicate corresponds to the semantics of the `PrefixedMapping` in earlier - * releases. + * This predicate corresponds to the semantics of the `PrefixedMapping` in earlier releases. */ - case class PrefixedTypeMatch(prefix: List[String], tpe: NamedType) extends MappingPredicate { + case class PrefixedTypeMatch(prefix: List[String], tpe: NamedType) + extends MappingPredicate { def apply(ctx: Context): Option[Int] = - if ( - ctx.tpe.underlyingNamed =:= tpe && - ctx.path.startsWith(prefix.reverse) - ) + if (ctx.tpe.underlyingNamed =:= tpe && + ctx.path.startsWith(prefix.reverse)) Some(prefix.length) else None @@ -915,29 +1061,33 @@ abstract class Mapping[F[_]] { /** * A predicate that matches the given `Path` as the suffix of the context path. * - * Note that a `Path` corresponds to an initial type, followed by a sequence of - * field selectors. The type found by following the field selectors from the initial - * type determines the final type to which the predicate applies. + * Note that a `Path` corresponds to an initial type, followed by a sequence of field + * selectors. The type found by following the field selectors from the initial type + * determines the final type to which the predicate applies. * - * This predicate is thus a slightly more restrictive variant of `PrefixedTypeMatch` - * which will match in any context with the given path and final type, irrespective - * of the initial type. + * This predicate is thus a slightly more restrictive variant of `PrefixedTypeMatch` which + * will match in any context with the given path and final type, irrespective of the initial + * type. * - * In practice `PathMatch` is more convenient to use in most - * circumstances and should be preferred to `PrefixedTypeMatch` unless the semantics - * of the latter are absolutely required. + * In practice `PathMatch` is more convenient to use in most circumstances and should be + * preferred to `PrefixedTypeMatch` unless the semantics of the latter are absolutely + * required. */ case class PathMatch(path: Path) extends MappingPredicate { lazy val tpe: NamedType = path.tpe.underlyingNamed def apply(ctx: Context): Option[Int] = - if ( - ctx.tpe.underlyingNamed =:= tpe && + if (ctx.tpe.underlyingNamed =:= tpe && ctx.path.startsWith(path.path.reverse) && - ((ctx.path.lengthCompare(path.path) == 0 && ctx.rootTpe.underlyingNamed =:= path.rootTpe.underlyingNamed) || - ctx.typePath.drop(path.path.length).headOption.exists(_.underlyingNamed =:= path.rootTpe.underlyingNamed)) - ) - Some(if(path.path.isEmpty) 0 else path.path.length+1) + ((ctx.path.lengthCompare(path.path) == 0 && ctx + .rootTpe + .underlyingNamed =:= path.rootTpe.underlyingNamed) || + ctx + .typePath + .drop(path.path.length) + .headOption + .exists(_.underlyingNamed =:= path.rootTpe.underlyingNamed))) + Some(if (path.path.isEmpty) 0 else path.path.length + 1) else None @@ -945,12 +1095,13 @@ abstract class Mapping[F[_]] { * Given a Context, yield a strictly extended Context which would be matched by this * predicate, if any, None otherwise. * - * For a PathMatch predicate, the contination context is the given context - * extended by the prefix path, navigating through interfaces and unions as necessary, - * but only if the path root type matches the given context type. + * For a PathMatch predicate, the contination context is the given context extended by the + * prefix path, navigating through interfaces and unions as necessary, but only if the + * path root type matches the given context type. */ def continuationContext(ctx: Context): Option[Context] = - if(path.rootTpe.underlyingNamed =:= ctx.tpe.underlyingNamed) extendContext(ctx, path.path) + if (path.rootTpe.underlyingNamed =:= ctx.tpe.underlyingNamed) + extendContext(ctx, path.path) else None } } @@ -962,29 +1113,31 @@ abstract class Mapping[F[_]] { } object ObjectMapping { - case class DefaultObjectMapping(predicate: MappingPredicate, fieldMappings: Seq[FieldMapping])( - implicit val pos: SourcePos + case class DefaultObjectMapping( + predicate: MappingPredicate, + fieldMappings: Seq[FieldMapping])( + implicit val pos: SourcePos ) extends ObjectMapping { override def showMappingType: String = "ObjectMapping" } def apply(predicate: MappingPredicate)(fieldMappings: FieldMapping*)( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultObjectMapping(predicate, fieldMappings) def apply(tpe: NamedType)(fieldMappings: FieldMapping*)( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultObjectMapping(MappingPredicate.TypeMatch(tpe), fieldMappings) def apply(path: Path)(fieldMappings: FieldMapping*)( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultObjectMapping(MappingPredicate.PathMatch(path), fieldMappings) def apply(tpe: NamedType, fieldMappings: List[FieldMapping])( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultObjectMapping(MappingPredicate.TypeMatch(tpe), fieldMappings) } @@ -999,142 +1152,174 @@ abstract class Mapping[F[_]] { } /** - * Abstract type of field mappings with effects. - */ + * Abstract type of field mappings with effects. + */ trait EffectMapping extends FieldMapping { def subtree: Boolean = true } - case class EffectField(fieldName: String, handler: EffectHandler[F], required: List[String] = Nil, hidden: Boolean = false)(implicit val pos: SourcePos) - extends EffectMapping + case class EffectField( + fieldName: String, + handler: EffectHandler[F], + required: List[String] = Nil, + hidden: Boolean = false)(implicit val pos: SourcePos) + extends EffectMapping /** - * Root effects can perform an intial effect prior to computing the resulting - * `Cursor` and effective `Query`. + * Root effects can perform an intial effect prior to computing the resulting `Cursor` and + * effective `Query`. * - * These effects are used to perform initial effectful setup for a query or to - * perform the effect associated with a GraphQL mutation. Convenience methods - * are provided to cover the cases where only one of the query or the cursor - * are computed. + * These effects are used to perform initial effectful setup for a query or to perform the + * effect associated with a GraphQL mutation. Convenience methods are provided to cover the + * cases where only one of the query or the cursor are computed. * - * If only the query is computed the default root cursor for the mapping will - * be used. If only the cursor is computed the client query (after elaboration) - * is used unmodified ... in this case results of the performed effect can only - * be passed to the result construction stage via the environment associated - * with the returned cursor. + * If only the query is computed the default root cursor for the mapping will be used. If only + * the cursor is computed the client query (after elaboration) is used unmodified ... in this + * case results of the performed effect can only be passed to the result construction stage + * via the environment associated with the returned cursor. */ - case class RootEffect private (fieldName: String, effect: (Query, Path, Env) => F[Result[(Query, Cursor)]])(implicit val pos: SourcePos) - extends EffectMapping { + case class RootEffect private ( + fieldName: String, + effect: (Query, Path, Env) => F[Result[(Query, Cursor)]])(implicit val pos: SourcePos) + extends EffectMapping { def hidden = false - def toRootStream: RootStream = RootStream(fieldName)((q, p, e) => Stream.eval(effect(q, p, e))) + def toRootStream: RootStream = + RootStream(fieldName)((q, p, e) => Stream.eval(effect(q, p, e))) } object RootEffect { + /** - * Yields a `RootEffect` which performs both an initial effect and yields an effect-specific query and - * corresponding root cursor. + * Yields a `RootEffect` which performs both an initial effect and yields an effect-specific + * query and corresponding root cursor. */ - def apply(fieldName: String)(effect: (Query, Path, Env) => F[Result[(Query, Cursor)]])(implicit pos: SourcePos, di: DummyImplicit): RootEffect = + def apply(fieldName: String)(effect: (Query, Path, Env) => F[Result[(Query, Cursor)]])( + implicit pos: SourcePos, + di: DummyImplicit): RootEffect = new RootEffect(fieldName, effect) /** - * Yields a `RootEffect` which performs an initial effect which leaves the query and default root cursor - * unchanged. + * Yields a `RootEffect` which performs an initial effect which leaves the query and default + * root cursor unchanged. */ - def computeUnit(fieldName: String)(effect: Env => F[Result[Unit]])(implicit pos: SourcePos): RootEffect = + def computeUnit(fieldName: String)(effect: Env => F[Result[Unit]])( + implicit pos: SourcePos): RootEffect = new RootEffect( fieldName, (query, path, env) => (for { - _ <- ResultT(effect(env)) + _ <- ResultT(effect(env)) qc <- ResultT(defaultRootCursor(query, path.rootTpe, None)) } yield qc.map(_.withEnv(env))).value ) /** - * Yields a `RootEffect` which performs an initial effect and yields an effect-specific root cursor. - */ - def computeCursor(fieldName: String)(effect: (Path, Env) => F[Result[Cursor]])(implicit pos: SourcePos): RootEffect = + * Yields a `RootEffect` which performs an initial effect and yields an effect-specific root + * cursor. + */ + def computeCursor(fieldName: String)(effect: (Path, Env) => F[Result[Cursor]])( + implicit pos: SourcePos): RootEffect = new RootEffect( fieldName, (query, path, env) => effect(path, env).map(_.map(c => (query, c))) ) /** - * Yields a `RootEffect` which performs an initial effect and yields an effect-specific query - * which is executed with respect to the default root cursor for the corresponding `Mapping`. - */ - def computeChild(fieldName: String)(effect: (Query, Path, Env) => F[Result[Query]])(implicit pos: SourcePos): RootEffect = + * Yields a `RootEffect` which performs an initial effect and yields an effect-specific + * query which is executed with respect to the default root cursor for the corresponding + * `Mapping`. + */ + def computeChild(fieldName: String)(effect: (Query, Path, Env) => F[Result[Query]])( + implicit pos: SourcePos): RootEffect = new RootEffect( fieldName, (query, path, env) => (for { - child <- ResultT(Query.extractChild(query).toResultOrError("Root query has unexpected shape").pure[F]) - q <- ResultT(effect(child, path, env).map(_.flatMap(Query.substChild(query, _).toResultOrError("Root query has unexpected shape")))) - qc <- ResultT(defaultRootCursor(q, path.rootTpe, None)) + child <- ResultT( + Query + .extractChild(query) + .toResultOrError("Root query has unexpected shape") + .pure[F]) + q <- ResultT( + effect(child, path, env).map(_.flatMap( + Query.substChild(query, _).toResultOrError("Root query has unexpected shape")))) + qc <- ResultT(defaultRootCursor(q, path.rootTpe, None)) } yield qc.map(_.withEnv(env))).value ) } /** - * Root streams can perform an intial effect prior to emitting the resulting - * cursors and effective queries. + * Root streams can perform an intial effect prior to emitting the resulting cursors and + * effective queries. * - * Stream effects are used for GraphQL subscriptions. Convenience methods are - * provided to cover the cases where only one of the query or the cursor are - * computed + * Stream effects are used for GraphQL subscriptions. Convenience methods are provided to + * cover the cases where only one of the query or the cursor are computed * - * If only the query is computed the default root cursor for the mapping will - * be used. If only the cursor is computed the client query (after elaboration) - * is used unmodified ... in this case results of the performed effect can only - * be passed to the result construction stage via the environment associated - * with the returned cursor. + * If only the query is computed the default root cursor for the mapping will be used. If only + * the cursor is computed the client query (after elaboration) is used unmodified ... in this + * case results of the performed effect can only be passed to the result construction stage + * via the environment associated with the returned cursor. */ - case class RootStream private (fieldName: String, effect: (Query, Path, Env) => Stream[F, Result[(Query, Cursor)]])(implicit val pos: SourcePos) - extends EffectMapping { + case class RootStream private ( + fieldName: String, + effect: (Query, Path, Env) => Stream[F, Result[(Query, Cursor)]])( + implicit val pos: SourcePos) + extends EffectMapping { def hidden = false } object RootStream { + /** - * Yields a `RootStream` which performs both an initial effect and yields an effect-specific query and - * corresponding root cursor. + * Yields a `RootStream` which performs both an initial effect and yields an effect-specific + * query and corresponding root cursor. */ - def apply(fieldName: String)(effect: (Query, Path, Env) => Stream[F, Result[(Query, Cursor)]])(implicit pos: SourcePos, di: DummyImplicit): RootStream = + def apply(fieldName: String)( + effect: (Query, Path, Env) => Stream[F, Result[(Query, Cursor)]])( + implicit pos: SourcePos, + di: DummyImplicit): RootStream = new RootStream(fieldName, effect) /** - * Yields a `RootStream` which yields a stream of effect-specific root cursors. - * - * This form of effect is typically used to implement GraphQL subscriptions. - */ - def computeCursor(fieldName: String)(effect: (Path, Env) => Stream[F, Result[Cursor]])(implicit pos: SourcePos): RootStream = + * Yields a `RootStream` which yields a stream of effect-specific root cursors. + * + * This form of effect is typically used to implement GraphQL subscriptions. + */ + def computeCursor(fieldName: String)(effect: (Path, Env) => Stream[F, Result[Cursor]])( + implicit pos: SourcePos): RootStream = new RootStream( fieldName, (query, path, env) => effect(path, env).map(_.map(c => (query, c))) ) /** - * Yields a `RootStream` which yields a stream of effect-specific queries - * which are executed with respect to the default root cursor for the - * corresponding `Mapping`. - * - * This form of effect is typically used to implement GraphQL subscriptions. - */ - def computeChild(fieldName: String)(effect: (Query, Path, Env) => Stream[F, Result[Query]])(implicit pos: SourcePos): RootStream = + * Yields a `RootStream` which yields a stream of effect-specific queries which are executed + * with respect to the default root cursor for the corresponding `Mapping`. + * + * This form of effect is typically used to implement GraphQL subscriptions. + */ + def computeChild(fieldName: String)(effect: (Query, Path, Env) => Stream[F, Result[Query]])( + implicit pos: SourcePos): RootStream = new RootStream( fieldName, (query, path, env) => - Query.extractChild(query).fold(Stream.emit[F, Result[(Query, Cursor)]](Result.internalError("Root query has unexpected shape"))) { child => - effect(child, path, env).flatMap(child0 => - Stream.eval( - (for { - q <- ResultT(child0.flatMap(Query.substChild(query, _).toResultOrError("Root query has unexpected shape")).pure[F]) - qc <- ResultT(defaultRootCursor(q, path.rootTpe, None)) - } yield qc.map(_.withEnv(env))).value - ) - ) - } + Query + .extractChild(query) + .fold(Stream.emit[F, Result[(Query, Cursor)]]( + Result.internalError("Root query has unexpected shape"))) { child => + effect(child, path, env).flatMap(child0 => + Stream.eval( + (for { + q <- ResultT( + child0 + .flatMap(Query + .substChild(query, _) + .toResultOrError("Root query has unexpected shape")) + .pure[F]) + qc <- ResultT(defaultRootCursor(q, path.rootTpe, None)) + } yield qc.map(_.withEnv(env))).value + )) + } ) } @@ -1146,40 +1331,59 @@ abstract class Mapping[F[_]] { object LeafMapping { - case class DefaultLeafMapping[T](predicate: MappingPredicate, encoder: Encoder[T], scalaTypeName: String)( - implicit val pos: SourcePos + case class DefaultLeafMapping[T]( + predicate: MappingPredicate, + encoder: Encoder[T], + scalaTypeName: String)( + implicit val pos: SourcePos ) extends LeafMapping[T] { override def showMappingType: String = "LeafMapping" } - def apply[T: TypeName](predicate: MappingPredicate)(implicit encoder: Encoder[T], pos: SourcePos): LeafMapping[T] = + def apply[T: TypeName](predicate: MappingPredicate)( + implicit encoder: Encoder[T], + pos: SourcePos): LeafMapping[T] = DefaultLeafMapping(predicate, encoder, typeName) - def apply[T: TypeName](tpe: NamedType)(implicit encoder: Encoder[T], pos: SourcePos): LeafMapping[T] = + def apply[T: TypeName]( + tpe: NamedType)(implicit encoder: Encoder[T], pos: SourcePos): LeafMapping[T] = DefaultLeafMapping(MappingPredicate.TypeMatch(tpe), encoder, typeName) - def apply[T: TypeName](path: Path)(implicit encoder: Encoder[T], pos: SourcePos): LeafMapping[T] = + def apply[T: TypeName]( + path: Path)(implicit encoder: Encoder[T], pos: SourcePos): LeafMapping[T] = DefaultLeafMapping(MappingPredicate.PathMatch(path), encoder, typeName) def unapply[T](lm: LeafMapping[T]): Option[(Type, Encoder[T])] = Some((lm.tpe, lm.encoder)) } - case class CursorField[T](fieldName: String, f: Cursor => Result[T], encoder: Encoder[T], required: List[String], hidden: Boolean)( - implicit val pos: SourcePos + case class CursorField[T]( + fieldName: String, + f: Cursor => Result[T], + encoder: Encoder[T], + required: List[String], + hidden: Boolean)( + implicit val pos: SourcePos ) extends FieldMapping { def subtree = false } object CursorField { - def apply[T](fieldName: String, f: Cursor => Result[T], required: List[String] = Nil, hidden: Boolean = false)(implicit encoder: Encoder[T], di: DummyImplicit): CursorField[T] = + def apply[T]( + fieldName: String, + f: Cursor => Result[T], + required: List[String] = Nil, + hidden: Boolean = false)( + implicit encoder: Encoder[T], + di: DummyImplicit): CursorField[T] = new CursorField(fieldName, f, encoder, required, hidden) } case class Delegate( - fieldName: String, - mapping: Mapping[F], - join: (Query, Cursor) => Result[Query] = ComponentElaborator.TrivialJoin - )(implicit val pos: SourcePos) extends FieldMapping { + fieldName: String, + mapping: Mapping[F], + join: (Query, Cursor) => Result[Query] = ComponentElaborator.TrivialJoin + )(implicit val pos: SourcePos) + extends FieldMapping { def hidden = false def subtree = true } @@ -1192,7 +1396,11 @@ abstract class Mapping[F[_]] { case om: ObjectMapping => om.fieldMappings.collect { case Delegate(fieldName, mapping, join) => - ComponentElaborator.ComponentMapping(schema.uncheckedRef(om.tpe), fieldName, mapping, join) + ComponentElaborator.ComponentMapping( + schema.uncheckedRef(om.tpe), + fieldName, + mapping, + join) } case _ => Seq.empty } @@ -1202,12 +1410,11 @@ abstract class Mapping[F[_]] { lazy val effectElaborator: EffectElaborator[F] = EffectElaborator { (ctx, fieldName) => - typeMappings.fieldMapping(ctx, fieldName).collect { - case e: EffectField => e.handler - } + typeMappings.fieldMapping(ctx, fieldName).collect { case e: EffectField => e.handler } } - def compilerPhases: List[QueryCompiler.Phase] = List(selectElaborator, componentElaborator, effectElaborator) + def compilerPhases: List[QueryCompiler.Phase] = + List(selectElaborator, componentElaborator, effectElaborator) def parserConfig: GraphQLParser.Config = GraphQLParser.defaultConfig lazy val graphQLParser: GraphQLParser = GraphQLParser(parserConfig) @@ -1220,8 +1427,11 @@ abstract class Mapping[F[_]] { val interpreter: QueryInterpreter[F] = new QueryInterpreter(this) - /** Cursor positioned at a GraphQL result leaf */ - case class LeafCursor(context: Context, focus: Any, parent: Option[Cursor], env: Env) extends Cursor { + /** + * Cursor positioned at a GraphQL result leaf + */ + case class LeafCursor(context: Context, focus: Any, parent: Option[Cursor], env: Env) + extends Cursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) def mkChild(context: Context = context, focus: Any = focus): LeafCursor = @@ -1230,9 +1440,13 @@ abstract class Mapping[F[_]] { def isLeaf: Boolean = tpe.isLeaf def asLeaf: Result[Json] = - encoderForLeaf(context).map(enc => enc(focus).success).getOrElse(Result.internalError( - s"Cannot encode value $focus at ${context.path.reverse.mkString("/")} (of GraphQL type ${context.tpe}). Did you forget a LeafMapping?".stripMargin.trim - )) + encoderForLeaf(context) + .map(enc => enc(focus).success) + .getOrElse(Result.internalError( + s"Cannot encode value $focus at ${context.path.reverse.mkString("/")} (of GraphQL type ${context.tpe}). Did you forget a LeafMapping?" + .stripMargin + .trim + )) def preunique: Result[Cursor] = { val listTpe = tpe.nonNull.list @@ -1250,7 +1464,8 @@ abstract class Mapping[F[_]] { } def asList[C](factory: Factory[Cursor, C]): Result[C] = (tpe, focus) match { - case (ListType(tpe), it: Seq[_]) => it.view.map(f => mkChild(context.asType(tpe), focus = f)).to(factory).success + case (ListType(tpe), it: Seq[_]) => + it.view.map(f => mkChild(context.asType(tpe), focus = f)).to(factory).success case _ => Result.internalError(s"Expected List type, found $tpe") } @@ -1268,7 +1483,8 @@ abstract class Mapping[F[_]] { def asNullable: Result[Option[Cursor]] = (tpe, focus) match { case (NullableType(_), None) => None.success - case (NullableType(tpe), Some(v)) => Some(mkChild(context.asType(tpe), focus = v)).success + case (NullableType(tpe), Some(v)) => + Some(mkChild(context.asType(tpe), focus = v)).success case _ => Result.internalError(s"Not nullable at ${context.path}") } @@ -1289,13 +1505,15 @@ abstract class Mapping[F[_]] { /** * Proxy `Cursor` which applies a function to the focus of an underlying `LeafCursor`. */ - case class FieldTransformCursor[T : ClassTag : TypeName](underlying: Cursor, f: T => Result[T]) extends ProxyCursor(underlying) { - override def withEnv(env: Env): Cursor = new FieldTransformCursor(underlying.withEnv(env), f) + case class FieldTransformCursor[T: ClassTag: TypeName](underlying: Cursor, f: T => Result[T]) + extends ProxyCursor(underlying) { + override def withEnv(env: Env): Cursor = + new FieldTransformCursor(underlying.withEnv(env), f) override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = underlying.field(fieldName, resultName).flatMap { case l: LeafCursor => for { - focus <- l.as[T] + focus <- l.as[T] ffocus <- f(focus) } yield l.copy(focus = ffocus) case _ => @@ -1304,55 +1522,68 @@ abstract class Mapping[F[_]] { } /** - * Construct a GraphQL response from the possibly absent result `data` - * and a collection of errors. + * Construct a GraphQL response from the possibly absent result `data` and a collection of + * errors. */ def mkResponse(data: Option[Json], errors: Chain[Problem]): Json = { val dataField = data.map { value => ("data", value) }.toList val fields = (dataField, errors.toList) match { - case (Nil, Nil) => List(("errors", Json.fromValues(List(Problem("Invalid query").asJson)))) - case (data, Nil) => data + case (Nil, Nil) => + List(("errors", Json.fromValues(List(Problem("Invalid query").asJson)))) + case (data, Nil) => data case (data, errs) => ("errors", errs.asJson) :: data } Json.fromFields(fields) } - /** Construct a GraphQL response from a `Result`. */ + /** + * Construct a GraphQL response from a `Result`. + */ def mkResponse(result: Result[Json]): F[Json] = result match { case Result.InternalError(err) => M.raiseError(err) case _ => mkResponse(result.toOption, result.toProblems).pure[F] } - /** Missing type mapping. */ - case class MissingTypeMapping(ctx: Context) - extends ValidationFailure(Severity.Error) { + /** + * Missing type mapping. + */ + case class MissingTypeMapping(ctx: Context) extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${showNamedType(ctx.tpe)})" override def formattedMessage: String = s"""|Missing type mapping. | |- The type ${graphql(showNamedType(ctx.tpe))} is defined by a Schema at (1). - |- ${UNDERLINED}No mapping was found for this type for path ${ctx.path.reverse.mkString("", "/", "")}.$RESET + |- ${UNDERLINED}No mapping was found for this type for path ${ctx + .path + .reverse + .mkString("", "/", "")}.$RESET | |(1) ${schema.pos} |""".stripMargin } - /** Ambiguous type mappings. */ + /** + * Ambiguous type mappings. + */ case class AmbiguousTypeMappings(ctx: Context, conflicts: Seq[TypeMapping]) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${showNamedType(ctx.tpe)})" override def formattedMessage: String = { val n = conflicts.length - val ref = if(n > 2) s"(2)..(${n+1})" else "(2), (3)" - val posns = conflicts.zip(2 to n+1).map { case (c, n) => s"($n) ${c.pos}" }.mkString("\n") + val ref = if (n > 2) s"(2)..(${n + 1})" else "(2), (3)" + val posns = + conflicts.zip(2 to n + 1).map { case (c, n) => s"($n) ${c.pos}" }.mkString("\n") s"""|Ambiguous type mappings. | |- The type ${graphql(showNamedType(ctx.tpe))} is defined by a Schema at (1). - |- Multiple equally specific mappings were found at $ref for this type for path ${ctx.path.reverse.mkString("", "/", "")}. + |- Multiple equally specific mappings were found at $ref for this type for path ${ctx + .path + .reverse + .mkString("", "/", "")}. |- ${UNDERLINED}Mappings must be unambiguous.$RESET | |(1) ${schema.pos} @@ -1361,32 +1592,43 @@ abstract class Mapping[F[_]] { } } - /** Object type `owner` declares `field` but no such mapping exists. */ + /** + * Object type `owner` declares `field` but no such mapping exists. + */ case class MissingFieldMapping(objectMapping: ObjectMapping, field: Field) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${showNamedType(objectMapping.tpe)}.${field.name}:${showType(field.tpe)})" override def formattedMessage: String = s"""|Missing field mapping. | - |- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${field.name}:${showType(field.tpe)}")} is defined by a Schema at (1). - |- The ${scala(objectMapping.showMappingType)} for ${graphql(showNamedType(objectMapping.tpe))} at (2) ${UNDERLINED}does not define a mapping for this field$RESET. + |- The field ${graphql( + s"${showNamedType(objectMapping.tpe)}.${field.name}:${showType(field.tpe)}")} is defined by a Schema at (1). + |- The ${scala(objectMapping.showMappingType)} for ${graphql(showNamedType( + objectMapping.tpe))} at (2) ${UNDERLINED}does not define a mapping for this field$RESET. | |(1) ${schema.pos} |(2) ${objectMapping.pos} |""".stripMargin } - /** Referenced field does not exist. */ - case class DeclaredFieldMappingIsHidden(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends ValidationFailure(Severity.Error) { + /** + * Referenced field does not exist. + */ + case class DeclaredFieldMappingIsHidden( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping) + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Declared field mapping is hidden. | - |- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}")} is defined by a Schema at (1). - |- The ${scala(objectMapping.showMappingType)} at (2) contains a ${scala(fieldMapping.showMappingType)} mapping for field ${graphql(fieldMapping.fieldName)} at (3). + |- The field ${graphql( + s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}")} is defined by a Schema at (1). + |- The ${scala(objectMapping.showMappingType)} at (2) contains a ${scala( + fieldMapping.showMappingType)} mapping for field ${graphql( + fieldMapping.fieldName)} at (3). |- This field mapping is marked as hidden. |- ${UNDERLINED}The mappings for declared fields must not be hidden.$RESET | @@ -1396,16 +1638,20 @@ abstract class Mapping[F[_]] { |""".stripMargin } - /** GraphQL type isn't applicable for mapping type. */ + /** + * GraphQL type isn't applicable for mapping type. + */ case class ObjectTypeExpected(objectMapping: ObjectMapping) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)})" override def formattedMessage: String = s"""|Inapplicable GraphQL type. | - |- The ${typeKind(objectMapping.tpe)} ${graphql(showNamedType(objectMapping.tpe))} is defined by a Schema at (1). - |- It is mapped by the ${scala(objectMapping.showMappingType)} at (2), which expects an object type. + |- The ${typeKind(objectMapping.tpe)} ${graphql( + showNamedType(objectMapping.tpe))} is defined by a Schema at (1). + |- It is mapped by the ${scala( + objectMapping.showMappingType)} at (2), which expects an object type. |- ${UNDERLINED}Use a different kind of mapping for this type.$RESET | |(1) ${schema.pos} @@ -1413,16 +1659,20 @@ abstract class Mapping[F[_]] { |""".stripMargin } - /** GraphQL type isn't applicable for mapping type. */ + /** + * GraphQL type isn't applicable for mapping type. + */ case class LeafTypeExpected(leafMapping: LeafMapping[_]) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${leafMapping.showMappingType}, ${showNamedType(leafMapping.tpe)})" override def formattedMessage: String = s"""|Inapplicable GraphQL type. | - |- The ${typeKind(leafMapping.tpe)} ${graphql(showNamedType(leafMapping.tpe))} is defined by a Schema at (1). - |- It is mapped by the ${scala(leafMapping.showMappingType)} at (2), which expects a leaf type. + |- The ${typeKind(leafMapping.tpe)} ${graphql( + showNamedType(leafMapping.tpe))} is defined by a Schema at (1). + |- It is mapped by the ${scala( + leafMapping.showMappingType)} at (2), which expects a leaf type. |- ${UNDERLINED}Use a different kind of mapping for this type.$RESET | |(1) ${schema.pos} @@ -1430,16 +1680,18 @@ abstract class Mapping[F[_]] { |""".stripMargin } - - /** Referenced type does not exist. */ + /** + * Referenced type does not exist. + */ case class ReferencedTypeDoesNotExist(typeMapping: TypeMapping) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${typeMapping.showMappingType}, ${showNamedType(typeMapping.tpe)})" override def formattedMessage: String = s"""|Referenced type does not exist. | - |- The ${scala(typeMapping.showMappingType)} at (1) references type ${graphql(showNamedType(typeMapping.tpe))}. + |- The ${scala(typeMapping.showMappingType)} at (1) references type ${graphql( + showNamedType(typeMapping.tpe))}. |- ${UNDERLINED}This type is undeclared$RESET in the Schema defined at (2). | |(1) ${typeMapping.pos} @@ -1447,15 +1699,18 @@ abstract class Mapping[F[_]] { |""".stripMargin } - /** Type mapping is unused. */ + /** + * Type mapping is unused. + */ case class UnusedTypeMapping(typeMapping: TypeMapping) - extends ValidationFailure(Severity.Warning) { + extends ValidationFailure(Severity.Warning) { override def toString: String = s"$productPrefix(${typeMapping.showMappingType}, ${showNamedType(typeMapping.tpe)})" override def formattedMessage: String = s"""|Type mapping is unused. | - |- The ${scala(typeMapping.showMappingType)} at (1) references type ${graphql(showNamedType(typeMapping.tpe))}. + |- The ${scala(typeMapping.showMappingType)} at (1) references type ${graphql( + showNamedType(typeMapping.tpe))}. |- ${UNDERLINED}This type mapping is unused$RESET by queries conforming to the Schema at (2). | |(1) ${typeMapping.pos} @@ -1463,15 +1718,19 @@ abstract class Mapping[F[_]] { |""".stripMargin } - /** Referenced field does not exist. */ + /** + * Referenced field does not exist. + */ case class UnusedFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends ValidationFailure(Severity.Error) { + extends ValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Field mapping is unused. | - |- The ${scala(objectMapping.showMappingType)} at (1) contains a ${scala(fieldMapping.showMappingType)} mapping for field ${graphql(fieldMapping.fieldName)} at (2). + |- The ${scala(objectMapping.showMappingType)} at (1) contains a ${scala( + fieldMapping.showMappingType)} mapping for field ${graphql( + fieldMapping.fieldName)} at (2). |- ${UNDERLINED}This field mapping is unused$RESET by queries conforming to the Schema at (3). | |(1) ${objectMapping.pos} diff --git a/modules/core/src/main/scala/minimizer.scala b/modules/core/src/main/scala/minimizer.scala index cf9e8dd3..31a8611a 100644 --- a/modules/core/src/main/scala/minimizer.scala +++ b/modules/core/src/main/scala/minimizer.scala @@ -58,10 +58,10 @@ object QueryMinimizer { def renderOperation(op: Operation): String = renderOperationType(op.operationType) + - op.name.map(nme => s" ${nme.value}").getOrElse("") + - renderVariableDefns(op.variables)+ - renderDirectives(op.directives)+ - renderSelectionSet(op.selectionSet) + op.name.map(nme => s" ${nme.value}").getOrElse("") + + renderVariableDefns(op.variables) + + renderDirectives(op.directives) + + renderSelectionSet(op.selectionSet) def renderOperationType(op: OperationType): String = op match { @@ -71,16 +71,20 @@ object QueryMinimizer { } def renderDirectives(dirs: List[Directive]): String = - dirs.map { case Directive(name, args) => s"@${name.value}${renderArguments(args)}" }.mkString("") + dirs + .map { case Directive(name, args) => s"@${name.value}${renderArguments(args)}" } + .mkString("") def renderVariableDefns(vars: List[VariableDefinition]): String = vars match { case Nil => "" case _ => - vars.map { - case VariableDefinition(name, tpe, default, dirs) => - s"$$${name.value}:${tpe.name}${default.map(v => s"=${renderValue(v)}").getOrElse("")}${renderDirectives(dirs)}" - }.mkString("(", ",", ")") + vars + .map { + case VariableDefinition(name, tpe, default, dirs) => + s"$$${name.value}:${tpe.name}${default.map(v => s"=${renderValue(v)}").getOrElse("")}${renderDirectives(dirs)}" + } + .mkString("(", ",", ")") } def renderSelectionSet(sels: List[Selection]): String = @@ -97,23 +101,25 @@ object QueryMinimizer { } def renderField(f: Field) = { - f.alias.map(a => s"${a.value}:").getOrElse("")+ - f.name.value+ - renderArguments(f.arguments)+ - renderDirectives(f.directives)+ - renderSelectionSet(f.selectionSet) + f.alias.map(a => s"${a.value}:").getOrElse("") + + f.name.value + + renderArguments(f.arguments) + + renderDirectives(f.directives) + + renderSelectionSet(f.selectionSet) } def renderArguments(args: List[(Name, Value)]): String = args match { case Nil => "" - case _ => args.map { case (n, v) => s"${n.value}:${renderValue(v)}" }.mkString("(", ",", ")") + case _ => + args.map { case (n, v) => s"${n.value}:${renderValue(v)}" }.mkString("(", ",", ")") } def renderInputObject(args: List[(Name, Value)]): String = args match { case Nil => "" - case _ => args.map { case (n, v) => s"${n.value}:${renderValue(v)}" }.mkString("{", ",", "}") + case _ => + args.map { case (n, v) => s"${n.value}:${renderValue(v)}" }.mkString("{", ",", "}") } def renderTypeCondition(tpe: Type): String = diff --git a/modules/core/src/main/scala/operation.scala b/modules/core/src/main/scala/operation.scala index e06f7539..d048af8a 100644 --- a/modules/core/src/main/scala/operation.scala +++ b/modules/core/src/main/scala/operation.scala @@ -15,8 +15,8 @@ package grackle -import syntax._ -import Query._ +import grackle.Query._ +import grackle.syntax._ sealed trait UntypedOperation { val name: Option[String] @@ -25,34 +25,36 @@ sealed trait UntypedOperation { val directives: List[Directive] def rootTpe(schema: Schema): Result[NamedType] = this match { - case _: UntypedOperation.UntypedQuery => schema.queryType.success - case _: UntypedOperation.UntypedMutation => schema.mutationType.toResult("No mutation type defined in this schema.") - case _: UntypedOperation.UntypedSubscription => schema.subscriptionType.toResult("No subscription type defined in this schema.") + case _: UntypedOperation.UntypedQuery => schema.queryType.success + case _: UntypedOperation.UntypedMutation => + schema.mutationType.toResult("No mutation type defined in this schema.") + case _: UntypedOperation.UntypedSubscription => + schema.subscriptionType.toResult("No subscription type defined in this schema.") } } object UntypedOperation { case class UntypedQuery( - name: Option[String], - query: Query, - variables: UntypedVarDefs, - directives: List[Directive] + name: Option[String], + query: Query, + variables: UntypedVarDefs, + directives: List[Directive] ) extends UntypedOperation case class UntypedMutation( - name: Option[String], - query: Query, - variables: UntypedVarDefs, - directives: List[Directive] + name: Option[String], + query: Query, + variables: UntypedVarDefs, + directives: List[Directive] ) extends UntypedOperation case class UntypedSubscription( - name: Option[String], - query: Query, - variables: UntypedVarDefs, - directives: List[Directive] + name: Option[String], + query: Query, + variables: UntypedVarDefs, + directives: List[Directive] ) extends UntypedOperation } case class Operation( - query: Query, - rootTpe: NamedType, - directives: List[Directive] + query: Query, + rootTpe: NamedType, + directives: List[Directive] ) diff --git a/modules/core/src/main/scala/parser.scala b/modules/core/src/main/scala/parser.scala index 70e60adb..ee9d0146 100644 --- a/modules/core/src/main/scala/parser.scala +++ b/modules/core/src/main/scala/parser.scala @@ -19,8 +19,8 @@ import scala.util.matching.Regex import cats.implicits._ import cats.parse.{Parser, Parser0} -import cats.parse.Parser._ import cats.parse.Numbers._ +import cats.parse.Parser._ import cats.parse.Rfc5234.{cr, crlf, digit, hexdig, lf} trait GraphQLParser { @@ -29,11 +29,11 @@ trait GraphQLParser { object GraphQLParser { case class Config( - maxSelectionDepth: Int, - maxSelectionWidth: Int, - maxInputValueDepth: Int, - maxListTypeDepth: Int, - terseError: Boolean + maxSelectionDepth: Int, + maxSelectionWidth: Int, + maxInputValueDepth: Int, + maxListTypeDepth: Int, + terseError: Boolean ) val defaultConfig: Config = @@ -65,7 +65,7 @@ object GraphQLParser { if (config.terseError) toResultTerseError(res) else toResult(res) } - val nameInitial = ('A' to 'Z') ++ ('a' to 'z') ++ Seq('_') + val nameInitial = ('A' to 'Z') ++ ('a' to 'z') ++ Seq('_') val nameSubsequent = nameInitial ++ ('0' to '9') def keyword(s: String) = token(string(s) <* not(charIn(nameSubsequent))) @@ -86,37 +86,66 @@ object GraphQLParser { def typeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.TypeDefinition] = { - def scalarTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.ScalarTypeDefinition] = + def scalarTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.ScalarTypeDefinition] = ((keyword("scalar") *> Name) ~ Directives.?).map { - case (name, dirs) => Ast.ScalarTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil)) + case (name, dirs) => + Ast.ScalarTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil)) } - def objectTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.ObjectTypeDefinition] = - ((keyword("type") *> Name) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition).map { - case (((name, ifs), dirs), fields) => Ast.ObjectTypeDefinition(name, desc.map(_.value), fields, ifs.getOrElse(Nil), dirs.getOrElse(Nil)) - } + def objectTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.ObjectTypeDefinition] = + ((keyword("type") *> Name) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition) + .map { + case (((name, ifs), dirs), fields) => + Ast.ObjectTypeDefinition( + name, + desc.map(_.value), + fields, + ifs.getOrElse(Nil), + dirs.getOrElse(Nil)) + } - def interfaceTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.InterfaceTypeDefinition] = - ((keyword("interface") *> Name) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition).map { - case (((name, ifs), dirs), fields) => Ast.InterfaceTypeDefinition(name, desc.map(_.value), fields, ifs.getOrElse(Nil), dirs.getOrElse(Nil)) - } + def interfaceTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.InterfaceTypeDefinition] = + ((keyword( + "interface") *> Name) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition) + .map { + case (((name, ifs), dirs), fields) => + Ast.InterfaceTypeDefinition( + name, + desc.map(_.value), + fields, + ifs.getOrElse(Nil), + dirs.getOrElse(Nil)) + } - def unionTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.UnionTypeDefinition] = + def unionTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.UnionTypeDefinition] = ((keyword("union") *> Name) ~ Directives.? ~ UnionMemberTypes).map { - case ((name, dirs), members) => Ast.UnionTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil), members) + case ((name, dirs), members) => + Ast.UnionTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil), members) } - def enumTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.EnumTypeDefinition] = + def enumTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.EnumTypeDefinition] = ((keyword("enum") *> Name) ~ Directives.? ~ EnumValuesDefinition).map { - case ((name, dirs), values) => Ast.EnumTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil), values) + case ((name, dirs), values) => + Ast.EnumTypeDefinition(name, desc.map(_.value), dirs.getOrElse(Nil), values) } - def inputObjectTypeDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.InputObjectTypeDefinition] = + def inputObjectTypeDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.InputObjectTypeDefinition] = ((keyword("input") *> Name) ~ Directives.? ~ InputFieldsDefinition).map { - case ((name, dirs), fields) => Ast.InputObjectTypeDefinition(name, desc.map(_.value), fields, dirs.getOrElse(Nil)) + case ((name, dirs), fields) => + Ast.InputObjectTypeDefinition( + name, + desc.map(_.value), + fields, + dirs.getOrElse(Nil)) } - scalarTypeDefinition(desc)| + scalarTypeDefinition(desc) | objectTypeDefinition(desc) | interfaceTypeDefinition(desc) | unionTypeDefinition(desc) | @@ -124,63 +153,87 @@ object GraphQLParser { inputObjectTypeDefinition(desc) } - def directiveDefinition(desc: Option[Ast.Value.StringValue]): Parser[Ast.DirectiveDefinition] = + def directiveDefinition( + desc: Option[Ast.Value.StringValue]): Parser[Ast.DirectiveDefinition] = ((keyword("directive") *> punctuation("@") *> Name) ~ - ArgumentsDefinition.? ~ (keyword("repeatable").? <* keyword("on")) ~ DirectiveLocations).map { - case (((name, args), rpt), locs) => Ast.DirectiveDefinition(name, desc.map(_.value), args.getOrElse(Nil), rpt.isDefined, locs) + ArgumentsDefinition.? ~ (keyword("repeatable").? <* keyword( + "on")) ~ DirectiveLocations).map { + case (((name, args), rpt), locs) => + Ast.DirectiveDefinition( + name, + desc.map(_.value), + args.getOrElse(Nil), + rpt.isDefined, + locs) } SchemaDefinition | - Description.?.with1.flatMap { desc => - typeDefinition(desc) | directiveDefinition(desc) - } + Description.?.with1.flatMap { desc => typeDefinition(desc) | directiveDefinition(desc) } } lazy val TypeSystemExtension: Parser[Ast.TypeSystemExtension] = { val SchemaExtension: Parser[Ast.SchemaExtension] = ((keyword("schema") *> Directives.?) ~ braces(RootOperationTypeDefinition.rep0).?).map { - case (dirs, rootdefs) => Ast.SchemaExtension(rootdefs.getOrElse(Nil), dirs.getOrElse(Nil)) + case (dirs, rootdefs) => + Ast.SchemaExtension(rootdefs.getOrElse(Nil), dirs.getOrElse(Nil)) } val TypeExtension: Parser[Ast.TypeExtension] = { val ScalarTypeExtension: Parser[Ast.ScalarTypeExtension] = ((keyword("scalar") *> NamedType) ~ Directives.?).map { - case (((name), dirs)) => Ast.ScalarTypeExtension(name, dirs.getOrElse(Nil)) + case (name, dirs) => Ast.ScalarTypeExtension(name, dirs.getOrElse(Nil)) } val ObjectTypeExtension: Parser[Ast.ObjectTypeExtension] = - ((keyword("type") *> NamedType) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition.?).map { - case (((name, ifs), dirs), fields) => Ast.ObjectTypeExtension(name, fields.getOrElse(Nil), ifs.getOrElse(Nil), dirs.getOrElse(Nil)) - } + ((keyword( + "type") *> NamedType) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition.?) + .map { + case (((name, ifs), dirs), fields) => + Ast.ObjectTypeExtension( + name, + fields.getOrElse(Nil), + ifs.getOrElse(Nil), + dirs.getOrElse(Nil)) + } val InterfaceTypeExtension: Parser[Ast.InterfaceTypeExtension] = - ((keyword("interface") *> NamedType) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition.?).map { - case (((name, ifs), dirs), fields) => Ast.InterfaceTypeExtension(name, fields.getOrElse(Nil), ifs.getOrElse(Nil), dirs.getOrElse(Nil)) - } + ((keyword( + "interface") *> NamedType) ~ ImplementsInterfaces.? ~ Directives.? ~ FieldsDefinition.?) + .map { + case (((name, ifs), dirs), fields) => + Ast.InterfaceTypeExtension( + name, + fields.getOrElse(Nil), + ifs.getOrElse(Nil), + dirs.getOrElse(Nil)) + } val UnionTypeExtension: Parser[Ast.UnionTypeExtension] = ((keyword("union") *> NamedType) ~ Directives.? ~ UnionMemberTypes.?).map { - case (((name), dirs), members) => Ast.UnionTypeExtension(name, dirs.getOrElse(Nil), members.getOrElse(Nil)) + case ((name, dirs), members) => + Ast.UnionTypeExtension(name, dirs.getOrElse(Nil), members.getOrElse(Nil)) } val EnumTypeExtension: Parser[Ast.EnumTypeExtension] = ((keyword("enum") *> NamedType) ~ Directives.? ~ EnumValuesDefinition.?).map { - case (((name), dirs), values) => Ast.EnumTypeExtension(name, dirs.getOrElse(Nil), values.getOrElse(Nil)) + case ((name, dirs), values) => + Ast.EnumTypeExtension(name, dirs.getOrElse(Nil), values.getOrElse(Nil)) } val InputObjectTypeExtension: Parser[Ast.InputObjectTypeExtension] = ((keyword("input") *> NamedType) ~ Directives.? ~ InputFieldsDefinition.?).map { - case (((name), dirs), fields) => Ast.InputObjectTypeExtension(name, dirs.getOrElse(Nil), fields.getOrElse(Nil)) + case ((name, dirs), fields) => + Ast.InputObjectTypeExtension(name, dirs.getOrElse(Nil), fields.getOrElse(Nil)) } - ScalarTypeExtension| - ObjectTypeExtension| - InterfaceTypeExtension| - UnionTypeExtension| - EnumTypeExtension| - InputObjectTypeExtension + ScalarTypeExtension | + ObjectTypeExtension | + InterfaceTypeExtension | + UnionTypeExtension | + EnumTypeExtension | + InputObjectTypeExtension } keyword("extend") *> (SchemaExtension | TypeExtension) @@ -191,7 +244,6 @@ object GraphQLParser { case (((optpe, _), tpe), dirs) => Ast.RootOperationTypeDefinition(optpe, tpe, dirs) } - lazy val Description = StringValue lazy val ImplementsInterfaces = @@ -201,8 +253,15 @@ object GraphQLParser { braces(FieldDefinition.rep0) lazy val FieldDefinition: Parser[Ast.FieldDefinition] = - (Description.?.with1 ~ Name ~ ArgumentsDefinition.? ~ punctuation(":") ~ Type ~ Directives.?).map { - case (((((desc, name), args), _), tpe), dirs) => Ast.FieldDefinition(name, desc.map(_.value), args.getOrElse(Nil), tpe, dirs.getOrElse(Nil)) + (Description.?.with1 ~ Name ~ ArgumentsDefinition.? ~ punctuation( + ":") ~ Type ~ Directives.?).map { + case (((((desc, name), args), _), tpe), dirs) => + Ast.FieldDefinition( + name, + desc.map(_.value), + args.getOrElse(Nil), + tpe, + dirs.getOrElse(Nil)) } lazy val ArgumentsDefinition: Parser[List[Ast.InputValueDefinition]] = @@ -212,9 +271,11 @@ object GraphQLParser { braces(InputValueDefinition.rep0) lazy val InputValueDefinition: Parser[Ast.InputValueDefinition] = - (Description.?.with1 ~ (Name <* punctuation(":")) ~ Type ~ DefaultValue.? ~ Directives.?).map { - case ((((desc, name), tpe), dv), dirs) => Ast.InputValueDefinition(name, desc.map(_.value), tpe, dv, dirs.getOrElse(Nil)) - } + (Description.?.with1 ~ (Name <* punctuation(":")) ~ Type ~ DefaultValue.? ~ Directives.?) + .map { + case ((((desc, name), tpe), dv), dirs) => + Ast.InputValueDefinition(name, desc.map(_.value), tpe, dv, dirs.getOrElse(Nil)) + } lazy val UnionMemberTypes: Parser[List[Ast.Type.Named]] = (punctuation("=") *> punctuation("|").?) *> NamedType.repSep0(punctuation("|")) @@ -224,32 +285,33 @@ object GraphQLParser { lazy val EnumValueDefinition: Parser[Ast.EnumValueDefinition] = (Description.?.with1 ~ Name ~ Directives.?).map { - case ((desc, name), dirs) => Ast.EnumValueDefinition(name, desc.map(_.value), dirs.getOrElse(Nil)) + case ((desc, name), dirs) => + Ast.EnumValueDefinition(name, desc.map(_.value), dirs.getOrElse(Nil)) } lazy val DirectiveLocations: Parser0[List[Ast.DirectiveLocation]] = punctuation("|").? *> DirectiveLocation.repSep0(punctuation("|")) lazy val DirectiveLocation: Parser[Ast.DirectiveLocation] = - keyword("QUERY") .as(Ast.DirectiveLocation.QUERY) | - keyword("MUTATION") .as(Ast.DirectiveLocation.MUTATION) | - keyword("SUBSCRIPTION").as(Ast.DirectiveLocation.SUBSCRIPTION) | - keyword("FIELD_DEFINITION").as(Ast.DirectiveLocation.FIELD_DEFINITION) | - keyword("FIELD").as(Ast.DirectiveLocation.FIELD) | - keyword("FRAGMENT_DEFINITION").as(Ast.DirectiveLocation.FRAGMENT_DEFINITION) | - keyword("FRAGMENT_SPREAD").as(Ast.DirectiveLocation.FRAGMENT_SPREAD) | - keyword("INLINE_FRAGMENT").as(Ast.DirectiveLocation.INLINE_FRAGMENT) | - keyword("VARIABLE_DEFINITION").as(Ast.DirectiveLocation.VARIABLE_DEFINITION) | - keyword("SCHEMA").as(Ast.DirectiveLocation.SCHEMA) | - keyword("SCALAR").as(Ast.DirectiveLocation.SCALAR) | - keyword("OBJECT").as(Ast.DirectiveLocation.OBJECT) | - keyword("ARGUMENT_DEFINITION").as(Ast.DirectiveLocation.ARGUMENT_DEFINITION) | - keyword("INTERFACE").as(Ast.DirectiveLocation.INTERFACE) | - keyword("UNION").as(Ast.DirectiveLocation.UNION) | - keyword("ENUM_VALUE").as(Ast.DirectiveLocation.ENUM_VALUE) | - keyword("ENUM").as(Ast.DirectiveLocation.ENUM) | - keyword("INPUT_OBJECT").as(Ast.DirectiveLocation.INPUT_OBJECT) | - keyword("INPUT_FIELD_DEFINITION").as(Ast.DirectiveLocation.INPUT_FIELD_DEFINITION) + keyword("QUERY").as(Ast.DirectiveLocation.QUERY) | + keyword("MUTATION").as(Ast.DirectiveLocation.MUTATION) | + keyword("SUBSCRIPTION").as(Ast.DirectiveLocation.SUBSCRIPTION) | + keyword("FIELD_DEFINITION").as(Ast.DirectiveLocation.FIELD_DEFINITION) | + keyword("FIELD").as(Ast.DirectiveLocation.FIELD) | + keyword("FRAGMENT_DEFINITION").as(Ast.DirectiveLocation.FRAGMENT_DEFINITION) | + keyword("FRAGMENT_SPREAD").as(Ast.DirectiveLocation.FRAGMENT_SPREAD) | + keyword("INLINE_FRAGMENT").as(Ast.DirectiveLocation.INLINE_FRAGMENT) | + keyword("VARIABLE_DEFINITION").as(Ast.DirectiveLocation.VARIABLE_DEFINITION) | + keyword("SCHEMA").as(Ast.DirectiveLocation.SCHEMA) | + keyword("SCALAR").as(Ast.DirectiveLocation.SCALAR) | + keyword("OBJECT").as(Ast.DirectiveLocation.OBJECT) | + keyword("ARGUMENT_DEFINITION").as(Ast.DirectiveLocation.ARGUMENT_DEFINITION) | + keyword("INTERFACE").as(Ast.DirectiveLocation.INTERFACE) | + keyword("UNION").as(Ast.DirectiveLocation.UNION) | + keyword("ENUM_VALUE").as(Ast.DirectiveLocation.ENUM_VALUE) | + keyword("ENUM").as(Ast.DirectiveLocation.ENUM) | + keyword("INPUT_OBJECT").as(Ast.DirectiveLocation.INPUT_OBJECT) | + keyword("INPUT_FIELD_DEFINITION").as(Ast.DirectiveLocation.INPUT_FIELD_DEFINITION) lazy val ExecutableDefinition: Parser[Ast.ExecutableDefinition] = OperationDefinition | FragmentDefinition @@ -262,23 +324,27 @@ object GraphQLParser { lazy val Operation: Parser[Ast.OperationDefinition.Operation] = (OperationType ~ Name.? ~ VariableDefinitions.? ~ Directives ~ SelectionSet).map { - case ((((op, name), vars), dirs), sels) => Ast.OperationDefinition.Operation(op, name, vars.orEmpty, dirs, sels) + case ((((op, name), vars), dirs), sels) => + Ast.OperationDefinition.Operation(op, name, vars.orEmpty, dirs, sels) } lazy val OperationType: Parser[Ast.OperationType] = - keyword("query") .as(Ast.OperationType.Query) | - keyword("mutation") .as(Ast.OperationType.Mutation) | - keyword("subscription").as(Ast.OperationType.Subscription) + keyword("query").as(Ast.OperationType.Query) | + keyword("mutation").as(Ast.OperationType.Mutation) | + keyword("subscription").as(Ast.OperationType.Subscription) lazy val Alias: Parser[Ast.Name] = Name <* punctuation(":") lazy val FragmentSpread: Parser[Ast.Selection.FragmentSpread] = - (FragmentName ~ Directives).map{ case (name, dirs) => Ast.Selection.FragmentSpread.apply(name, dirs)} + (FragmentName ~ Directives).map { + case (name, dirs) => Ast.Selection.FragmentSpread.apply(name, dirs) + } def Field(n: Int): Parser[Ast.Selection.Field] = (Alias.backtrack.?.with1 ~ Name ~ Arguments.? ~ Directives ~ SelectionSetN(n).?).map { - case ((((alias, name), args), dirs), sel) => Ast.Selection.Field(alias, name, args.orEmpty, dirs, sel.orEmpty) + case ((((alias, name), args), dirs), sel) => + Ast.Selection.Field(alias, name, args.orEmpty, dirs, sel.orEmpty) } def InlineFragment(n: Int): Parser[Ast.Selection.InlineFragment] = @@ -288,13 +354,15 @@ object GraphQLParser { def Selection(n: Int): Parser[Ast.Selection] = Field(n) | - (punctuation("...") *> (InlineFragment(n) | FragmentSpread)) + (punctuation("...") *> (InlineFragment(n) | FragmentSpread)) lazy val SelectionSet: Parser[List[Ast.Selection]] = SelectionSetN(maxSelectionDepth) def SelectionSetN(n: Int): Parser[List[Ast.Selection]] = - braces(guard0(n, "exceeded maximum selection depth")(Selection(_).repAs0(max = maxSelectionWidth))) + braces( + guard0(n, "exceeded maximum selection depth")( + Selection(_).repAs0(max = maxSelectionWidth))) lazy val Arguments: Parser[List[(Ast.Name, Ast.Value)]] = parens(Argument.rep0) @@ -321,7 +389,9 @@ object GraphQLParser { .map(Ast.Value.EnumValue.apply) def ListValue(n: Int): Parser[Ast.Value.ListValue] = - token(squareBrackets(guard0(n, "exceeded maximum input value depth")(ValueN(_).rep0)).map(Ast.Value.ListValue.apply)) + token( + squareBrackets(guard0(n, "exceeded maximum input value depth")(ValueN(_).rep0)) + .map(Ast.Value.ListValue.apply)) lazy val NumericLiteral: Parser[Ast.Value] = { def narrow(d: BigDecimal): Ast.Value.FloatValue = @@ -332,13 +402,13 @@ object GraphQLParser { // negative sign on a zero integer part (e.g. `-0.99`) is preserved. Parsing // it to an `Int` first would collapse `-0` to `0`, dropping the sign for any // value in (-1, 0). - (intLiteral.string ~ (char('.') *> digit.rep.string).? ~ ((char('e') | char('E')) *> intLiteral.string).?) - .map { - case ((a, Some(b)), None) => narrow(BigDecimal(s"$a.$b")) - case ((a, None), Some(c)) => narrow(BigDecimal(s"${a}E$c")) - case ((a, Some(b)), Some(c)) => narrow(BigDecimal(s"$a.${b}E$c")) - case ((a, None), None) => Ast.Value.IntValue(a.toInt) - } + (intLiteral.string ~ (char('.') *> digit.rep.string).? ~ ((char('e') | char( + 'E')) *> intLiteral.string).?).map { + case ((a, Some(b)), None) => narrow(BigDecimal(s"$a.$b")) + case ((a, None), Some(c)) => narrow(BigDecimal(s"${a}E$c")) + case ((a, Some(b)), Some(c)) => narrow(BigDecimal(s"$a.${b}E$c")) + case ((a, None), None) => Ast.Value.IntValue(a.toInt) + } ) } @@ -349,7 +419,8 @@ object GraphQLParser { (Name <* punctuation(":")) ~ ValueN(n) def ObjectValue(n: Int): Parser[Ast.Value.ObjectValue] = - braces(guard0(n, "exceeded maximum input value depth")(ObjectField(_).rep0)).map(Ast.Value.ObjectValue.apply) + braces(guard0(n, "exceeded maximum input value depth")(ObjectField(_).rep0)) + .map(Ast.Value.ObjectValue.apply) lazy val StringValue: Parser[Ast.Value.StringValue] = token(stringLiteral).map(Ast.Value.StringValue.apply) @@ -372,7 +443,8 @@ object GraphQLParser { lazy val VariableDefinition: Parser[Ast.VariableDefinition] = ((Variable <* punctuation(":")) ~ Type ~ DefaultValue.? ~ Directives.?).map { - case (((v, tpe), dv), dirs) => Ast.VariableDefinition(v.name, tpe, dv, dirs.getOrElse(Nil)) + case (((v, tpe), dv), dirs) => + Ast.VariableDefinition(v.name, tpe, dv, dirs.getOrElse(Nil)) } lazy val Variable: Parser[Ast.Value.Variable] = @@ -382,7 +454,8 @@ object GraphQLParser { punctuation("=") *> Value def ListType(n: Int): Parser[Ast.Type.List] = - squareBrackets(guard(n, "exceeded maximum list type depth")(TypeN)).map(Ast.Type.List.apply) + squareBrackets(guard(n, "exceeded maximum list type depth")(TypeN)) + .map(Ast.Type.List.apply) lazy val namedMaybeNull: Parser[Ast.Type] = (NamedType ~ punctuation("!").?).map { case (t, None) => t @@ -407,7 +480,9 @@ object GraphQLParser { Directive.rep0 lazy val Directive: Parser[Ast.Directive] = - punctuation("@") *> (Name ~ Arguments.?).map { case (n, ods) => Ast.Directive(n, ods.orEmpty)} + punctuation("@") *> (Name ~ Arguments.?).map { + case (n, ods) => Ast.Directive(n, ods.orEmpty) + } lazy val Name: Parser[Ast.Name] = token(charIn(nameInitial) ~ charIn(nameSubsequent).rep0).map { @@ -415,10 +490,10 @@ object GraphQLParser { } def guard0[T](n: Int, msg: String)(p: Int => Parser0[T]): Parser0[T] = - if (n <= 0) Parser.failWith(msg) else defer0(p(n-1)) + if (n <= 0) Parser.failWith(msg) else defer0(p(n - 1)) def guard[T](n: Int, msg: String)(p: Int => Parser[T]): Parser[T] = - if (n <= 0) Parser.failWith(msg) else defer(p(n-1)) + if (n <= 0) Parser.failWith(msg) else defer(p(n - 1)) } private object CommentedText { @@ -428,11 +503,17 @@ object GraphQLParser { val skipWhitespace: Parser0[Unit] = charsWhile0(c => c.isWhitespace || c == ',').void - /** Parser that consumes a comment */ + /** + * Parser that consumes a comment + */ val comment: Parser[Unit] = - (char('#') *> (charWhere(c => c != '\n' && c != '\r')).rep0 <* charIn('\n', '\r') <* skipWhitespace).void + (char('#') *> charWhere(c => c != '\n' && c != '\r').rep0 <* charIn( + '\n', + '\r') <* skipWhitespace).void - /** Turns a parser into one that skips trailing whitespace and comments */ + /** + * Turns a parser into one that skips trailing whitespace and comments + */ def token[A](p: Parser[A]): Parser[A] = p <* skipWhitespace <* comment.rep0 @@ -440,21 +521,27 @@ object GraphQLParser { p <* skipWhitespace <* comment.rep0 /** - * Consumes `left` and `right`, including the trailing and preceding whitespace, - * respectively, and returns the value of `p`. - */ - private def _bracket[A,B,C](left: Parser[B], p: Parser0[A], right: Parser[C]): Parser[A] = + * Consumes `left` and `right`, including the trailing and preceding whitespace, + * respectively, and returns the value of `p`. + */ + private def _bracket[A, B, C](left: Parser[B], p: Parser0[A], right: Parser[C]): Parser[A] = token(left) *> token0(p) <* token(right) - /** Turns a parser into one that consumes surrounding parentheses `()` */ + /** + * Turns a parser into one that consumes surrounding parentheses `()` + */ def parens[A](p: Parser0[A]): Parser[A] = _bracket(char('('), p, char(')')) - /** Turns a parser into one that consumes surrounding curly braces `{}` */ + /** + * Turns a parser into one that consumes surrounding curly braces `{}` + */ def braces[A](p: Parser0[A]): Parser[A] = _bracket(char('{'), p, char('}')) - /** Turns a parser into one that consumes surrounding square brackets `[]` */ + /** + * Turns a parser into one that consumes surrounding square brackets `[]` + */ def squareBrackets[A](p: Parser0[A]): Parser[A] = _bracket(char('['), p, char(']')) } @@ -465,12 +552,12 @@ object GraphQLParser { val lineTerminator: Parser[String] = (lf | cr | crlf).string - val sourceCharacter: Parser[String] = (charIn(0x0009.toChar, 0x000A.toChar, 0x000D.toChar) | charIn(0x0020.toChar to 0xFFFF.toChar)).string + val sourceCharacter: Parser[String] = + (charIn(0x0009.toChar, 0x000a.toChar, 0x000d.toChar) | charIn( + 0x0020.toChar to 0xffff.toChar)).string val escapedUnicode: Parser[String] = string("\\u") *> - hexdig - .repExactlyAs[String](4) - .map(hex => Integer.parseInt(hex, 16).toChar.toString) + hexdig.repExactlyAs[String](4).map(hex => Integer.parseInt(hex, 16).toChar.toString) val escapedCharacter: Parser[String] = char('\\') *> ( @@ -484,18 +571,17 @@ object GraphQLParser { char('t').as("\t") ) - val stringCharacter: Parser[String] = ( + val stringCharacter: Parser[String] = (not(charIn('"', '\\') | lineTerminator).with1 *> sourceCharacter) | escapedUnicode | - escapedCharacter - ) + escapedCharacter val blockStringCharacter: Parser[String] = string("\\\"\"\"").as("\"\"\"") | (not(string("\"\"\"")).with1 *> sourceCharacter) - //https://spec.graphql.org/June2018/#BlockStringValue() - //TODO this traverses over lines a hideous number of times(but matching the - //algorithm in the spec). Can it be optimized? + // https://spec.graphql.org/June2018/#BlockStringValue() + // TODO this traverses over lines a hideous number of times(but matching the + // algorithm in the spec). Can it be optimized? val blockQuotesInner: Parser0[String] = blockStringCharacter.repAs0[String].map { str => val isWhitespace: Regex = "[ \t]*".r var commonIndent: Int = -1 @@ -512,9 +598,9 @@ object GraphQLParser { } lineNum = lineNum + 1 } - val formattedReversed: List[String] = if ( commonIndent >= 0) { - str.linesIterator.foldLeft[List[String]](Nil) { - (acc, l) => if (acc == Nil) l :: acc else l.drop(commonIndent) :: acc + val formattedReversed: List[String] = if (commonIndent >= 0) { + str.linesIterator.foldLeft[List[String]](Nil) { (acc, l) => + if (acc == Nil) l :: acc else l.drop(commonIndent) :: acc } } else { str.linesIterator.toList @@ -523,8 +609,10 @@ object GraphQLParser { noTrailingEmpty.dropWhile(isWhitespace.matches(_)).mkString("\n") } - - (not(string("\"\"\"")).with1 *> stringCharacter.repAs0[String].with1.surroundedBy(char('"'))) | blockQuotesInner.with1.surroundedBy(string("\"\"\"")) + (not(string("\"\"\"")).with1 *> stringCharacter + .repAs0[String] + .with1 + .surroundedBy(char('"'))) | blockQuotesInner.with1.surroundedBy(string("\"\"\"")) } diff --git a/modules/core/src/main/scala/predicate.scala b/modules/core/src/main/scala/predicate.scala index ca22df8f..b89796d7 100644 --- a/modules/core/src/main/scala/predicate.scala +++ b/modules/core/src/main/scala/predicate.scala @@ -19,19 +19,21 @@ import scala.annotation.tailrec import scala.util.matching.Regex import cats.Apply -import cats.kernel.{ Eq, Order } import cats.implicits._ +import cats.kernel.{Eq, Order} -import syntax._ +import grackle.syntax._ /** * A reified function over a `Cursor`. * - * Query interpreters will typically need to introspect predicates (eg. in the doobie module - * we need to be able to construct where clauses from predicates over fields/attributes), so - * these cannot be arbitrary functions `Cursor => Boolean`. + * Query interpreters will typically need to introspect predicates (eg. in the doobie module we + * need to be able to construct where clauses from predicates over fields/attributes), so these + * cannot be arbitrary functions `Cursor => Boolean`. */ -trait Term[T] extends Product with Serializable { // fun fact: making this covariant crashes Scala 3 +trait Term[T] + extends Product + with Serializable { // fun fact: making this covariant crashes Scala 3 def apply(c: Cursor): Result[T] def children: List[Term[_]] @@ -48,7 +50,10 @@ trait Term[T] extends Product with Serializable { // fun fact: making this covar def forallR(f: Term[_] => Result[Boolean]): Result[Boolean] = f(this).flatMap { p => if (!p) false.success - else children.foldLeftM(true) { case (acc, elem) => if(!acc) false.success else elem.forallR(f) } + else + children.foldLeftM(true) { + case (acc, elem) => if (!acc) false.success else elem.forallR(f) + } } } @@ -74,9 +79,14 @@ trait Path { def isRoot = path.isEmpty } -/** A path starting from some root type. */ +/** + * A path starting from some root type. + */ object Path { - /** Construct an empty `Path` rooted at the given type. */ + + /** + * Construct an empty `Path` rooted at the given type. + */ def from(tpe: Type): Path = PathImpl(tpe, Nil, tpe) @@ -88,35 +98,40 @@ object Path { lazy val isList: Boolean = rootTpe.pathIsList(path) def /(elem: String): Path = { - val ftpe = tpe.underlyingField(elem).getOrElse(ScalarType.AttributeType) // Matches Context#forFieldOrAttribute + val ftpe = tpe + .underlyingField(elem) + .getOrElse(ScalarType.AttributeType) // Matches Context#forFieldOrAttribute copy(path = path :+ elem, tpe = ftpe) } def %(ntpe: NamedType): Path = - if(ntpe <:< tpe) copy(tpe = ntpe) + if (ntpe <:< tpe) copy(tpe = ntpe) else throw new IllegalArgumentException(s"Type $ntpe is not a subtype of $tpe") } } sealed trait PathTerm { - def children: List[Term[_]] = Nil + def children: List[Term[_]] = Nil def path: List[String] } object PathTerm { case class ListPath[A](path: List[String]) extends Term[List[A]] with PathTerm { def apply(c: Cursor): Result[List[A]] = - c.flatListPath(path).flatMap(_.traverse { - case Predicate.ScalarFocus(f) => f.success - case _ => Result.internalError("impossible") - }).asInstanceOf[Result[List[A]]] + c.flatListPath(path) + .flatMap(_.traverse { + case Predicate.ScalarFocus(f) => f.success + case _ => Result.internalError("impossible") + }) + .asInstanceOf[Result[List[A]]] } case class UniquePath[A](path: List[String]) extends Term[A] with PathTerm { def apply(c: Cursor): Result[A] = c.listPath(path).flatMap { case List(Predicate.ScalarFocus(a: A @unchecked)) => a.success - case other => Result.internalError(s"Expected exactly one element for path $path found $other") + case other => + Result.internalError(s"Expected exactly one element for path $path found $other") } } @@ -196,7 +211,7 @@ object Predicate { } case class Not(x: Predicate) extends Predicate { - def apply(c: Cursor): Result[Boolean] = x(c).map(! _) + def apply(c: Cursor): Result[Boolean] = x(c).map(!_) def children = List(x) } @@ -214,7 +229,8 @@ object Predicate { } case class Contains[T: Eq](x: Term[List[T]], y: Term[T]) extends Predicate { - def apply(c: Cursor): Result[Boolean] = Apply[Result].map2(x(c), y(c))((xs, y0) => xs.exists(_ === y0)) + def apply(c: Cursor): Result[Boolean] = + Apply[Result].map2(x(c), y(c))((xs, y0) => xs.exists(_ === y0)) def children = List(x, y) def subst(x: Term[List[T]], y: Term[T]): Contains[T] = copy(x = x, y = y) } @@ -281,7 +297,7 @@ object Predicate { } case class NotB(x: Term[Int]) extends Term[Int] { - def apply(c: Cursor): Result[Int] = x(c).map(~ _) + def apply(c: Cursor): Result[Int] = x(c).map(~_) def children = List(x) } diff --git a/modules/core/src/main/scala/problem.scala b/modules/core/src/main/scala/problem.scala index 1398f5ff..e45d15f9 100644 --- a/modules/core/src/main/scala/problem.scala +++ b/modules/core/src/main/scala/problem.scala @@ -19,12 +19,14 @@ import cats.Eq import io.circe._ import io.circe.syntax._ -/** A problem, to be reported back to the user. */ +/** + * A problem, to be reported back to the user. + */ final case class Problem( - message: String, - locations: List[(Int, Int)] = Nil, - path: List[String] = Nil, - extensions: Option[JsonObject] = None, + message: String, + locations: List[(Int, Int)] = Nil, + path: List[String] = Nil, + extensions: Option[JsonObject] = None ) { override def toString = { @@ -32,14 +34,17 @@ final case class Problem( path.mkString("/") lazy val locationsText: String = - locations.map { case (a, b) => - if (a == b) a.toString else s"$a..$b" - } .mkString(", ") + locations + .map { + case (a, b) => + if (a == b) a.toString else s"$a..$b" + } + .mkString(", ") val s = (path.nonEmpty, locations.nonEmpty) match { - case (true, true) => s"$message (at $pathText: $locationsText)" - case (true, false) => s"$message (at $pathText)" - case (false, true) => s"$message (at $locationsText)" + case (true, true) => s"$message (at $pathText: $locationsText)" + case (true, false) => s"$message (at $pathText)" + case (false, true) => s"$message (at $locationsText)" case (false, false) => message } @@ -52,31 +57,34 @@ final case class Problem( object Problem { implicit val ProblemEncoder: Encoder[Problem] = { p => - val locationsField: List[(String, Json)] = if (p.locations.isEmpty) Nil - else List( - "locations" -> - p.locations.map { case (line, col) => - Json.obj( - "line" -> line.asJson, - "col" -> col.asJson - ) - } .asJson - ) + else + List( + "locations" -> + p.locations + .map { + case (line, col) => + Json.obj( + "line" -> line.asJson, + "col" -> col.asJson + ) + } + .asJson + ) val pathField: List[(String, Json)] = if (p.path.isEmpty) Nil - else List(("path" -> p.path.asJson)) + else List("path" -> p.path.asJson) val extensionsField: List[(String, Json)] = p.extensions.fold(List.empty[(String, Json)])(obj => List("extensions" -> obj.asJson)) Json.fromFields( "message" -> p.message.asJson :: - locationsField ::: - pathField ::: - extensionsField + locationsField ::: + pathField ::: + extensionsField ) } diff --git a/modules/core/src/main/scala/query.scala b/modules/core/src/main/scala/query.scala index d29ae565..126ae9d6 100644 --- a/modules/core/src/main/scala/query.scala +++ b/modules/core/src/main/scala/query.scala @@ -20,12 +20,17 @@ import scala.annotation.tailrec import cats.implicits._ import cats.kernel.Order -import syntax._ -import Query._ +import grackle.Query._ +import grackle.syntax._ -/** GraphQL query Algebra */ +/** + * GraphQL query Algebra + */ sealed trait Query { - /** Groups this query with its argument, Groups on either side are merged */ + + /** + * Groups this query with its argument, Groups on either side are merged + */ def ~(query: Query): Query = (this, query) match { case (Group(hd), Group(tl)) => Group(hd ++ tl) case (hd, Group(tl)) => Group(hd :: tl) @@ -33,18 +38,23 @@ sealed trait Query { case (hd, tl) => Group(List(hd, tl)) } - /** Yields a String representation of this query */ + /** + * Yields a String representation of this query + */ def render: String } object Query { - /** Select field `name` possibly aliased, and continue with `child` */ + + /** + * Select field `name` possibly aliased, and continue with `child` + */ case class Select(name: String, alias: Option[String], child: Query) extends Query { def resultName: String = alias.getOrElse(name) def render = { val rname = s"${alias.map(a => s"$a:").getOrElse("")}$name" - val rchild = if(child == Empty) "" else s" { ${child.render} }" + val rchild = if (child == Empty) "" else s" { ${child.render} }" s"$rname$rchild" } } @@ -60,45 +70,64 @@ object Query { new Select(name, None, child) } - /** Precursor of a `Select` node, containing uncompiled field arguments and directives. */ - case class UntypedSelect(name: String, alias: Option[String], args: List[Binding], directives: List[Directive], child: Query) extends Query { + /** + * Precursor of a `Select` node, containing uncompiled field arguments and directives. + */ + case class UntypedSelect( + name: String, + alias: Option[String], + args: List[Binding], + directives: List[Directive], + child: Query) + extends Query { def resultName: String = alias.getOrElse(name) def render = { val rname = s"${alias.map(a => s"$a:").getOrElse("")}$name" - val rargs = if(args.isEmpty) "" else s"(${args.map(_.render).mkString(", ")})" - val rchild = if(child == Empty) "" else s" { ${child.render} }" + val rargs = if (args.isEmpty) "" else s"(${args.map(_.render).mkString(", ")})" + val rchild = if (child == Empty) "" else s" { ${child.render} }" s"$rname$rargs$rchild" } } - /** A Group of sibling queries at the same level */ + /** + * A Group of sibling queries at the same level + */ case class Group(queries: List[Query]) extends Query { def render = queries.map(_.render).mkString("{", ", ", "}") } - /** Continues with single-element-list-producing `child` and yields the single element */ + /** + * Continues with single-element-list-producing `child` and yields the single element + */ case class Unique(child: Query) extends Query { def render = s"" } - /** Retains only elements satisfying `pred` and continues with `child` */ + /** + * Retains only elements satisfying `pred` and continues with `child` + */ case class Filter(pred: Predicate, child: Query) extends Query { def render = s"" } - /** Identifies a component boundary. - * `join` is applied to the current cursor and `child` yielding a continuation query which will be - * evaluated by the interpreter identified by `componentId`. + /** + * Identifies a component boundary. `join` is applied to the current cursor and `child` + * yielding a continuation query which will be evaluated by the interpreter identified by + * `componentId`. */ - case class Component[F[_]](mapping: Mapping[F], join: (Query, Cursor) => Result[Query], child: Query) extends Query { + case class Component[F[_]]( + mapping: Mapping[F], + join: (Query, Cursor) => Result[Query], + child: Query) + extends Query { def render = s"" } - /** Embeds possibly batched effects. - * `handler` is applied to one or more possibly batched queries and cursors yielding corresponding - * continuation queries and cursors which will be evaluated by the current interpreter in the next - * phase. + /** + * Embeds possibly batched effects. `handler` is applied to one or more possibly batched + * queries and cursors yielding corresponding continuation queries and cursors which will be + * evaluated by the current interpreter in the next phase. */ case class Effect[F[_]](handler: EffectHandler[F], child: Query) extends Query { def render = s"" @@ -108,17 +137,22 @@ object Query { def runEffects(queries: List[(Query, Cursor)]): F[Result[List[Cursor]]] } - /** Evaluates an introspection query relative to `schema` */ + /** + * Evaluates an introspection query relative to `schema` + */ case class Introspect(schema: Schema, child: Query) extends Query { def render = s"" } - /** Add `env` to the environment for the continuation `child` */ + /** + * Add `env` to the environment for the continuation `child` + */ case class Environment(env: Env, child: Query) extends Query { def render = s"" } - /** Representation of a fragment spread prior to comilation. + /** + * Representation of a fragment spread prior to comilation. * * During compilation this node will be replaced by its definition, guarded by a `Narrow` * corresponding to the type condition of the fragment. @@ -127,12 +161,17 @@ object Query { def render = s"" } - /** Representation of an inline fragment prior to comilation. + /** + * Representation of an inline fragment prior to comilation. * * During compilation this node will be replaced by its child, guarded by a `Narrow` * corresponding to the type condition of the fragment, if any. */ - case class UntypedInlineFragment(tpnme: Option[String], directives: List[Directive], child: Query) extends Query { + case class UntypedInlineFragment( + tpnme: Option[String], + directives: List[Directive], + child: Query) + extends Query { def render = s" s"on $tc ").getOrElse("")} ${child.render}>" } @@ -143,18 +182,23 @@ object Query { def render = s"" } - /** Limits the results of list-producing continuation `child` to `num` elements */ + /** + * Limits the results of list-producing continuation `child` to `num` elements + */ case class Limit(num: Int, child: Query) extends Query { def render = s"" } - /** Drops the first `num` elements of list-producing continuation `child`. */ + /** + * Drops the first `num` elements of list-producing continuation `child`. + */ case class Offset(num: Int, child: Query) extends Query { def render = s"" } - /** Orders the results of list-producing continuation `child` by fields - * specified by `selections`. + /** + * Orders the results of list-producing continuation `child` by fields specified by + * `selections`. */ case class OrderBy(selections: OrderSelections, child: Query) extends Query { def render = s"" @@ -181,7 +225,10 @@ object Query { } } - case class OrderSelection[T: Order](term: Term[T], ascending: Boolean = true, nullsLast: Boolean = true) { + case class OrderSelection[T: Order]( + term: Term[T], + ascending: Boolean = true, + nullsLast: Boolean = true) { def apply(x: Cursor, y: Cursor): Int = { def deref(c: Cursor): Option[T] = if (c.isNullable) c.asNullable.toOption.flatten.flatMap(term(_).toOption) @@ -189,8 +236,8 @@ object Query { (deref(x), deref(y)) match { case (None, None) => 0 - case (_, None) => (if (nullsLast) -1 else 1) - case (None, _) => (if (nullsLast) 1 else -1) + case (_, None) => if (nullsLast) -1 else 1 + case (None, _) => if (nullsLast) 1 else -1 case (Some(x0), Some(y0)) => val ord = Order[T].compare(x0, y0) if (ascending) ord @@ -201,20 +248,23 @@ object Query { def subst(term: Term[T]): OrderSelection[T] = copy(term = term) } - /** Computes the number of top-level elements of `child` */ + /** + * Computes the number of top-level elements of `child` + */ case class Count(child: Query) extends Query { def render = s"count { ${child.render} }" } /** - * Uses the supplied function to compute a continuation `Cursor` from the - * current `Cursor`. + * Uses the supplied function to compute a continuation `Cursor` from the current `Cursor`. */ case class TransformCursor(f: Cursor => Result[Cursor], child: Query) extends Query { def render = s"" } - /** The terminal query */ + /** + * The terminal query + */ case object Empty extends Query { def render = "" } @@ -227,48 +277,61 @@ object Query { type VarDefs = List[InputValue] type Vars = Map[String, (Type, Value)] - /** Precursor of a variable definition before compilation */ - case class UntypedVarDef(name: String, tpe: Ast.Type, default: Option[Value], directives: List[Directive]) + /** + * Precursor of a variable definition before compilation + */ + case class UntypedVarDef( + name: String, + tpe: Ast.Type, + default: Option[Value], + directives: List[Directive]) - /** Precursor of a fragment definition before compilation */ - case class UntypedFragment(name: String, tpnme: String, directives: List[Directive], child: Query) + /** + * Precursor of a fragment definition before compilation + */ + case class UntypedFragment( + name: String, + tpnme: String, + directives: List[Directive], + child: Query) def renameRoot(q: Query, rootName: String): Option[Query] = q match { case sel: Select if sel.resultName == rootName => Some(sel) - case sel: Select => Some(sel.copy(alias = Some(rootName))) - case e@Environment(_, child) => renameRoot(child, rootName).map(rc => e.copy(child = rc)) - case t@TransformCursor(_, child) => renameRoot(child, rootName).map(rc => t.copy(child = rc)) + case sel: Select => Some(sel.copy(alias = Some(rootName))) + case e @ Environment(_, child) => renameRoot(child, rootName).map(rc => e.copy(child = rc)) + case t @ TransformCursor(_, child) => + renameRoot(child, rootName).map(rc => t.copy(child = rc)) case _ => None } /** - * Computes the root name and optional alias of the supplied query - * if it is unique, `None` otherwise. - */ + * Computes the root name and optional alias of the supplied query if it is unique, `None` + * otherwise. + */ def rootName(q: Query): Option[(String, Option[String])] = { def loop(q: Query): Option[(String, Option[String])] = q match { case UntypedSelect(name, alias, _, _, _) => Some((name, alias)) - case Select(name, alias, _) => Some((name, alias)) - case Environment(_, child) => loop(child) - case TransformCursor(_, child) => loop(child) - case _ => None + case Select(name, alias, _) => Some((name, alias)) + case Environment(_, child) => loop(child) + case TransformCursor(_, child) => loop(child) + case _ => None } loop(q) } /** - * Computes the possibly aliased result name of the supplied query - * if it is unique, `None` otherwise. - */ + * Computes the possibly aliased result name of the supplied query if it is unique, `None` + * otherwise. + */ def resultName(q: Query): Option[String] = { def loop(q: Query): Option[String] = q match { case UntypedSelect(name, alias, _, _, _) => Some(alias.getOrElse(name)) - case Select(name, alias, _) => Some(alias.getOrElse(name)) - case Environment(_, child) => loop(child) - case TransformCursor(_, child) => loop(child) - case _ => None + case Select(name, alias, _) => Some(alias.getOrElse(name)) + case Environment(_, child) => loop(child) + case TransformCursor(_, child) => loop(child) + case _ => None } loop(q) } @@ -279,8 +342,8 @@ object Query { } /** - * Renames the root of `target` to match `source` if possible. - */ + * Renames the root of `target` to match `source` if possible. + */ def alignResultName(source: Query, target: Query): Option[Query] = for { nme <- resultName(source) @@ -288,8 +351,7 @@ object Query { } yield res /** - * Yields a list of the top level queries of the supplied, possibly - * grouped query. + * Yields a list of the top level queries of the supplied, possibly grouped query. */ def ungroup(query: Query): List[Query] = query match { @@ -300,7 +362,7 @@ object Query { def groupWithTypeCase(query: Query): Boolean = { @tailrec def loop(qs: Iterator[Query]): Boolean = - if(!qs.hasNext) false + if (!qs.hasNext) false else qs.next() match { case _: Narrow => true @@ -318,24 +380,24 @@ object Query { def loop(q: Query): List[Query] = q match { case UntypedSelect(_, _, _, _, child) => ungroup(child) - case Select(_, _, child) => ungroup(child) - case Environment(_, child) => loop(child) + case Select(_, _, child) => ungroup(child) + case Environment(_, child) => loop(child) case TransformCursor(_, child) => loop(child) - case _ => Nil + case _ => Nil } loop(q) } /** - * Yields the top-level field selection of the supplied Query - * if it is unique, `None` otherwise. + * Yields the top-level field selection of the supplied Query if it is unique, `None` + * otherwise. */ def extractChild(query: Query): Option[Query] = { def loop(q: Query): Option[Query] = q match { case UntypedSelect(_, _, _, _, child) => Some(child) - case Select(_, _, child) => Some(child) - case Environment(_, child) => loop(child) + case Select(_, _, child) => Some(child) + case Environment(_, child) => loop(child) case TransformCursor(_, child) => loop(child) case _ => None } @@ -343,50 +405,47 @@ object Query { } /** - * Yields the supplied query with its the top-level field selection - * of the supplied replaced with `newChild` if it is unique, `None` - * otherwise. - */ + * Yields the supplied query with its the top-level field selection of the supplied replaced + * with `newChild` if it is unique, `None` otherwise. + */ def substChild(query: Query, newChild: Query): Option[Query] = { def loop(q: Query): Option[Query] = q match { - case u: UntypedSelect => Some(u.copy(child = newChild)) - case s: Select => Some(s.copy(child = newChild)) - case e@Environment(_, child) => loop(child).map(c => e.copy(child = c)) - case t@TransformCursor(_, child) => loop(child).map(c => t.copy(child = c)) + case u: UntypedSelect => Some(u.copy(child = newChild)) + case s: Select => Some(s.copy(child = newChild)) + case e @ Environment(_, child) => loop(child).map(c => e.copy(child = c)) + case t @ TransformCursor(_, child) => loop(child).map(c => t.copy(child = c)) case _ => None } loop(query) } /** - * True if `fieldName` is a top-level selection of the supplied query, - * false otherwise. + * True if `fieldName` is a top-level selection of the supplied query, false otherwise. */ def hasField(query: Query, fieldName: String): Boolean = { def loop(q: Query): Boolean = ungroup(q).exists { case UntypedSelect(`fieldName`, _, _, _, _) => true case Select(`fieldName`, _, _) => true - case Environment(_, child) => loop(child) + case Environment(_, child) => loop(child) case TransformCursor(_, child) => loop(child) - case _ => false + case _ => false } loop(query) } /** - * Returns the alias, if any, of the top-level field `fieldName` in - * the supplied query. + * Returns the alias, if any, of the top-level field `fieldName` in the supplied query. */ def fieldAlias(query: Query, fieldName: String): Option[String] = { def loop(q: Query): Option[String] = ungroup(q).collectFirstSome { case UntypedSelect(`fieldName`, alias, _, _, _) => alias case Select(`fieldName`, alias, _) => alias - case Environment(_, child) => loop(child) + case Environment(_, child) => loop(child) case TransformCursor(_, child) => loop(child) - case _ => None + case _ => None } loop(query) } @@ -399,8 +458,8 @@ object Query { q match { case Group(qs) => Group(qs.map(loop)) case s: Select => f(s) - case e@Environment(_, child) => e.copy(child = loop(child)) - case t@TransformCursor(_, child) => t.copy(child = loop(child)) + case e @ Environment(_, child) => e.copy(child = loop(child)) + case t @ TransformCursor(_, child) => t.copy(child = loop(child)) case other => other } loop(query) @@ -414,26 +473,32 @@ object Query { q match { case Group(qs) => qs.traverse(loop).map(Group(_)) case s: Select => f(s) - case e@Environment(_, child) => loop(child).map(ec => e.copy(child = ec)) - case t@TransformCursor(_, child) => loop(child).map(ec => t.copy(child = ec)) + case e @ Environment(_, child) => loop(child).map(ec => e.copy(child = ec)) + case t @ TransformCursor(_, child) => loop(child).map(ec => t.copy(child = ec)) case other => other.success } loop(query) } - /** Constructor/extractor for nested Filter/OrderBy/Limit/Offset patterns - * in the query algebra - **/ + /** + * Constructor/extractor for nested Filter/OrderBy/Limit/Offset patterns in the query algebra + */ object FilterOrderByOffsetLimit { - def apply(pred: Option[Predicate], oss: Option[List[OrderSelection[_]]], offset: Option[Int], limit: Option[Int], child: Query): Query = { - val filter = pred.map(p => Filter(p, child)).getOrElse(child) - val order = oss.map(o => OrderBy(OrderSelections(o), filter)).getOrElse(filter) - val off = offset.map(n => Offset(n, order)).getOrElse(order) - val lim = limit.map(n => Limit(n, off)).getOrElse(off) + def apply( + pred: Option[Predicate], + oss: Option[List[OrderSelection[_]]], + offset: Option[Int], + limit: Option[Int], + child: Query): Query = { + val filter = pred.map(p => Filter(p, child)).getOrElse(child) + val order = oss.map(o => OrderBy(OrderSelections(o), filter)).getOrElse(filter) + val off = offset.map(n => Offset(n, order)).getOrElse(order) + val lim = limit.map(n => Limit(n, off)).getOrElse(off) lim } - def unapply(q: Query): Option[(Option[Predicate], Option[List[OrderSelection[_]]], Option[Int], Option[Int], Query)] = { + def unapply(q: Query): Option[ + (Option[Predicate], Option[List[OrderSelection[_]]], Option[Int], Option[Int], Query)] = { val (limit, q0) = q match { case Limit(lim, child) => (Some(lim), child) case child => (None, child) @@ -450,12 +515,14 @@ object Query { case Filter(pred, child) => (Some(pred), child) case child => (None, child) } - if(limit.isEmpty && offset.isEmpty && order.isEmpty && filter.isEmpty) None + if (limit.isEmpty && offset.isEmpty && order.isEmpty && filter.isEmpty) None else Some((filter, order, offset, limit, q3)) } } - /** Construct a query which yields all the supplied paths */ + /** + * Construct a query which yields all the supplied paths + */ def mkPathQuery(paths: List[List[String]]): List[Query] = paths match { case Nil => Nil @@ -465,16 +532,22 @@ object Query { val multiElemPaths = paths.filter(_.length > 1).distinct val grouped: List[Query] = multiElemPaths.groupBy(_.head).toList.map { case (fieldName, suffixes) => - Select(fieldName, mergeQueries(mkPathQuery(suffixes.map(_.tail).filterNot(_.isEmpty)))) + Select( + fieldName, + mergeQueries(mkPathQuery(suffixes.map(_.tail).filterNot(_.isEmpty)))) } oneElemQueries ++ grouped } - /** Merge the given queries as a single query */ + /** + * Merge the given queries as a single query + */ def mergeQueries(qs: List[Query]): Query = TypedQueryMerger.merge(qs) - /** Merge the given untyped queries as a single untyped query */ + /** + * Merge the given untyped queries as a single untyped query + */ def mergeUntypedQueries(qs: List[Query]): Query = UntypedQueryMerger.merge(qs) @@ -491,14 +564,15 @@ object Query { qs.foldLeft((Set.empty[(String, Option[String])], List.empty[Query])) { case ((seen, acc), q) => q match { - case Key(key@(_, _)) => + case Key(key @ (_, _)) => if (seen.contains(key)) (seen, acc) else (seen + key, groupedSelects(key) :: acc) case Narrow(tpe, child) => (seen, Narrow(tpe, merge(List(child))) :: acc) case elem => (seen, elem :: acc) } - }._2.reverse + }._2 + .reverse val mergedSelects = mergeInOrder(flattened) @@ -507,25 +581,29 @@ object Query { case List(one) => one case qs => Group(qs) } - } + } } def flattenLevel(qs: List[Query]): List[Query] = { @tailrec - def loop(qs: Iterator[Query], prevNarrow: Option[(TypeRef, List[Query])], acc: List[Query]): List[Query] = { + def loop( + qs: Iterator[Query], + prevNarrow: Option[(TypeRef, List[Query])], + acc: List[Query]): List[Query] = { def addNarrow: List[Query] = prevNarrow match { case None => acc - case Some((tpe, List(child))) => (Narrow(tpe, child) :: acc) - case Some((tpe, children)) => (Narrow(tpe, Group(children.reverse)) :: acc) + case Some((tpe, List(child))) => Narrow(tpe, child) :: acc + case Some((tpe, children)) => Narrow(tpe, Group(children.reverse)) :: acc } - if(!qs.hasNext) addNarrow.reverse + if (!qs.hasNext) addNarrow.reverse else qs.next() match { case Narrow(tpe, child) => prevNarrow match { case None => loop(qs, Some((tpe, List(child))), acc) - case Some((tpe0, children)) if tpe0.name == tpe.name => loop(qs, Some((tpe, child :: children)), acc) + case Some((tpe0, children)) if tpe0.name == tpe.name => + loop(qs, Some((tpe, child :: children)), acc) case _ => loop(qs, Some((tpe, List(child))), addNarrow) } case Group(gs) => loop(gs.iterator ++ qs, prevNarrow, acc) @@ -548,7 +626,12 @@ object Query { } def groupSelects(qs: List[Query]): Map[(String, Option[String]), Query] = { - val selects = qs.filter { case _: Select => true case _ => false }.asInstanceOf[List[Select]] + val selects = qs + .filter { + case _: Select => true + case _ => false + } + .asInstanceOf[List[Select]] selects.groupBy(sel => (sel.name, sel.alias)).view.mapValues(mergeSelects).toMap } @@ -566,7 +649,12 @@ object Query { } def groupSelects(qs: List[Query]): Map[(String, Option[String]), Query] = { - val selects = qs.filter { case _: UntypedSelect => true case _ => false }.asInstanceOf[List[UntypedSelect]] + val selects = qs + .filter { + case _: UntypedSelect => true + case _ => false + } + .asInstanceOf[List[UntypedSelect]] selects.groupBy(sel => (sel.name, sel.alias)).view.mapValues(mergeSelects).toMap } diff --git a/modules/core/src/main/scala/queryinterpreter.scala b/modules/core/src/main/scala/queryinterpreter.scala index 868fa574..5cad9eff 100644 --- a/modules/core/src/main/scala/queryinterpreter.scala +++ b/modules/core/src/main/scala/queryinterpreter.scala @@ -20,63 +20,72 @@ import scala.collection.mutable import scala.jdk.CollectionConverters._ import cats.{Monad, Monoid} -import cats.data.{ Chain, NonEmptyChain } +import cats.data.{Chain, NonEmptyChain} import cats.implicits._ import fs2.Stream import io.circe.Json -import syntax._ -import Cursor.ListTransformCursor -import Query._ -import QueryInterpreter.ProtoJson -import ProtoJson._ +import grackle.Cursor.ListTransformCursor +import grackle.Query._ +import grackle.QueryInterpreter.ProtoJson +import grackle.QueryInterpreter.ProtoJson._ +import grackle.syntax._ class QueryInterpreter[F[_]](mapping: Mapping[F]) { import mapping.{M, RootCursor, RootEffect, RootStream} - /** Interpret `query` with expected type `rootTpe`. + /** + * Interpret `query` with expected type `rootTpe`. * - * The query is fully interpreted, including deferred or staged - * components. + * The query is fully interpreted, including deferred or staged components. * - * GraphQL errors are accumulated in the result. + * GraphQL errors are accumulated in the result. */ def run(query: Query, rootTpe: Type, env: Env): Stream[F, Result[Json]] = { val rootCursor = RootCursor(Context(rootTpe), None, env) val mergedResults = - if(mapping.schema.subscriptionType.exists(_ =:= rootTpe)) + if (mapping.schema.subscriptionType.exists(_ =:= rootTpe)) runSubscription(query, rootTpe, rootCursor) else Stream.eval(runOneShot(query, rootTpe, rootCursor)) (for { pvalue <- ResultT(mergedResults) - value <- ResultT(Stream.eval(QueryInterpreter.complete[F](pvalue))) + value <- ResultT(Stream.eval(QueryInterpreter.complete[F](pvalue))) } yield value).value } /** - * Run a subscription query yielding a stream of results. + * Run a subscription query yielding a stream of results. */ - def runSubscription(query: Query, rootTpe: Type, rootCursor: Cursor): Stream[F, Result[ProtoJson]] = + def runSubscription( + query: Query, + rootTpe: Type, + rootCursor: Cursor): Stream[F, Result[ProtoJson]] = ungroup(query) match { case Nil => Result(ProtoJson.fromJson(Json.Null)).pure[Stream[F, *]] case List(root) => - (for { - rootName <- Query.rootName(root) - RootStream(fieldName, effect) <- mapping.rootStream(Context(rootTpe), rootName._1).orElse(mapping.rootEffect(Context(rootTpe), rootName._1).map(_.toRootStream)) - } yield - effect(root, rootTpe / fieldName, rootCursor.fullEnv.addFromQuery(root)).map(_.flatMap { // TODO Rework in terms of cursor + ( + for { + rootName <- Query.rootName(root) + RootStream(fieldName, effect) <- mapping + .rootStream(Context(rootTpe), rootName._1) + .orElse(mapping.rootEffect(Context(rootTpe), rootName._1).map(_.toRootStream)) + } yield effect(root, rootTpe / fieldName, rootCursor.fullEnv.addFromQuery(root)) + .map(_.flatMap { // TODO Rework in terms of cursor case (q, c) => runValue(q, rootTpe, c) }) - ).getOrElse(Result.internalError("EffectMapping required for subscriptions").pure[Stream[F, *]]) + ).getOrElse( + Result.internalError("EffectMapping required for subscriptions").pure[Stream[F, *]]) case _ => - Result.internalError("Only one root selection permitted for subscriptions").pure[Stream[F, *]] + Result + .internalError("Only one root selection permitted for subscriptions") + .pure[Stream[F, *]] } /** - * Run a non-subscription query yielding a single result. + * Run a non-subscription query yielding a single result. */ def runOneShot(query: Query, rootTpe: Type, rootCursor: Cursor): F[Result[ProtoJson]] = { case class PureQuery(query: Query) @@ -86,21 +95,24 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { val ungrouped = ungroup(query) val hasRootStream = ungrouped.exists { root => - Query.rootName(root).flatMap(rootName => mapping.rootStream(rootContext, rootName._1)).isDefined + Query + .rootName(root) + .flatMap(rootName => mapping.rootStream(rootContext, rootName._1)) + .isDefined } - if(hasRootStream) + if (hasRootStream) Result.internalError("RootStream only permitted in subscriptions").pure[F].widen else { val (effectfulQueries, pureQueries) = ungrouped.partitionMap { query => (for { rootName <- Query.rootName(query) - re <- mapping.rootEffect(rootContext, rootName._1) + re <- mapping.rootEffect(rootContext, rootName._1) } yield Left(EffectfulQuery(query, re))).getOrElse(Right(PureQuery(query))) } val pureResults: F[List[Result[ProtoJson]]] = - if(pureQueries.isEmpty) Nil.pure[F].widen + if (pureQueries.isEmpty) Nil.pure[F].widen else { val (introQueries, nonIntroQueries) = pureQueries.partitionMap { case PureQuery(i: Introspect) => Left(i) @@ -128,45 +140,43 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { } val effectfulResults: F[List[Result[ProtoJson]]] = - if(effectfulQueries.isEmpty) Nil.pure[F].widen + if (effectfulQueries.isEmpty) Nil.pure[F].widen else { effectfulQueries.traverse { case EffectfulQuery(query, RootEffect(fieldName, effect)) => - effect(query, rootTpe / fieldName, rootCursor.fullEnv.addFromQuery(query)).map(_.flatMap { // TODO Rework in terms of cursor - case (q, c) => runValue(q, rootTpe, c) - }) + effect(query, rootTpe / fieldName, rootCursor.fullEnv.addFromQuery(query)) + .map(_.flatMap { // TODO Rework in terms of cursor + case (q, c) => runValue(q, rootTpe, c) + }) } } for { pr <- pureResults er <- effectfulResults - } yield - ((pr ++ er) match { - case Nil => Result(ProtoJson.fromJson(Json.Null)) - case List(r) => r - case hd :: tl => - tl.foldLeft(hd) { - case (acc, elem) => acc |+| elem - } - }) match { - case Result.Failure(errs) => Result.Warning(errs, ProtoJson.fromJson(Json.Null)) - case other => other - } + } yield ((pr ++ er) match { + case Nil => Result(ProtoJson.fromJson(Json.Null)) + case List(r) => r + case hd :: tl => + tl.foldLeft(hd) { case (acc, elem) => acc |+| elem } + }) match { + case Result.Failure(errs) => Result.Warning(errs, ProtoJson.fromJson(Json.Null)) + case other => other + } } } - /** Interpret `query` with expected type `rootTpe`. + /** + * Interpret `query` with expected type `rootTpe`. * - * At most one stage will be run and the result may contain deferred - * components. + * At most one stage will be run and the result may contain deferred components. * - * Errors are accumulated on the `Left` of the result. + * Errors are accumulated on the `Left` of the result. */ def runRootValue(query: Query, rootTpe: Type, parentCursor: Cursor): F[Result[ProtoJson]] = (for { - qc <- ResultT(mapping.defaultRootCursor(query, rootTpe, Some(parentCursor))) - value <- ResultT(runValue(qc._1, rootTpe, qc._2).pure[F]) + qc <- ResultT(mapping.defaultRootCursor(query, rootTpe, Some(parentCursor))) + value <- ResultT(runValue(qc._1, rootTpe, qc._2).pure[F]) } yield value).value def cursorCompatible(tpe: Type, cursorTpe: Type): Boolean = { @@ -184,10 +194,9 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { /** * Interpret `query` against `cursor`, yielding a collection of fields. * - * If the query is valid, the field subqueries will all be valid fields - * of the enclosing type `tpe` and the resulting fields may be used to - * build a Json object of type `tpe`. If the query is invalid errors - * will be returned on the left hand side of the result. + * If the query is valid, the field subqueries will all be valid fields of the enclosing type + * `tpe` and the resulting fields may be used to build a Json object of type `tpe`. If the + * query is invalid errors will be returned on the left hand side of the result. */ def runFields(query: Query, tpe: Type, cursor: Cursor): Result[List[(String, ProtoJson)]] = if (!cursorCompatible(tpe, cursor.tpe)) @@ -195,13 +204,16 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { else { query match { case g: Group if groupWithTypeCase(g) => - ungroup(g).flatTraverse(query => runFields(query, tpe, cursor)).map(fs => mergeFields(fs).toList) + ungroup(g) + .flatTraverse(query => runFields(query, tpe, cursor)) + .map(fs => mergeFields(fs).toList) case Group(siblings) => siblings.flatTraverse(query => runFields(query, tpe, cursor)) - case Introspect(schema, s@Select("__typename", _, Empty)) if tpe.isNamed => - val fail = Result.failure(s"'__typename' cannot be applied to non-selectable type '$tpe'") + case Introspect(schema, s @ Select("__typename", _, Empty)) if tpe.isNamed => + val fail = + Result.failure(s"'__typename' cannot be applied to non-selectable type '$tpe'") def mkTypeNameFields(name: String) = List((s.resultName, ProtoJson.fromJson(Json.fromString(name)))).success def mkTypeNameFieldsOrFail(name: Option[String]) = @@ -210,45 +222,58 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { tpe.dealias match { case o: ObjectType => mkTypeNameFields(o.name) case i: InterfaceType => - schema.implementations(i).collectFirstSomeM { o => - cursor.narrowsTo(schema.uncheckedRef(o)).ifF(Some(o.name), None) - }.flatMap(mkTypeNameFieldsOrFail) + schema + .implementations(i) + .collectFirstSomeM { o => + cursor.narrowsTo(schema.uncheckedRef(o)).ifF(Some(o.name), None) + } + .flatMap(mkTypeNameFieldsOrFail) case u: UnionType => - u.members.map(_.dealias).collectFirstSomeM { nt => - cursor.narrowsTo(schema.uncheckedRef(nt)).ifF(Some(nt.name), None) - }.flatMap(mkTypeNameFieldsOrFail) + u.members + .map(_.dealias) + .collectFirstSomeM { nt => + cursor.narrowsTo(schema.uncheckedRef(nt)).ifF(Some(nt.name), None) + } + .flatMap(mkTypeNameFieldsOrFail) case _ => fail } case sel: Select if tpe.isNullable => - cursor.asNullable.sequence.map { rc => - for { - c <- rc - fields <- runFields(sel, tpe, c) - } yield fields - }.getOrElse(List((sel.resultName, ProtoJson.fromJson(Json.Null))).success) + cursor + .asNullable + .sequence + .map { rc => + for { + c <- rc + fields <- runFields(sel, tpe, c) + } yield fields + } + .getOrElse(List((sel.resultName, ProtoJson.fromJson(Json.Null))).success) - case sel@Select(_, _, Count(Select(countName, _, _))) => + case sel @ Select(_, _, Count(Select(countName, _, _))) => def size(c: Cursor): Result[Int] = if (c.isList) c.asList(Iterator).map(_.size) else 1.success for { - c0 <- cursor.field(countName, None) - count <- if (c0.isNullable) c0.asNullable.flatMap(_.map(size).getOrElse(0.success)) - else size(c0) + c0 <- cursor.field(countName, None) + count <- + if (c0.isNullable) c0.asNullable.flatMap(_.map(size).getOrElse(0.success)) + else size(c0) } yield List((sel.resultName, ProtoJson.fromJson(Json.fromInt(count)))) - case sel@Select(_, _, Effect(handler, cont)) => + case sel @ Select(_, _, Effect(handler, cont)) => for { - value <- ProtoJson.effect(mapping, handler.asInstanceOf[EffectHandler[F]], cont, cursor).success + value <- ProtoJson + .effect(mapping, handler.asInstanceOf[EffectHandler[F]], cont, cursor) + .success } yield List((sel.resultName, value)) - case sel@Select(fieldName, resultName, child) => + case sel @ Select(fieldName, resultName, child) => val fieldTpe = tpe.field(fieldName).getOrElse(ScalarType.AttributeType) for { - c <- cursor.field(fieldName, resultName) - value <- runValue(child, fieldTpe, c) + c <- cursor.field(fieldName, resultName) + value <- runValue(child, fieldTpe, c) } yield List((sel.resultName, value)) case Narrow(tp1, child) => @@ -256,14 +281,15 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { if (!n) Nil.success else for { - c <- cursor.narrow(tp1) + c <- cursor.narrow(tp1) fields <- runFields(child, tp1, c) } yield fields } - case c@Component(_, _, cont) => + case c @ Component(_, _, cont) => for { - componentName <- resultName(cont).toResultOrError("Join continuation has unexpected shape") + componentName <- resultName(cont).toResultOrError( + "Join continuation has unexpected shape") value <- runValue(c, tpe, cursor) } yield List((componentName, ProtoJson.select(value, componentName))) @@ -272,7 +298,7 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { case TransformCursor(f, child) => for { - ct <- f(cursor) + ct <- f(cursor) fields <- runFields(child, tpe, ct) } yield fields @@ -281,7 +307,12 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { } } - def runList(query: Query, tpe: Type, parent: Cursor, unique: Boolean, nullable: Boolean): Result[ProtoJson] = { + def runList( + query: Query, + tpe: Type, + parent: Cursor, + unique: Boolean, + nullable: Boolean): Result[ProtoJson] = { val (query0, f) = query match { case TransformCursor(f, child) => (child, Some(f)) @@ -301,15 +332,15 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { query0 match { case FilterOrderByOffsetLimit(pred, selections, offset, limit, child) => val sorted = - if(pred.isEmpty && selections.isEmpty) cursors + if (pred.isEmpty && selections.isEmpty) cursors else { val cs = cursors.toSeq val filtered = pred match { case Some(p) => cs.filterA(p(_)) match { - case err@Result.InternalError(_) => return err - case fail@Result.Failure(_) => return fail + case err @ Result.InternalError(_) => return err + case fail @ Result.Failure(_) => return fail case Result.Success(cs) => cs case Result.Warning(_, cs) => cs } @@ -321,7 +352,7 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { case (None, None) => sorted case (Some(off), None) => sorted.drop(off) case (None, Some(lim)) => sorted.take(lim) - case (Some(off), Some(lim)) => sorted.slice(off, off+lim) + case (Some(off), Some(lim)) => sorted.slice(off, off + lim) } transformElems(sliced).map(cs => (child, cs)) case other => @@ -333,14 +364,15 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { val builder = Vector.newBuilder[ProtoJson] var problems = Chain.empty[Problem] builder.sizeHint(ic.knownSize) - while(ic.hasNext) { + while (ic.hasNext) { val c = ic.next() if (!cursorCompatible(tpe, c.tpe)) - return Result.internalError(s"Mismatched query and cursor type in runList: $tpe ${c.tpe}") + return Result.internalError( + s"Mismatched query and cursor type in runList: $tpe ${c.tpe}") runValue(child, tpe, c) match { - case err@Result.InternalError(_) => return err - case fail@Result.Failure(_) => return fail + case err @ Result.InternalError(_) => return err + case fail @ Result.Failure(_) => return fail case Result.Success(v) => builder.addOne(v) case Result.Warning(ps, v) => builder.addOne(v) @@ -349,32 +381,34 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { } def mkResult(j: ProtoJson): Result[ProtoJson] = - NonEmptyChain.fromChain(problems).map(neps => Result.Warning(neps, j)).getOrElse(j.success) + NonEmptyChain + .fromChain(problems) + .map(neps => Result.Warning(neps, j)) + .getOrElse(j.success) if (!unique) mkResult(ProtoJson.fromValues(builder.result())) else { val size = builder.knownSize if (size == 1) mkResult(builder.result()(0)) else if (size == 0) { - if(nullable) mkResult(ProtoJson.fromJson(Json.Null)) + if (nullable) mkResult(ProtoJson.fromJson(Json.Null)) else Result.internalError(s"No match") } else Result.internalError(s"Multiple matches") } } for { - cursors <- parent.asList(Iterator) - ccs <- applyOps(cursors) - (child, cs) = ccs - res <- mkResult(child, cs) + cursors <- parent.asList(Iterator) + ccs <- applyOps(cursors) + (child, cs) = ccs + res <- mkResult(child, cs) } yield res } /** * Interpret `query` against `cursor` with expected type `tpe`. * - * If the query is invalid errors will be returned on the left hand side - * of the result. + * If the query is invalid errors will be returned on the left hand side of the result. */ def runValue(query: Query, tpe: Type, cursor: Cursor): Result[ProtoJson] = { if (!cursorCompatible(tpe, cursor.tpe)) @@ -389,7 +423,7 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { case Result.Success(ic) => val builder = Vector.newBuilder[ProtoJson] builder.sizeHint(ic.knownSize) - while(ic.hasNext) { + while (ic.hasNext) { val c = ic.next() runValue(query, tpe, c) match { case Result.Success(v) => builder.addOne(v) @@ -398,61 +432,65 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { } ProtoJson.fromValues(builder.result()).success case Result.Warning(ps, _) => Result.Failure(ps) - case fail@Result.Failure(_) => fail - case err@Result.InternalError(_) => err + case fail @ Result.Failure(_) => fail + case err @ Result.InternalError(_) => err } case (Component(mapping, join, child), _) => join(child, cursor).flatMap { case Group(conts) => for { - childName <- resultName(child).toResultOrError("Join child has unexpected shape") - elems <- conts.traverse { case cont => - for { - componentName <- resultName(cont).toResultOrError("Join continuation has unexpected shape") - } yield - ProtoJson.select( - ProtoJson.component(mapping, cont, cursor), - componentName - ) - } - } yield - ProtoJson.fromDisjointFields( - List(childName -> ProtoJson.fromValues(elems)) - ) + childName <- resultName(child).toResultOrError( + "Join child has unexpected shape") + elems <- conts.traverse { + case cont => + for { + componentName <- resultName(cont).toResultOrError( + "Join continuation has unexpected shape") + } yield ProtoJson.select( + ProtoJson.component(mapping, cont, cursor), + componentName + ) + } + } yield ProtoJson.fromDisjointFields( + List(childName -> ProtoJson.fromValues(elems)) + ) case cont => for { - renamedCont <- alignResultName(child, cont).toResultOrError("Join continuation has unexpected shape") + renamedCont <- alignResultName(child, cont).toResultOrError( + "Join continuation has unexpected shape") } yield ProtoJson.component(mapping, renamedCont, cursor) } case (Unique(child), _) => - cursor.preunique.flatMap(c => - runList(child, tpe.nonNull, c, true, tpe.isNullable) - ) + cursor.preunique.flatMap(c => runList(child, tpe.nonNull, c, true, tpe.isNullable)) case (_, ListType(tpe)) => runList(query, tpe, cursor, false, false) case (TransformCursor(f, child), _) => for { - ct <- f(cursor) + ct <- f(cursor) value <- runValue(child, tpe, ct) } yield value case (_, NullableType(tpe)) => - cursor.asNullable.sequence.map { rc => - for { - c <- rc - value <- runValue(query, tpe, c) - } yield value - }.getOrElse(ProtoJson.fromJson(Json.Null).success) + cursor + .asNullable + .sequence + .map { rc => + for { + c <- rc + value <- runValue(query, tpe, c) + } yield value + } + .getOrElse(ProtoJson.fromJson(Json.Null).success) - case (_, (_: ScalarType) | (_: EnumType)) => + case (_, _: ScalarType | _: EnumType) => cursor.asLeaf.map(ProtoJson.fromJson) - case (_, (_: ObjectType) | (_: InterfaceType) | (_: UnionType)) => + case (_, _: ObjectType | _: InterfaceType | _: UnionType) => runFields(query, tpe, cursor).map(ProtoJson.fromDisjointFields) case _ => @@ -463,19 +501,25 @@ class QueryInterpreter[F[_]](mapping: Mapping[F]) { } object QueryInterpreter { + /** * Opaque type of partially constructed query results. * - * Values may be fully expanded Json values, objects or arrays which not - * yet fully evaluated subtrees, or subqueries which are deferred to the - * next stage or another component of a composite interpreter. + * Values may be fully expanded Json values, objects or arrays which not yet fully evaluated + * subtrees, or subqueries which are deferred to the next stage or another component of a + * composite interpreter. */ type ProtoJson <: AnyRef object ProtoJson { private[QueryInterpreter] sealed trait DeferredJson // A result which depends on an effect and a continuation in the next stage of this or another interpreter. - private[QueryInterpreter] case class EffectJson[F[_]](mapping: Mapping[F], handler: Option[EffectHandler[F]], query: Query, cursor: Cursor) extends DeferredJson + private[QueryInterpreter] case class EffectJson[F[_]]( + mapping: Mapping[F], + handler: Option[EffectHandler[F]], + query: Query, + cursor: Cursor) + extends DeferredJson // A partially constructed object which has at least one deferred subtree. private[QueryInterpreter] case class ProtoObject(fields: Seq[(String, ProtoJson)]) // A partially constructed array which has at least one deferred element. @@ -486,17 +530,22 @@ object QueryInterpreter { implicit val monoidInstance: Monoid[ProtoJson] = new Monoid[ProtoJson] { val empty: ProtoJson = fromJson(Json.Null) - def combine(x: ProtoJson, y: ProtoJson): ProtoJson = ProtoJson.mergeProtoJson(List(x, y)) + def combine(x: ProtoJson, y: ProtoJson): ProtoJson = + ProtoJson.mergeProtoJson(List(x, y)) } /** - * Delegate `query` to the interpreter `interpreter`. When evaluated by - * that interpreter the query will have expected type `rootTpe`. + * Delegate `query` to the interpreter `interpreter`. When evaluated by that interpreter the + * query will have expected type `rootTpe`. */ def component[F[_]](mapping: Mapping[F], query: Query, cursor: Cursor): ProtoJson = wrap(EffectJson(mapping, None, query, cursor)) - def effect[F[_]](mapping: Mapping[F], handler: EffectHandler[F], query: Query, cursor: Cursor): ProtoJson = + def effect[F[_]]( + mapping: Mapping[F], + handler: EffectHandler[F], + query: Query, + cursor: Cursor): ProtoJson = wrap(EffectJson(mapping, Some(handler), query, cursor)) def fromJson(value: Json): ProtoJson = wrap(value) @@ -504,13 +553,12 @@ object QueryInterpreter { /** * Combine possibly partial fields to create a possibly partial object. * - * If all fields are complete then they will be combined as a complete - * Json object. + * If all fields are complete then they will be combined as a complete Json object. * * Assumes that all fields are disjoint. */ def fromDisjointFields(fields: Seq[(String, ProtoJson)]): ProtoJson = - if(fields.forall(_._2.isInstanceOf[Json])) + if (fields.forall(_._2.isInstanceOf[Json])) wrap(Json.fromFields(fields.asInstanceOf[Seq[(String, Json)]])) else wrap(ProtoObject(fields)) @@ -518,8 +566,7 @@ object QueryInterpreter { /** * Combine possibly partial fields to create a possibly partial object. * - * If all fields are complete then they will be combined as a complete - * Json object. + * If all fields are complete then they will be combined as a complete Json object. */ def fromFields(fields: Seq[(String, ProtoJson)]): ProtoJson = fromDisjointFields(mergeFields(fields)) @@ -527,11 +574,10 @@ object QueryInterpreter { /** * Combine possibly partial values to create a possibly partial array. * - * If all values are complete then they will be combined as a complete - * Json array. + * If all values are complete then they will be combined as a complete Json array. */ def fromValues(elems: Seq[ProtoJson]): ProtoJson = - if(elems.forall(_.isInstanceOf[Json])) + if (elems.forall(_.isInstanceOf[Json])) wrap(Json.fromValues(elems.asInstanceOf[Seq[Json]])) else wrap(ProtoArray(elems)) @@ -539,8 +585,7 @@ object QueryInterpreter { /** * Select a value from a possibly partial object. * - * If the object is complete the selection will be a complete - * Json value. + * If the object is complete the selection will be a complete Json value. */ def select(elem: ProtoJson, fieldName: String): ProtoJson = elem match { @@ -553,13 +598,14 @@ object QueryInterpreter { /** * Test whether the argument contains any deferred subtrees * - * Yields `true` if the argument contains any component or staged - * subtrees, false otherwise. + * Yields `true` if the argument contains any component or staged subtrees, false otherwise. */ def isDeferred(p: ProtoJson): Boolean = p.isInstanceOf[DeferredJson] - /** Recursively merge a list of ProtoJson values. */ + /** + * Recursively merge a list of ProtoJson values. + */ def mergeProtoJson(elems: Seq[ProtoJson]): ProtoJson = { elems match { case Seq(elem) => elem @@ -572,21 +618,26 @@ object QueryInterpreter { } } - /** Recursively merge a list of ProtoJson objects. */ + /** + * Recursively merge a list of ProtoJson objects. + */ def mergeProtoObjects(objs: Seq[ProtoJson]): ProtoJson = objs match { case Seq(obj) => obj case Seq(_, _, _*) => val fieldss = objs flatMap { case ProtoObject(fields) => fields - case j: Json if j.isObject => j.asObject.get.toIterable.map { case (k, v) => (k, wrap(v)) } + case j: Json if j.isObject => + j.asObject.get.toIterable.map { case (k, v) => (k, wrap(v)) } case _ => Nil } fromFields(fieldss) case _ => wrap(Json.Null) } - /** Recursively merge a list of ProtoJson arrays. */ + /** + * Recursively merge a list of ProtoJson arrays. + */ def mergeProtoArrays(arrs: Seq[ProtoJson]): ProtoJson = arrs match { case Seq(arr) => arr @@ -603,7 +654,9 @@ object QueryInterpreter { case _ => wrap(Json.Null) } - /** Recursively merge a list of ProtoJson fields. */ + /** + * Recursively merge a list of ProtoJson fields. + */ def mergeFields(fields: Seq[(String, ProtoJson)]): Seq[(String, ProtoJson)] = { def hasDuplicates[T](xs: Seq[(String, T)]): Boolean = xs match { @@ -616,11 +669,14 @@ object QueryInterpreter { if (!hasDuplicates(fields)) fields else { val groupedFields = fields.groupMap(_._1)(_._2).view.mapValues(mergeProtoJson).toMap - fields.foldLeft((Set.empty[String], List.empty[(String, ProtoJson)])) { - case ((seen, acc), (fieldName, _)) => - if (seen.contains(fieldName)) (seen, acc) - else (seen + fieldName, (fieldName, groupedFields(fieldName)) :: acc) - }._2.reverse + fields + .foldLeft((Set.empty[String], List.empty[(String, ProtoJson)])) { + case ((seen, acc), (fieldName, _)) => + if (seen.contains(fieldName)) (seen, acc) + else (seen + fieldName, (fieldName, groupedFields(fieldName)) :: acc) + } + ._2 + .reverse } } @@ -645,30 +701,29 @@ object QueryInterpreter { /** * Complete a possibly partial result. * - * Completes a single possibly partial result as described for - * `completeAll`. + * Completes a single possibly partial result as described for `completeAll`. */ def complete[F[_]: Monad](pj: ProtoJson): F[Result[Json]] = pj match { case j: Json => Result(j).pure[F] - case _ => completeAll[F](List(pj)).map(_.map(_.head)) // result is 1:1 with the argument, so head is safe + case _ => + completeAll[F](List(pj)) + .map(_.map(_.head)) // result is 1:1 with the argument, so head is safe } - /** Complete a collection of possibly deferred results. + /** + * Complete a collection of possibly deferred results. * - * Each result is completed by locating any subtrees which have been - * deferred or delegated to some other component interpreter in an - * overall composite interpreter. Deferred subtrees are gathered, - * grouped by their associated interpreter and then evaluated in - * batches. The results of these batch evaluations are then - * completed in a subsequent stage recursively until the results are - * fully evaluated or yield errors. + * Each result is completed by locating any subtrees which have been deferred or delegated to + * some other component interpreter in an overall composite interpreter. Deferred subtrees are + * gathered, grouped by their associated interpreter and then evaluated in batches. The + * results of these batch evaluations are then completed in a subsequent stage recursively + * until the results are fully evaluated or yield errors. * - * Complete results are substituted back into the corresponding - * enclosing Json. + * Complete results are substituted back into the corresponding enclosing Json. * - * Errors are aggregated across all the results and are accumulated - * on the `Left` of the result. + * Errors are aggregated across all the results and are accumulated on the `Left` of the + * result. */ def completeAll[F[_]: Monad](pjs: List[ProtoJson]): F[Result[List[Json]]] = { def gatherDeferred(pj: ProtoJson): List[DeferredJson] = { @@ -676,13 +731,14 @@ object QueryInterpreter { def loop(pending: Chain[ProtoJson], acc: List[DeferredJson]): List[DeferredJson] = pending.uncons match { case None => acc - case Some((hd, tl)) => (hd: @unchecked) match { - case _: Json => loop(tl, acc) - case d: DeferredJson => loop(tl, d :: acc) - case ProtoObject(fields) => loop(Chain.fromSeq(fields.map(_._2)) ++ tl, acc) - case ProtoArray(elems) => loop(Chain.fromSeq(elems) ++ tl, acc) - case ProtoSelect(elem, _) => loop(elem +: tl, acc) - } + case Some((hd, tl)) => + (hd: @unchecked) match { + case _: Json => loop(tl, acc) + case d: DeferredJson => loop(tl, d :: acc) + case ProtoObject(fields) => loop(Chain.fromSeq(fields.map(_._2)) ++ tl, acc) + case ProtoArray(elems) => loop(Chain.fromSeq(elems) ++ tl, acc) + case ProtoSelect(elem, _) => loop(elem +: tl, acc) + } } pj match { @@ -694,7 +750,7 @@ object QueryInterpreter { def scatterResults(pj: ProtoJson, subst: mutable.Map[DeferredJson, Json]): Json = { def loop(pj: ProtoJson): Json = (pj: @unchecked) match { - case p: Json => p + case p: Json => p case d: DeferredJson => subst(d) case ProtoObject(fields) => val fields0 = fields.map { case (label, pvalue) => (label, loop(pvalue)) } @@ -710,7 +766,11 @@ object QueryInterpreter { } val batchedEffects = - pjs.flatMap(gatherDeferred).asInstanceOf[List[EffectJson[F]]].groupMap(ej => (ej.mapping, ej.handler))(identity).toList + pjs + .flatMap(gatherDeferred) + .asInstanceOf[List[EffectJson[F]]] + .groupMap(ej => (ej.mapping, ej.handler))(identity) + .toList (for { batchedResults <- @@ -724,12 +784,23 @@ object QueryInterpreter { ResultT(mapping.combineAndRun(queries)) case Some(handler) => for { - cs <- ResultT(handler.runEffects(queries)) - conts <- ResultT(queries.traverse { case (q, _) => Query.extractChild(q).toResultOrError("Continuation query has the wrong shape") }.pure[F]) - res <- ResultT(combineResults((conts, cs).parMapN { case (query, cursor) => mapping.interpreter.runValue(query, cursor.tpe, cursor) }).pure[F]) + cs <- ResultT(handler.runEffects(queries)) + conts <- ResultT( + queries + .traverse { + case (q, _) => + Query + .extractChild(q) + .toResultOrError("Continuation query has the wrong shape") + } + .pure[F]) + res <- ResultT(combineResults((conts, cs).parMapN { + case (query, cursor) => + mapping.interpreter.runValue(query, cursor.tpe, cursor) + }).pure[F]) } yield res } - next <- ResultT(completeAll[F](pnext)) + next <- ResultT(completeAll[F](pnext)) } yield batch.zip(next) } } yield { diff --git a/modules/core/src/main/scala/result.scala b/modules/core/src/main/scala/result.scala index 27086be8..86808e76 100644 --- a/modules/core/src/main/scala/result.scala +++ b/modules/core/src/main/scala/result.scala @@ -18,19 +18,33 @@ package grackle import scala.annotation.tailrec import scala.util.control.NonFatal -import cats.{~>, Applicative, Eq, Eval, Functor, Monad, MonadError, Parallel, Semigroup, Traverse} +import cats.{ + ~>, + Applicative, + Eq, + Eval, + Functor, + Monad, + MonadError, + Parallel, + Semigroup, + Traverse +} import cats.arrow.FunctionK import cats.data.{Chain, NonEmptyChain} import cats.implicits._ /** - * A result value. - * - * A result of type `T`, a non-empty collection of errors encoded as - * Json, or both. - */ + * A result value. + * + * A result of type `T`, a non-empty collection of errors encoded as Json, or both. + */ sealed trait Result[+T] { - def fold[U](failure: NonEmptyChain[Problem] => U, success: T => U, warning: (NonEmptyChain[Problem], T) => U, error: Throwable => U): U = + def fold[U]( + failure: NonEmptyChain[Problem] => U, + success: T => U, + warning: (NonEmptyChain[Problem], T) => U, + error: Throwable => U): U = this match { case Result.Success(value) => success(value) case Result.Warning(problems, value) => warning(problems, value) @@ -42,8 +56,8 @@ sealed trait Result[+T] { this match { case Result.Success(value) => Result.Success(f(value)) case Result.Warning(problems, value) => Result.Warning(problems, f(value)) - case other@Result.Failure(_) => other - case other@Result.InternalError(_) => other + case other @ Result.Failure(_) => other + case other @ Result.InternalError(_) => other } def flatMap[U](f: T => Result[U]): Result[U] = @@ -54,18 +68,18 @@ sealed trait Result[+T] { case Result.Success(fv) => Result.Warning(problems, fv) case Result.Warning(fps, fv) => Result.Warning(problems ++ fps, fv) case Result.Failure(fps) => Result.Failure(problems ++ fps) - case other@Result.InternalError(_) => other + case other @ Result.InternalError(_) => other } - case other@Result.Failure(_) => other - case other@Result.InternalError(_) => other + case other @ Result.Failure(_) => other + case other @ Result.InternalError(_) => other } def traverse[F[_], U](f: T => F[U])(implicit F: Applicative[F]): F[Result[U]] = this match { case Result.Success(value) => F.map(f(value))(Result.Success(_)) case Result.Warning(problems, value) => F.map(f(value))(Result.Warning(problems, _)) - case other@Result.Failure(_) => F.pure(other) - case other@Result.InternalError(_) => F.pure(other) + case other @ Result.Failure(_) => F.pure(other) + case other @ Result.InternalError(_) => F.pure(other) } final def exists(p: T => Boolean): Boolean = toOption.exists(p) @@ -98,7 +112,7 @@ sealed trait Result[+T] { case Result.Success(value) => Result.Warning(problems, value) case Result.Warning(ps, value) => Result.Warning(ps ++ problems, value) case Result.Failure(ps) => Result.Failure(ps ++ problems) - case other@Result.InternalError(_) => other + case other @ Result.InternalError(_) => other } def combine[U >: T](that: Result[U])(implicit S: Semigroup[U]): Result[U] = @@ -108,14 +122,14 @@ sealed trait Result[+T] { case Result.Success(u) => Result.Success(S.combine(t, u)) case Result.Warning(ps2, u) => Result.Warning(ps2, S.combine(t, u)) case Result.Failure(ps2) => Result.Warning(ps2, t) - case err@Result.InternalError(_) => err + case err @ Result.InternalError(_) => err } case Result.Warning(ps1, t) => that match { case Result.Success(u) => Result.Warning(ps1, S.combine(t, u)) case Result.Warning(ps2, u) => Result.Warning(ps1 ++ ps2, S.combine(t, u)) case Result.Failure(ps2) => Result.Warning(ps1 ++ ps2, t) - case err@Result.InternalError(_) => err + case err @ Result.InternalError(_) => err } case Result.Failure(ps1) => that match { @@ -124,16 +138,16 @@ sealed trait Result[+T] { case Result.Failure(ps2) => Result.Failure(ps1 ++ ps2) case Result.InternalError(thr2) => Result.InternalError(thr2) } - case err@Result.InternalError(_) => err + case err @ Result.InternalError(_) => err } def ===[TT >: T](that: Result[TT])(implicit TT: Eq[TT]): Boolean = (this, that) match { - case (Result.Failure(p), Result.Failure(pp)) => p == pp - case (Result.Success(t), Result.Success(tt)) => TT.eqv(t, tt) - case (Result.Warning(p, t), Result.Warning(pp, tt)) => (p == pp) && TT.eqv(t, tt) + case (Result.Failure(p), Result.Failure(pp)) => p == pp + case (Result.Success(t), Result.Success(tt)) => TT.eqv(t, tt) + case (Result.Warning(p, t), Result.Warning(pp, tt)) => (p == pp) && TT.eqv(t, tt) case (Result.InternalError(e), Result.InternalError(ee)) => e == ee - case _ => false + case _ => false } def getOrElse[U >: T](ifNone: => U): U = toOption.getOrElse(ifNone) @@ -178,16 +192,24 @@ object Result extends ResultInstances { final case class Failure(problems: NonEmptyChain[Problem]) extends Result[Nothing] final case class InternalError(error: Throwable) extends Result[Nothing] - /** Yields a Success with the given value. */ + /** + * Yields a Success with the given value. + */ def apply[A](a: A): Result[A] = Success(a) - /** Yields a Success with the given value. */ + /** + * Yields a Success with the given value. + */ def pure[A](a: A): Result[A] = Success(a) - /** Yields a Success with unit value. */ + /** + * Yields a Success with unit value. + */ val unit: Result[Unit] = pure(()) - /** Yields a Success with the given value. */ + /** + * Yields a Success with the given value. + */ def success[A](a: A): Result[A] = Success(a) def warning[A](warning: Problem, value: A): Result[A] = @@ -211,7 +233,7 @@ object Result extends ResultInstances { def fromOption[A](oa: Option[A], ifNone: => Problem): Result[A] = oa match { case Some(a) => Result(a) - case None => Result.failure(ifNone) + case None => Result.failure(ifNone) } def fromOption[A](oa: Option[A], ifNone: => String)(implicit ev: DummyImplicit): Result[A] = @@ -241,8 +263,8 @@ object Result extends ResultInstances { // contains any internal errors the result will be the first of these. def combineAllWithDefault[T](ress: List[Result[T]], default: => T): Result[List[T]] = { lazy val default0 = default - (ress.foldLeft(Right((Chain.empty, Nil)): Either[Throwable, (Chain[Problem], List[T])]) { - case (err@Left(_), _) => err + ress.foldLeft(Right((Chain.empty, Nil)): Either[Throwable, (Chain[Problem], List[T])]) { + case (err @ Left(_), _) => err case (_, Result.InternalError(err)) => Left(err) case (Right((ps, ts)), elem) => elem match { @@ -251,11 +273,14 @@ object Result extends ResultInstances { case Result.Warning(ps0, t) => Right((ps ++ ps0.toChain, t :: ts)) case Result.InternalError(err) => Left(err) } - }) match { + } match { case Left(err) => Result.internalError(err) case Right((ps, ts)) => val ts0 = ts.reverse - NonEmptyChain.fromChain(ps).map(ps0 => Result.Warning(ps0, ts0)).getOrElse(Result.Success(ts0)) + NonEmptyChain + .fromChain(ps) + .map(ps0 => Result.Warning(ps0, ts0)) + .getOrElse(Result.Success(ts0)) } } } @@ -263,43 +288,46 @@ object Result extends ResultInstances { trait ResultInstances extends ResultInstances0 { implicit def grackleSemigroupForResult[A: Semigroup]: Semigroup[Result[A]] = _ combine _ - implicit val grackleMonadErrorForResult: MonadError[Result, Either[Throwable, NonEmptyChain[Problem]]] = + implicit val grackleMonadErrorForResult + : MonadError[Result, Either[Throwable, NonEmptyChain[Problem]]] = new MonadError[Result, Either[Throwable, NonEmptyChain[Problem]]] { def raiseError[A](e: Either[Throwable, NonEmptyChain[Problem]]): Result[A] = e.fold(Result.InternalError(_), Result.Failure(_)) - def handleErrorWith[A](fa: Result[A])(f: Either[Throwable, NonEmptyChain[Problem]] => Result[A]): Result[A] = + def handleErrorWith[A](fa: Result[A])( + f: Either[Throwable, NonEmptyChain[Problem]] => Result[A]): Result[A] = fa match { - case Result.Failure(ps) => f(Right(ps)) + case Result.Failure(ps) => f(Right(ps)) case Result.InternalError(e) => f(Left(e)) - case _ => fa + case _ => fa } def flatMap[A, B](fa: Result[A])(f: A => Result[B]): Result[B] = fa.flatMap(f) - override def map2Eval[A, B, C](fa: Result[A], fb: Eval[Result[B]])(f: (A, B) => C): Eval[Result[C]] = + override def map2Eval[A, B, C](fa: Result[A], fb: Eval[Result[B]])( + f: (A, B) => C): Eval[Result[C]] = fa match { - case err@Result.InternalError(_) => Eval.now(err) // no need to evaluate fb - case fail@Result.Failure(_) => Eval.now(fail) // no need to evaluate fb - case notLeft => fb.map(fb => map2(notLeft, fb)(f)) + case err @ Result.InternalError(_) => Eval.now(err) // no need to evaluate fb + case fail @ Result.Failure(_) => Eval.now(fail) // no need to evaluate fb + case notLeft => fb.map(fb => map2(notLeft, fb)(f)) } def tailRecM[A, B](a: A)(fn: A => Result[Either[A, B]]): Result[B] = { @tailrec def loop(v: Result[Either[A, B]]): Result[B] = v match { - case err@Result.InternalError(_) => err - case fail@Result.Failure(_) => fail - case Result.Success(Right(b)) => Result.success(b) + case err @ Result.InternalError(_) => err + case fail @ Result.Failure(_) => fail + case Result.Success(Right(b)) => Result.success(b) case Result.Warning(ps, Right(b)) => Result.Warning(ps, b) - case Result.Success(Left(a)) => loop(fn(a)) + case Result.Success(Left(a)) => loop(fn(a)) case Result.Warning(ps, Left(a)) => fn(a) match { - case err@Result.InternalError(_) => err - case Result.Success(a) => loop(Result.Warning(ps, a)) - case Result.Failure(ps0) => Result.Failure(ps ++ ps0) - case Result.Warning(ps0, a) => loop(Result.Warning(ps ++ ps0, a)) + case err @ Result.InternalError(_) => err + case Result.Success(a) => loop(Result.Warning(ps, a)) + case Result.Failure(ps0) => Result.Failure(ps ++ ps0) + case Result.Warning(ps0, a) => loop(Result.Warning(ps ++ ps0, a)) } } loop(fn(a)) @@ -323,27 +351,27 @@ trait ResultInstances extends ResultInstances0 { def pure[A](a: A): Result[A] = Result.success(a) def ap[A, B](ff: Result[A => B])(fa: Result[A]): Result[B] = fa match { - case err@Result.InternalError(_) => err - case fail@Result.Failure(ps) => + case err @ Result.InternalError(_) => err + case fail @ Result.Failure(ps) => ff match { - case err@Result.InternalError(_) => err - case Result.Success(_) => fail - case Result.Failure(ps0) => Result.Failure(ps0 ++ ps) - case Result.Warning(ps0, _) => Result.Failure(ps0 ++ ps) + case err @ Result.InternalError(_) => err + case Result.Success(_) => fail + case Result.Failure(ps0) => Result.Failure(ps0 ++ ps) + case Result.Warning(ps0, _) => Result.Failure(ps0 ++ ps) } case Result.Success(a) => ff match { - case err@Result.InternalError(_) => err - case fail@Result.Failure(_) => fail - case Result.Success(f) => Result.success(f(a)) - case Result.Warning(ps, f) => Result.Warning(ps, f(a)) + case err @ Result.InternalError(_) => err + case fail @ Result.Failure(_) => fail + case Result.Success(f) => Result.success(f(a)) + case Result.Warning(ps, f) => Result.Warning(ps, f(a)) } case Result.Warning(ps, a) => ff match { - case err@Result.InternalError(_) => err - case fail@Result.Failure(_) => fail - case Result.Success(f) => Result.Warning(ps, f(a)) - case Result.Warning(ps0, f) => Result.Warning(ps0 ++ ps, f(a)) + case err @ Result.InternalError(_) => err + case fail @ Result.Failure(_) => fail + case Result.Success(f) => Result.Warning(ps, f(a)) + case Result.Warning(ps0, f) => Result.Warning(ps0 ++ ps, f(a)) } } } @@ -358,7 +386,8 @@ trait ResultInstances0 { def traverse[F[_]: Applicative, A, B](fa: Result[A])(f: A => F[B]): F[Result[B]] = fa.traverse(f) - override def mapAccumulate[S, B, C](init: S, fa: Result[B])(f: (S, B) => (S, C)): (S, Result[C]) = + override def mapAccumulate[S, B, C](init: S, fa: Result[B])( + f: (S, B) => (S, C)): (S, Result[C]) = fa match { case err @ Result.InternalError(_) => (init, err) case fail @ Result.Failure(_) => (init, fail) @@ -396,28 +425,27 @@ final case class ResultT[F[_], A](value: F[Result[A]]) { def flatMap[B](f: A => ResultT[F, B])(implicit F: Monad[F]): ResultT[F, B] = ResultT(F.flatMap(value) { - case err@Result.InternalError(_) => F.pure(err) - case fail@Result.Failure(_) => F.pure(fail) - case Result.Success(a) => f(a).value + case err @ Result.InternalError(_) => F.pure(err) + case fail @ Result.Failure(_) => F.pure(fail) + case Result.Success(a) => f(a).value case Result.Warning(ps, a) => F.map(f(a).value) { - case err@Result.InternalError(_) => err - case Result.Success(b) => Result.Warning(ps, b) - case Result.Failure(ps0) => Result.Failure(ps ++ ps0) - case Result.Warning(ps0, b) => Result.Warning(ps ++ ps0, b) + case err @ Result.InternalError(_) => err + case Result.Success(b) => Result.Warning(ps, b) + case Result.Failure(ps0) => Result.Failure(ps ++ ps0) + case Result.Warning(ps0, b) => Result.Warning(ps ++ ps0, b) } }) } object ResultT { - implicit def grackleApplicativeForResultT[F[_]](implicit F: Applicative[F]): Applicative[ResultT[F, *]] = + implicit def grackleApplicativeForResultT[F[_]]( + implicit F: Applicative[F]): Applicative[ResultT[F, *]] = new Applicative[ResultT[F, *]] { override def pure[A](a: A): ResultT[F, A] = ResultT(F.pure(Result.success(a))) def ap[A, B](fab: ResultT[F, A => B])(fa: ResultT[F, A]): ResultT[F, B] = ResultT( - F.map2(fab.value, fa.value) { - case (fab, a) => fab.ap(a) - } + F.map2(fab.value, fa.value) { case (fab, a) => fab.ap(a) } ) } @@ -425,7 +453,7 @@ object ResultT { ResultT(fa.map(Result.success)) def unit[F[_]](implicit F: Applicative[F]): ResultT[F, Unit] = - pure(()) + pure(()) def pure[F[_], A](a: A)(implicit F: Applicative[F]): ResultT[F, A] = fromResult(Result.pure(a)) @@ -433,7 +461,7 @@ object ResultT { def fromResult[F[_], A](a: Result[A])(implicit F: Applicative[F]): ResultT[F, A] = ResultT(a.pure[F]) - def success[F[_]: Applicative, A](a: A): ResultT[F, A] = + def success[F[_]: Applicative, A](a: A): ResultT[F, A] = ResultT(Result.success(a).pure[F]) def warning[F[_]: Applicative, A](warning: NonEmptyChain[Problem], value: A): ResultT[F, A] = diff --git a/modules/core/src/main/scala/schema.scala b/modules/core/src/main/scala/schema.scala index c4ce0bdf..4e5c0fb5 100644 --- a/modules/core/src/main/scala/schema.scala +++ b/modules/core/src/main/scala/schema.scala @@ -15,18 +15,24 @@ package grackle -import scala.collection.mutable.{ Map => MMap } +import scala.collection.mutable.{Map => MMap} import cats.implicits._ import io.circe.Json import org.tpolecat.sourcepos.SourcePos -import syntax._ -import Ast.{DirectiveLocation, InterfaceTypeDefinition, ObjectTypeDefinition, TypeDefinition, UnionTypeDefinition} -import Query._ -import ScalarType._ -import UntypedOperation._ -import Value._ +import grackle.Ast.{ + DirectiveLocation, + InterfaceTypeDefinition, + ObjectTypeDefinition, + TypeDefinition, + UnionTypeDefinition +} +import grackle.Query._ +import grackle.ScalarType._ +import grackle.UntypedOperation._ +import grackle.Value._ +import grackle.syntax._ /** * Representation of a GraphQL schema @@ -37,32 +43,42 @@ trait Schema { def pos: SourcePos - /** The types defined by this `Schema` prior to any extensions. */ + /** + * The types defined by this `Schema` prior to any extensions. + */ def baseTypes: List[NamedType] - /** The types defined by this `Schema` with any extensions applied. */ + /** + * The types defined by this `Schema` with any extensions applied. + */ lazy val types: List[NamedType] = if (typeExtensions.isEmpty) baseTypes else baseTypes.map(extendType(typeExtensions)) - /** The directives defined by this `Schema`. */ + /** + * The directives defined by this `Schema`. + */ def directives: List[DirectiveDef] - /** The schema extensions defined by this `Schema` */ + /** + * The schema extensions defined by this `Schema` + */ def schemaExtensions: List[SchemaExtension] - /** The type extensions defined by this `Schema` */ + /** + * The type extensions defined by this `Schema` + */ def typeExtensions: List[TypeExtension] /** * A reference by name to a type defined by this `Schema`. * - * This method should be used to obtain the references to types - * required for the definitions of mappings and the bodies of select - * elaborators because TypeRefs can be meaningfully compared for - * equality using `==`. + * This method should be used to obtain the references to types required for the definitions + * of mappings and the bodies of select elaborators because TypeRefs can be meaningfully + * compared for equality using `==`. * - * @throws java.lang.IllegalArgumentException if the type is not defined in this schema + * @throws java.lang.IllegalArgumentException + * if the type is not defined in this schema */ def ref(tpnme: String): TypeRef = { if (!types.exists(_.name == tpnme)) @@ -72,19 +88,18 @@ trait Schema { } /** - * Alias for `uncheckedRef` for use within constructors of concrete - * `Schema` values. + * Alias for `uncheckedRef` for use within constructors of concrete `Schema` values. * - * `TypeRef`s refer to types defined in this schema by name and hence - * can be used as part of mutually recursive type definitions. + * `TypeRef`s refer to types defined in this schema by name and hence can be used as part of + * mutually recursive type definitions. */ protected def TypeRef(tpnme: String): TypeRef = uncheckedRef(tpnme) /** * The default type of a GraphQL schema * - * Unless a type named `"Schema"` is explicitly defined as part of - * this `Schema` a definition of the form, + * Unless a type named `"Schema"` is explicitly defined as part of this `Schema` a definition + * of the form, * * ``` * type Schema { @@ -103,12 +118,11 @@ trait Schema { ObjectType( name = "Schema", description = None, - fields = - List( - definition("Query").map(mkRootDef("query")), - definition("Mutation").map(mkRootDef("mutation")), - definition("Subscription").map(mkRootDef("subscription")) - ).flatten, + fields = List( + definition("Query").map(mkRootDef("query")), + definition("Mutation").map(mkRootDef("mutation")), + definition("Subscription").map(mkRootDef("subscription")) + ).flatten, interfaces = Nil, directives = Nil ) @@ -127,11 +141,11 @@ trait Schema { /** * A reference by name to a type defined by this `Schema`. * - * `TypeRef`s refer to types defined in this schema by name and hence - * can be used as part of mutually recursive type definitions. + * `TypeRef`s refer to types defined in this schema by name and hence can be used as part of + * mutually recursive type definitions. * - * Note that this method should be used with caution as it does not - * check that the type is defined in this schema. + * Note that this method should be used with caution as it does not check that the type is + * defined in this schema. */ def uncheckedRef(tpnme: String): TypeRef = new TypeRef(this, tpnme) @@ -139,12 +153,11 @@ trait Schema { /** * A reference to a type defined by this `Schema`. * - * The primary use of this method is to obtain a TypeRef - * corresponding to a type defined in this schema for use in - * builtin equality comparisons with other TypeRefs. + * The primary use of this method is to obtain a TypeRef corresponding to a type defined in + * this schema for use in builtin equality comparisons with other TypeRefs. * - * Note that this method should be used with caution as it does not - * check that the type is defined in this schema. + * Note that this method should be used with caution as it does not check that the type is + * defined in this schema. */ def uncheckedRef(tpe: NamedType): TypeRef = uncheckedRef(tpe.name) @@ -154,27 +167,39 @@ trait Schema { /** * The schema type. * - * Either the explicitly defined type named `"Schema"` or the default - * schema type if not defined. + * Either the explicitly defined type named `"Schema"` or the default schema type if not + * defined. */ lazy val schemaType: NamedType = if (schemaExtensions.isEmpty) baseSchemaType else extendSchemaType(schemaExtensions, baseSchemaType) - /** The type of queries defined by this `Schema`*/ + /** + * The type of queries defined by this `Schema` + */ def queryType: NamedType = schemaType.field("query").flatMap(_.nonNull.asNamed).get - /** The type of mutations defined by this `Schema`*/ + /** + * The type of mutations defined by this `Schema` + */ def mutationType: Option[NamedType] = schemaType.field("mutation").flatMap(_.nonNull.asNamed) - /** The type of subscriptions defined by this `Schema`*/ - def subscriptionType: Option[NamedType] = schemaType.field("subscription").flatMap(_.nonNull.asNamed) + /** + * The type of subscriptions defined by this `Schema` + */ + def subscriptionType: Option[NamedType] = + schemaType.field("subscription").flatMap(_.nonNull.asNamed) - /** True if the supplied type is one of the Query, Mutation or Subscription root types, false otherwise */ + /** + * True if the supplied type is one of the Query, Mutation or Subscription root types, false + * otherwise + */ def isRootType(tpe: Type): Boolean = tpe =:= queryType || mutationType.exists(_ =:= tpe) || subscriptionType.exists(_ =:= tpe) - /** Are the supplied alternatives exhaustive for `tp` */ + /** + * Are the supplied alternatives exhaustive for `tp` + */ def exhaustive(tp: Type, branches: List[Type]): Boolean = { types.forall { case o: ObjectType => !(o <:< tp) || branches.exists(b => o <:< b) @@ -193,7 +218,9 @@ trait Schema { MMap.from(grouped) } - /** Yields the `ObjectType` implementations of the given `InterfaceType`. */ + /** + * Yields the `ObjectType` implementations of the given `InterfaceType`. + */ def implementations(it: InterfaceType): List[ObjectType] = implIndex.getOrElse(it.name, Nil) @@ -202,7 +229,7 @@ trait Schema { private def extendType(extns: List[TypeExtension])(baseType: NamedType): NamedType = { baseType match { case ScalarType(name, description, directives) => - val exts = extns.collect { case se@ScalarExtension(`name`, _) => se } + val exts = extns.collect { case se @ ScalarExtension(`name`, _) => se } if (exts.isEmpty) baseType else { val newDirectives = exts.flatMap(_.directives) @@ -210,27 +237,37 @@ trait Schema { } case InterfaceType(name, description, fields, interfaces, directives) => - val exts = extns.collect { case ie@InterfaceExtension(`name`, _, _, _) => ie } + val exts = extns.collect { case ie @ InterfaceExtension(`name`, _, _, _) => ie } if (exts.isEmpty) baseType else { val newFields = exts.flatMap(_.fields) val newInterfaces = exts.flatMap(_.interfaces) val newDirectives = exts.flatMap(_.directives) - InterfaceType(name, description, fields ++ newFields, interfaces ++ newInterfaces, directives ++ newDirectives) + InterfaceType( + name, + description, + fields ++ newFields, + interfaces ++ newInterfaces, + directives ++ newDirectives) } case ObjectType(name, description, fields, interfaces, directives) => - val exts = extns.collect { case oe@ObjectExtension(`name`, _, _, _) => oe } + val exts = extns.collect { case oe @ ObjectExtension(`name`, _, _, _) => oe } if (exts.isEmpty) baseType else { val newFields = exts.flatMap(_.fields) val newInterfaces = exts.flatMap(_.interfaces) val newDirectives = exts.flatMap(_.directives) - ObjectType(name, description, fields ++ newFields, interfaces ++ newInterfaces, directives ++ newDirectives) + ObjectType( + name, + description, + fields ++ newFields, + interfaces ++ newInterfaces, + directives ++ newDirectives) } case UnionType(name, description, members, directives) => - val exts = extns.collect { case ue@UnionExtension(`name`, _, _) => ue } + val exts = extns.collect { case ue @ UnionExtension(`name`, _, _) => ue } if (exts.isEmpty) baseType else { val newMembers = exts.flatMap(_.members) @@ -239,7 +276,7 @@ trait Schema { } case EnumType(name, description, enumValues, directives) => - val exts = extns.collect { case ee@EnumExtension(`name`, _, _) => ee } + val exts = extns.collect { case ee @ EnumExtension(`name`, _, _) => ee } if (exts.isEmpty) baseType else { val newValues = exts.flatMap(_.enumValues) @@ -248,12 +285,16 @@ trait Schema { } case InputObjectType(name, description, inputFields, directives) => - val exts = extns.collect { case ioe@InputObjectExtension(`name`, _, _) => ioe } + val exts = extns.collect { case ioe @ InputObjectExtension(`name`, _, _) => ioe } if (exts.isEmpty) baseType else { val newFields = exts.flatMap(_.inputFields) val newDirectives = exts.flatMap(_.directives) - InputObjectType(name, description, inputFields ++ newFields, directives ++ newDirectives) + InputObjectType( + name, + description, + inputFields ++ newFields, + directives ++ newDirectives) } case tr: TypeRef => @@ -264,18 +305,27 @@ trait Schema { } } - private def extendSchemaType(extns: List[SchemaExtension], schemaType: NamedType): NamedType = { + private def extendSchemaType( + extns: List[SchemaExtension], + schemaType: NamedType): NamedType = { schemaType match { case ObjectType(name, description, fields, interfaces, directives) => val newFields = extns.flatMap(_.rootOperations) val newDirectives = extns.flatMap(_.directives) - ObjectType(name, description, fields ++ newFields, interfaces, directives ++ newDirectives) + ObjectType( + name, + description, + fields ++ newFields, + interfaces, + directives ++ newDirectives) case _ => schemaType } } - /** Returns all subtypes of `tpe` */ + /** + * Returns all subtypes of `tpe` + */ def subtypes(tpe: NamedType): Set[NamedType] = types.filter(_ <:< tpe).toSet } @@ -288,24 +338,26 @@ object Schema { } case class SchemaExtension( - rootOperations: List[Field], - directives: List[Directive] + rootOperations: List[Field], + directives: List[Directive] ) /** * A GraphQL type definition. */ sealed trait Type extends Product { + /** * Is this type equivalent to `other`. * - * Note that plain `==` will distinguish types from type aliases, - * which is typically not desirable, so `=:=` is usually the - * most appropriate comparison operator. + * Note that plain `==` will distinguish types from type aliases, which is typically not + * desirable, so `=:=` is usually the most appropriate comparison operator. */ def =:=(other: Type): Boolean = (this eq other) || (dealias == other.dealias) - /** `true` if this type is a subtype of `other`. */ + /** + * `true` if this type is a subtype of `other`. + */ def <:<(other: Type): Boolean = (this.dealias, other.dealias) match { case (tp1, tp2) if tp1 == tp2 => true @@ -326,8 +378,8 @@ sealed trait Type extends Product { }) /** - * Yield the type of the field of this type named `fieldName` or - * `None` if there is no such field. + * Yield the type of the field of this type named `fieldName` or `None` if there is no such + * field. */ def field(fieldName: String): Option[Type] = this match { case NullableType(tpe) => tpe.field(fieldName) @@ -337,11 +389,15 @@ sealed trait Type extends Product { case _ => None } - /** `true` if this type has a field named `fieldName`, false otherwise. */ + /** + * `true` if this type has a field named `fieldName`, false otherwise. + */ def hasField(fieldName: String): Boolean = field(fieldName).isDefined - /** Yields the definition of `fieldName` in this type if it exists, `None` otherwise. */ + /** + * Yields the definition of `fieldName` in this type if it exists, `None` otherwise. + */ def fieldInfo(fieldName: String): Option[Field] = this match { case NullableType(tpe) => tpe.fieldInfo(fieldName) case ListType(tpe) => tpe.fieldInfo(fieldName) @@ -350,8 +406,8 @@ sealed trait Type extends Product { } /** - * `true` if this type has a field named `fieldName` which is undefined in - * some interface it implements + * `true` if this type has a field named `fieldName` which is undefined in some interface it + * implements */ def variantField(fieldName: String): Boolean = underlyingObject match { @@ -361,8 +417,8 @@ sealed trait Type extends Product { } /** - * Yield the type of the field at the end of the path `fns` starting - * from this type, or `None` if there is no such field. + * Yield the type of the field at the end of the path `fns` starting from this type, or `None` + * if there is no such field. */ def path(fns: List[String]): Option[Type] = (fns, this) match { case (Nil, _) => Some(this) @@ -379,9 +435,8 @@ sealed trait Type extends Product { /** * Does the path `fns` from this type specify multiple values. * - * `true` if navigating through the path `fns` from this type - * might specify 0 or more values. This will be the case if the - * path passes through at least one field of a List type. + * `true` if navigating through the path `fns` from this type might specify 0 or more values. + * This will be the case if the path passes through at least one field of a List type. */ def pathIsList(fns: List[String]): Boolean = (fns, this) match { case (Nil, _) => this.isList @@ -398,9 +453,8 @@ sealed trait Type extends Product { /** * Does the path `fns` from this type specify a nullable type. * - * `true` if navigating through the path `fns` from this type - * might specify an optional value. This will be the case if the - * path passes through at least one field of a nullable type. + * `true` if navigating through the path `fns` from this type might specify an optional value. + * This will be the case if the path passes through at least one field of a nullable type. */ def pathIsNullable(fns: List[String]): Boolean = (fns, this) match { case (Nil, _) => false @@ -414,19 +468,27 @@ sealed trait Type extends Product { case _ => false } - /** Strip off aliases */ + /** + * Strip off aliases + */ def dealias: Type = this - /** true if a non-TypeRef or a TypeRef to a defined type */ + /** + * true if a non-TypeRef or a TypeRef to a defined type + */ def exists: Boolean = true - /** Is this type nullable? */ + /** + * Is this type nullable? + */ def isNullable: Boolean = this match { case NullableType(_) => true case _ => false } - /** This type if it is nullable, `Nullable(this)` otherwise. */ + /** + * This type if it is nullable, `Nullable(this)` otherwise. + */ def nullable: Type = this match { case t: NullableType => t case t => NullableType(t) @@ -435,15 +497,17 @@ sealed trait Type extends Product { /** * A non-nullable version of this type. * - * If this type is nullable, yield the non-nullable underlying - * type. Otherwise yield this type. + * If this type is nullable, yield the non-nullable underlying type. Otherwise yield this + * type. */ def nonNull: Type = this match { case NullableType(tpe) => tpe.nonNull case _ => this } - /** Is this type a list. */ + /** + * Is this type a list. + */ def isList: Boolean = this match { case ListType(_) => true case _ => false @@ -452,8 +516,7 @@ sealed trait Type extends Product { /** * The element type of this type. * - * If this type is is a list, yield the non-list underlying type. - * Otherwise yield `None`. + * If this type is is a list, yield the non-list underlying type. Otherwise yield `None`. */ def item: Option[Type] = this match { case NullableType(tpe) => tpe.item @@ -461,7 +524,9 @@ sealed trait Type extends Product { case _ => None } - /** This type if it is a (nullable) list, `ListType(this)` otherwise. */ + /** + * This type if it is a (nullable) list, `ListType(this)` otherwise. + */ def list: Type = this match { case l: ListType => l case NullableType(tpe) => NullableType(tpe.list) @@ -478,10 +543,9 @@ sealed trait Type extends Product { /** * Yield the object type underlying this type. * - * Strip off all aliases, nullability and enclosing list types until - * an underlying object type is reached, in which case yield it, or a - * non-object type which isn't further reducible is reached, in which - * case yield `None`. + * Strip off all aliases, nullability and enclosing list types until an underlying object type + * is reached, in which case yield it, or a non-object type which isn't further reducible is + * reached, in which case yield `None`. */ def underlyingObject: Option[NamedType] = this match { case NullableType(tpe) => tpe.underlyingObject @@ -494,13 +558,11 @@ sealed trait Type extends Product { } /** - * Yield the type of the field named `fieldName` of the object type - * underlying this type. + * Yield the type of the field named `fieldName` of the object type underlying this type. * - * Strip off all aliases, nullability and enclosing list types until - * an underlying object type is reached which has a field named - * `fieldName`, in which case yield the type of that field; if there - * is no such field, yields `None`. + * Strip off all aliases, nullability and enclosing list types until an underlying object type + * is reached which has a field named `fieldName`, in which case yield the type of that field; + * if there is no such field, yields `None`. */ def underlyingField(fieldName: String): Option[Type] = this match { case NullableType(tpe) => tpe.underlyingField(fieldName) @@ -512,22 +574,22 @@ sealed trait Type extends Product { } /** - * Yield the named type underlying this type. - * - * Strips of nullability and enclosing list types until an - * underlying named type is reached. This method will always - * yield a named type. - */ + * Yield the named type underlying this type. + * + * Strips of nullability and enclosing list types until an underlying named type is reached. + * This method will always yield a named type. + */ def underlyingNamed: NamedType = this match { case NullableType(tpe) => tpe.underlyingNamed - case ListType(tpe) => tpe.underlyingNamed + case ListType(tpe) => tpe.underlyingNamed case tpe: NamedType => tpe } - /** Is this type a leaf type? + /** + * Is this type a leaf type? * - * `true` if after stripping of aliases the underlying type a scalar or an - * enum, `false` otherwise. + * `true` if after stripping of aliases the underlying type a scalar or an enum, `false` + * otherwise. */ def isLeaf: Boolean = this match { case TypeRef(_, _) if exists => dealias.isLeaf @@ -536,8 +598,8 @@ sealed trait Type extends Product { } /** - * If the underlying type of this type is a scalar or an enum then yield it - * otherwise yield `None`. + * If the underlying type of this type is a scalar or an enum then yield it otherwise yield + * `None`. */ def asLeaf: Option[Type] = this match { case TypeRef(_, _) if exists => dealias.asLeaf @@ -545,14 +607,12 @@ sealed trait Type extends Product { case _ => None } - /** * Is the underlying of this type a leaf type? * - * Strip off all aliases, nullability and enclosing list types until - * an underlying leaf type is reached, in which case yield true, or an - * a object, interface or union type which is reached, in which case - * yield false. + * Strip off all aliases, nullability and enclosing list types until an underlying leaf type + * is reached, in which case yield true, or an a object, interface or union type which is + * reached, in which case yield false. */ def isUnderlyingLeaf: Boolean = this match { case NullableType(tpe) => tpe.isUnderlyingLeaf @@ -565,10 +625,9 @@ sealed trait Type extends Product { /** * Yield the leaf type underlying this type. * - * Strip off all aliases, nullability and enclosing list types until - * an underlying leaf type is reached, in which case yield it, or an - * a object, interface or union type which is reached, in which case - * yield `None`. + * Strip off all aliases, nullability and enclosing list types until an underlying leaf type + * is reached, in which case yield it, or an a object, interface or union type which is + * reached, in which case yield `None`. */ def underlyingLeaf: Option[Type] = this match { case NullableType(tpe) => tpe.underlyingLeaf @@ -609,12 +668,16 @@ sealed trait Type extends Product { // Move all below into object Type? -/** A type with a schema-defined name. +/** + * A type with a schema-defined name. * * This includes object types, inferface types and enums. */ sealed trait NamedType extends Type { - /** The name of this type */ + + /** + * The name of this type + */ def name: String override def dealias: NamedType = this @@ -653,27 +716,27 @@ case class TypeRef private[grackle] (schema: Schema, name: String) extends Named /** * Represents scalar types such as Int, String, and Boolean. Scalars cannot have fields. * - * @see https://spec.graphql.org/draft/#sec-Scalars + * @see + * https://spec.graphql.org/draft/#sec-Scalars */ case class ScalarType( - name: String, - description: Option[String], - directives: List[Directive] -) extends Type with NamedType { + name: String, + description: Option[String], + directives: List[Directive] +) extends Type + with NamedType { import ScalarType._ override def isScalar: Boolean = true - /** True if this is one of the five built-in Scalar types defined in the GraphQL Specification. */ + /** + * True if this is one of the five built-in Scalar types defined in the GraphQL Specification. + */ def isBuiltIn: Boolean = this match { - case IntType | - FloatType | - StringType | - BooleanType | - IDType => true - case _ => false - } + case IntType | FloatType | StringType | BooleanType | IDType => true + case _ => false + } def specifiedByURL: Option[String] = for { @@ -694,58 +757,53 @@ object ScalarType { val IntType = ScalarType( name = "Int", - description = - Some( - """|The Int scalar type represents a signed 32‐bit numeric non‐fractional value. - |Response formats that support a 32‐bit integer or a number type should use that - |type to represent this scalar. + description = Some( + """|The Int scalar type represents a signed 32‐bit numeric non‐fractional value. + |Response formats that support a 32‐bit integer or a number type should use that + |type to represent this scalar. """.stripMargin.trim - ), + ), directives = Nil ) val FloatType = ScalarType( name = "Float", - description = - Some( - """|The Float scalar type represents signed double‐precision fractional values as - |specified by IEEE 754. Response formats that support an appropriate - |double‐precision number type should use that type to represent this scalar. + description = Some( + """|The Float scalar type represents signed double‐precision fractional values as + |specified by IEEE 754. Response formats that support an appropriate + |double‐precision number type should use that type to represent this scalar. """.stripMargin.trim - ), + ), directives = Nil ) val StringType = ScalarType( name = "String", - description = - Some( - """|The String scalar type represents textual data, represented as UTF‐8 character - |sequences. The String type is most often used by GraphQL to represent free‐form - |human‐readable text. + description = Some( + """|The String scalar type represents textual data, represented as UTF‐8 character + |sequences. The String type is most often used by GraphQL to represent free‐form + |human‐readable text. """.stripMargin.trim - ), + ), directives = Nil ) val BooleanType = ScalarType( name = "Boolean", - description = - Some( - """|The Boolean scalar type represents true or false. Response formats should use a - |built‐in boolean type if supported; otherwise, they should use their - |representation of the integers 1 and 0. + description = Some( + """|The Boolean scalar type represents true or false. Response formats should use a + |built‐in boolean type if supported; otherwise, they should use their + |representation of the integers 1 and 0. """.stripMargin.trim - ), + ), directives = Nil ) val IDType = ScalarType( name = "ID", - description = - Some( - """|The ID scalar type represents a unique identifier, often used to refetch an - |object or as the key for a cache. The ID type is serialized in the same way as a - |String; however, it is not intended to be human‐readable. + description = Some( + """|The ID scalar type represents a unique identifier, often used to refetch an + |object or as the key for a cache. The ID type is serialized in the same way as a + |String; however, it is not intended to be human‐readable. """.stripMargin.trim - ), + ), directives = Nil ) @@ -770,6 +828,7 @@ sealed trait TypeWithFields extends NamedType { @deprecated("Use interfaces instead", "0.20.0") def allInterfaces: List[NamedType] = interfaces } + /** * Mixin for types that can be deprecated. */ @@ -781,33 +840,35 @@ protected trait Deprecatable { def isDeprecated: Boolean = deprecatedDirective.isDefined def deprecationReason: Option[String] = for { - dir <- deprecatedDirective + dir <- deprecatedDirective reason <- dir.args.collectFirst { case Binding("reason", StringValue(reason)) => reason } } yield reason } /** - * Scalar extensions allow additional directives to be applied to a pre-existing Scalar type - * - * @see https://spec.graphql.org/draft/#sec-Scalar-Extensions - */ + * Scalar extensions allow additional directives to be applied to a pre-existing Scalar type + * + * @see + * https://spec.graphql.org/draft/#sec-Scalar-Extensions + */ case class ScalarExtension( - baseType: String, - directives: List[Directive] + baseType: String, + directives: List[Directive] ) extends TypeExtension /** * Interfaces are an abstract type where there are common fields declared. Any type that * implements an interface must define all the fields with names and types exactly matching. * - * @see https://spec.graphql.org/draft/#sec-Interfaces + * @see + * https://spec.graphql.org/draft/#sec-Interfaces */ case class InterfaceType( - name: String, - description: Option[String], - fields: List[Field], - interfaces: List[NamedType], - directives: List[Directive] + name: String, + description: Option[String], + fields: List[Field], + interfaces: List[NamedType], + directives: List[Directive] ) extends TypeWithFields { override def isInterface: Boolean = true } @@ -815,26 +876,28 @@ case class InterfaceType( /** * Interface extensions allow additional fields to be added to a pre-existing interface type * - * @see https://spec.graphql.org/draft/#sec-Interface-Extensions - **/ + * @see + * https://spec.graphql.org/draft/#sec-Interface-Extensions + */ case class InterfaceExtension( - baseType: String, - fields: List[Field], - interfaces: List[NamedType], - directives: List[Directive] + baseType: String, + fields: List[Field], + interfaces: List[NamedType], + directives: List[Directive] ) extends TypeExtension /** * Object types represent concrete instantiations of sets of fields. * - * @see https://spec.graphql.org/draft/#sec-Object-Extensions + * @see + * https://spec.graphql.org/draft/#sec-Object-Extensions */ case class ObjectType( - name: String, - description: Option[String], - fields: List[Field], - interfaces: List[NamedType], - directives: List[Directive] + name: String, + description: Option[String], + fields: List[Field], + interfaces: List[NamedType], + directives: List[Directive] ) extends TypeWithFields { override def isObject: Boolean = true } @@ -842,28 +905,31 @@ case class ObjectType( /** * Object extensions allow additional fields to be added to a pre-existing object type * - * @see https://spec.graphql.org/draft/#sec-Object-Extensions - **/ + * @see + * https://spec.graphql.org/draft/#sec-Object-Extensions + */ case class ObjectExtension( - baseType: String, - fields: List[Field], - interfaces: List[NamedType], - directives: List[Directive] + baseType: String, + fields: List[Field], + interfaces: List[NamedType], + directives: List[Directive] ) extends TypeExtension /** - * Unions are an abstract type where no common fields are declared. The possible types of a union - * are explicitly listed out in elements. Types can be made parts of unions without + * Unions are an abstract type where no common fields are declared. The possible types of a + * union are explicitly listed out in elements. Types can be made parts of unions without * modification of that type. * - * @see https://spec.graphql.org/draft/#sec-Unions + * @see + * https://spec.graphql.org/draft/#sec-Unions */ case class UnionType( - name: String, - description: Option[String], - members: List[NamedType], - directives: List[Directive] -) extends Type with NamedType { + name: String, + description: Option[String], + members: List[NamedType], + directives: List[Directive] +) extends Type + with NamedType { override def isUnion: Boolean = true override def toString: String = members.mkString("|") } @@ -871,90 +937,101 @@ case class UnionType( /** * Union extensions allow additional members to be added to a pre-existing union type * - * @see https://spec.graphql.org/draft/#sec-Union-Extensions - **/ + * @see + * https://spec.graphql.org/draft/#sec-Union-Extensions + */ case class UnionExtension( - baseType: String, - members: List[NamedType], - directives: List[Directive] + baseType: String, + members: List[NamedType], + directives: List[Directive] ) extends TypeExtension /** * Enums are special scalars that can only have a defined set of values. * - * @see https://spec.graphql.org/draft/#sec-Enums + * @see + * https://spec.graphql.org/draft/#sec-Enums */ case class EnumType( - name: String, - description: Option[String], - enumValues: List[EnumValueDefinition], - directives: List[Directive] -) extends Type with NamedType { + name: String, + description: Option[String], + enumValues: List[EnumValueDefinition], + directives: List[Directive] +) extends Type + with NamedType { override def isEnum: Boolean = true def hasValue(name: String): Boolean = enumValues.exists(_.name == name) def value(name: String): Option[EnumValue] = valueDefinition(name).map(_ => EnumValue(name)) - def valueDefinition(name: String): Option[EnumValueDefinition] = enumValues.find(_.name == name) + def valueDefinition(name: String): Option[EnumValueDefinition] = + enumValues.find(_.name == name) } /** * Enum extensions allow additional values to be added to a pre-existing enum type * - * @see https://spec.graphql.org/draft/#sec-Enum-Extensions - **/ + * @see + * https://spec.graphql.org/draft/#sec-Enum-Extensions + */ case class EnumExtension( - baseType: String, - enumValues: List[EnumValueDefinition], - directives: List[Directive] + baseType: String, + enumValues: List[EnumValueDefinition], + directives: List[Directive] ) extends TypeExtension /** * The `EnumValue` type represents one of possible values of an enum. * - * @see https://spec.graphql.org/draft/#sec-The-__EnumValue-Type + * @see + * https://spec.graphql.org/draft/#sec-The-__EnumValue-Type */ case class EnumValueDefinition( - name: String, - description: Option[String], - directives: List[Directive] + name: String, + description: Option[String], + directives: List[Directive] ) extends Deprecatable /** - * Input objects are composite types used as inputs into queries defined as a list of named input - * values. + * Input objects are composite types used as inputs into queries defined as a list of named + * input values. * - * @see https://spec.graphql.org/draft/#sec-Input-Objects + * @see + * https://spec.graphql.org/draft/#sec-Input-Objects */ case class InputObjectType( - name: String, - description: Option[String], - inputFields: List[InputValue], - directives: List[Directive] -) extends Type with NamedType { + name: String, + description: Option[String], + inputFields: List[InputValue], + directives: List[Directive] +) extends Type + with NamedType { def inputFieldInfo(name: String): Option[InputValue] = inputFields.find(_.name == name) def isOneOf: Boolean = directives.exists(_.name == "oneOf") } /** - * Input Object extensions allow additional fields to be added to a pre-existing Input Object type + * Input Object extensions allow additional fields to be added to a pre-existing Input Object + * type * - * @see https://spec.graphql.org/draft/#sec-Input-Object-Extensions - **/ + * @see + * https://spec.graphql.org/draft/#sec-Input-Object-Extensions + */ case class InputObjectExtension( - baseType: String, - inputFields: List[InputValue], - directives: List[Directive] + baseType: String, + inputFields: List[InputValue], + directives: List[Directive] ) extends TypeExtension /** * Lists represent sequences of values in GraphQL. A List type is a type modifier: it wraps * another type instance in the ofType field, which defines the type of each item in the list. * - * @see https://spec.graphql.org/draft/#sec-List + * @see + * https://spec.graphql.org/draft/#sec-List */ case class ListType( - ofType: Type + ofType: Type ) extends Type { def directives: List[Directive] = Nil override def toString: String = s"[$ofType]" @@ -965,10 +1042,11 @@ case class ListType( * Non‐null types do not allow null as a response, and indicate required inputs for arguments * and input object fields. * - * @see https://spec.graphql.org/draft/#sec-Non-Null + * @see + * https://spec.graphql.org/draft/#sec-Non-Null */ case class NullableType( - ofType: Type + ofType: Type ) extends Type { def directives: List[Directive] = Nil override def toString: String = s"$ofType?" @@ -977,26 +1055,28 @@ case class NullableType( /** * The `Field` type represents each field in an Object or Interface type. * - * @see https://spec.graphql.org/draft/#sec-The-__Field-Type + * @see + * https://spec.graphql.org/draft/#sec-The-__Field-Type */ case class Field( - name: String, - description: Option[String], - args: List[InputValue], - tpe: Type, - directives: List[Directive] + name: String, + description: Option[String], + args: List[InputValue], + tpe: Type, + directives: List[Directive] ) extends Deprecatable /** - * @param defaultValue a String encoding (using the GraphQL language) of the default value used by - * this input value in the condition a value is not provided at runtime. + * @param defaultValue + * a String encoding (using the GraphQL language) of the default value used by this input + * value in the condition a value is not provided at runtime. */ case class InputValue( - name: String, - description: Option[String], - tpe: Type, - defaultValue: Option[Value], - directives: List[Directive] + name: String, + description: Option[String], + tpe: Type, + defaultValue: Option[Value], + directives: List[Directive] ) extends Deprecatable sealed trait Value @@ -1036,8 +1116,9 @@ object Value { case Ast.Value.NullValue => NullValue.success case Ast.Value.ListValue(vs) => vs.traverse(fromAst).map(ListValue(_)) case Ast.Value.ObjectValue(fs) => - fs.traverse { case (name, value) => - fromAst(value).map(v => (name.value, v)) + fs.traverse { + case (name, value) => + fromAst(value).map(v => (name.value, v)) }.map(ObjectValue(_)) } } @@ -1048,10 +1129,11 @@ object Value { def unapply(value: Value): Option[List[String]] = value match { - case ListValue(l) => l.traverse { - case StringValue(s) => Some(s) - case _ => None - } + case ListValue(l) => + l.traverse { + case StringValue(s) => Some(s) + case _ => None + } case _ => None } } @@ -1076,12 +1158,12 @@ object Value { /** * Resolve a value against its definition. * - * + Absent and null values are defaulted if the InputValue provides a default. - * + Absent and null values are checked against the nullability of the InputValue. - * + Enum values are checked against the possible values of the EnumType. - * + Primitive values are converted to custom Scalars or IDs where appropriate. - * + The elements of list values are checked against their element type. - * + The fields of input object values are checked against their field definitions. + * + Absent and null values are defaulted if the InputValue provides a default. + Absent and + * null values are checked against the nullability of the InputValue. + Enum values are + * checked against the possible values of the EnumType. + Primitive values are converted to + * custom Scalars or IDs where appropriate. + The elements of list values are checked against + * their element type. + The fields of input object values are checked against their field + * definitions. */ def checkValue(iv: InputValue, value: Option[Value], location: String): Result[Value] = (iv.tpe.dealias, value) match { @@ -1122,42 +1204,53 @@ object Value { IDValue(s).success case (IDType, Some(IntValue(i))) => IDValue(i.toString).success - case (e: EnumType, Some(value@EnumValue(name))) if e.hasValue(name) => + case (e: EnumType, Some(value @ EnumValue(name))) if e.hasValue(name) => value.success case (ListType(tpe), Some(ListValue(arr))) => - arr.traverse { elem => - checkValue(iv.copy(tpe = tpe, defaultValue = None), Some(elem), location) - }.map(ListValue.apply) + arr + .traverse { elem => + checkValue(iv.copy(tpe = tpe, defaultValue = None), Some(elem), location) + } + .map(ListValue.apply) case (i @ InputObjectType(nme, _, ivs, _), Some(ObjectValue(fs))) => val obj = fs.toMap val unknownFields = fs.map(_._1).filterNot(f => ivs.exists(_.name == f)) if (unknownFields.nonEmpty) - Result.failure(s"Unknown field(s) ${unknownFields.map(s => s"'$s'").mkString("", ", ", "")} for input object value of type ${nme} in $location") + Result.failure( + s"Unknown field(s) ${unknownFields.map(s => s"'$s'").mkString("", ", ", "")} for input object value of type ${nme} in $location") else { val isOneOf = i.isOneOf val presentValues = obj.filterNot { case (_, v) => v == AbsentValue } if (isOneOf && presentValues.isEmpty) - Result.failure(s"Exactly one key must be specified for oneOf input object ${nme} in $location") - else if (isOneOf && presentValues.size != 1) - Result.failure(s"Exactly one key must be specified for oneOf input object ${nme} in $location, but found ${presentValues.keys.map(s => s"'$s'").mkString(", ")}") + Result.failure( + s"Exactly one key must be specified for oneOf input object ${nme} in $location") + else if (isOneOf && presentValues.size != 1) + Result.failure( + s"Exactly one key must be specified for oneOf input object ${nme} in $location, but found ${presentValues.keys.map(s => s"'$s'").mkString(", ")}") else if (isOneOf && presentValues.exists { case (_, v) => v == NullValue }) - Result.failure(s"Value for member field '${presentValues.head._1}' must be non-null for ${nme} in $location") + Result.failure( + s"Value for member field '${presentValues.head._1}' must be non-null for ${nme} in $location") else - ivs.traverse(iv => checkValue(iv, obj.get(iv.name), location).map(v => (iv.name, v))).map(ObjectValue.apply) + ivs + .traverse(iv => checkValue(iv, obj.get(iv.name), location).map(v => (iv.name, v))) + .map(ObjectValue.apply) } - case (tpe, Some(value)) => Result.failure(s"Expected $tpe found '${SchemaRenderer.renderValue(value)}' for '${iv.name}' in $location") - case (tpe, None) => Result.failure(s"Value of type $tpe required for '${iv.name}' in $location") + case (tpe, Some(value)) => + Result.failure( + s"Expected $tpe found '${SchemaRenderer.renderValue(value)}' for '${iv.name}' in $location") + case (tpe, None) => + Result.failure(s"Value of type $tpe required for '${iv.name}' in $location") } /** * Resolve a Json variable value against its definition. * - * + Absent and null values are defaulted if the InputValue provides a default. - * + Absent and null values are checked against the nullability of the InputValue. - * + Enum values are checked against the possible values of the EnumType. - * + Primitive values are converted to custom Scalars or IDs where appropriate. - * + The elements of list values are checked against their element type. - * + The fields of input object values are checked against their field definitions. + * + Absent and null values are defaulted if the InputValue provides a default. + Absent and + * null values are checked against the nullability of the InputValue. + Enum values are + * checked against the possible values of the EnumType. + Primitive values are converted to + * custom Scalars or IDs where appropriate. + The elements of list values are checked against + * their element type. + The fields of input object values are checked against their field + * definitions. */ def checkVarValue(iv: InputValue, value: Option[Json], location: String): Result[Value] = { import JsonExtractor._ @@ -1197,17 +1290,24 @@ object Value { case (e: EnumType, Some(jsonString(name))) if e.hasValue(name) => EnumValue(name).success case (ListType(tpe), Some(jsonArray(arr))) => - arr.traverse { elem => - checkVarValue(iv.copy(tpe = tpe, defaultValue = None), Some(elem), location) - }.map(vs => ListValue(vs.toList)) + arr + .traverse { elem => + checkVarValue(iv.copy(tpe = tpe, defaultValue = None), Some(elem), location) + } + .map(vs => ListValue(vs.toList)) case (InputObjectType(nme, _, ivs, _), Some(jsonObject(obj))) => val unknownFields = obj.keys.filterNot(f => ivs.exists(_.name == f)) if (unknownFields.nonEmpty) - Result.failure(s"Unknown field(s) ${unknownFields.map(s => s"'$s'").mkString("", ", ", "")} in input object value of type ${nme} in $location") + Result.failure( + s"Unknown field(s) ${unknownFields.map(s => s"'$s'").mkString("", ", ", "")} in input object value of type ${nme} in $location") else - ivs.traverse(iv => checkVarValue(iv, obj(iv.name), location).map(v => (iv.name, v))).map(ObjectValue.apply) - case (tpe, Some(value)) => Result.failure(s"Expected $tpe found '$value' for '${iv.name}' in $location") - case (tpe, None) => Result.failure(s"Value of type $tpe required for '${iv.name}' in $location") + ivs + .traverse(iv => checkVarValue(iv, obj(iv.name), location).map(v => (iv.name, v))) + .map(ObjectValue.apply) + case (tpe, Some(value)) => + Result.failure(s"Expected $tpe found '$value' for '${iv.name}' in $location") + case (tpe, None) => + Result.failure(s"Value of type $tpe required for '${iv.name}' in $location") } } } @@ -1215,14 +1315,15 @@ object Value { /** * The `Directive` type represents a Directive that a server supports. * - * @see https://spec.graphql.org/draft/#sec-The-__Directive-Type + * @see + * https://spec.graphql.org/draft/#sec-The-__Directive-Type */ case class DirectiveDef( - name: String, - description: Option[String], - args: List[InputValue], - isRepeatable: Boolean, - locations: List[DirectiveLocation] + name: String, + description: Option[String], + args: List[InputValue], + isRepeatable: Boolean, + locations: List[DirectiveLocation] ) object DirectiveDef { @@ -1237,7 +1338,10 @@ object DirectiveDef { ), List(InputValue("if", Some("Skipped with true."), BooleanType, None, Nil)), false, - List(DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT) + List( + DirectiveLocation.FIELD, + DirectiveLocation.FRAGMENT_SPREAD, + DirectiveLocation.INLINE_FRAGMENT) ) val Include: DirectiveDef = @@ -1251,7 +1355,10 @@ object DirectiveDef { ), List(InputValue("if", Some("Included when true."), BooleanType, None, Nil)), false, - List(DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT) + List( + DirectiveLocation.FIELD, + DirectiveLocation.FRAGMENT_SPREAD, + DirectiveLocation.INLINE_FRAGMENT) ) val Deprecated: DirectiveDef = @@ -1263,9 +1370,21 @@ object DirectiveDef { |fields on a type or deprecated enum values. """.stripMargin.trim ), - List(InputValue("reason", Some("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)."), StringType, Some(StringValue("No longer supported")), Nil)), + List( + InputValue( + "reason", + Some( + "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)."), + StringType, + Some(StringValue("No longer supported")), + Nil + )), false, - List(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE) + List( + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + DirectiveLocation.ENUM_VALUE) ) val SpecifiedBy: DirectiveDef = @@ -1273,11 +1392,17 @@ object DirectiveDef { "specifiedBy", Some( """|The @specifiedBy built-in directive is used within the type system definition language - |to provide a scalar specification URL for specifying the behavior of custom scalar types. - |The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types. + |to provide a scalar specification URL for specifying the behavior of custom scalar types. + |The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types. """.stripMargin.trim ), - List(InputValue("url", Some("The URL that specifies the behavior of this scalar."), StringType, None, Nil)), + List( + InputValue( + "url", + Some("The URL that specifies the behavior of this scalar."), + StringType, + None, + Nil)), false, List(DirectiveLocation.SCALAR) ) @@ -1297,16 +1422,16 @@ object DirectiveDef { } case class Directive( - name: String, - args: List[Binding] + name: String, + args: List[Binding] ) object Directive { def fromAst(d: Ast.Directive): Result[Directive] = { val Ast.Directive(Ast.Name(nme), args) = d - args.traverse { - case (Ast.Name(nme), value) => Value.fromAst(value).map(Binding(nme, _)) - }.map(Directive(nme, _)) + args + .traverse { case (Ast.Name(nme), value) => Value.fromAst(value).map(Binding(nme, _)) } + .map(Directive(nme, _)) } def validateDirectivesForSchema(schema: Schema): List[Problem] = { @@ -1314,36 +1439,75 @@ object Directive { tpe match { case o: ObjectType => validateDirectives(schema, Ast.DirectiveLocation.OBJECT, o.directives, Map.empty) ++ - o.fields.flatMap(validateFieldDirectives) + o.fields.flatMap(validateFieldDirectives) case i: InterfaceType => - validateDirectives(schema, Ast.DirectiveLocation.INTERFACE, i.directives, Map.empty) ++ - i.fields.flatMap(validateFieldDirectives) + validateDirectives( + schema, + Ast.DirectiveLocation.INTERFACE, + i.directives, + Map.empty) ++ + i.fields.flatMap(validateFieldDirectives) case u: UnionType => validateDirectives(schema, Ast.DirectiveLocation.UNION, u.directives, Map.empty) case e: EnumType => validateDirectives(schema, Ast.DirectiveLocation.ENUM, e.directives, Map.empty) ++ - e.enumValues.flatMap(v => validateDirectives(schema, Ast.DirectiveLocation.ENUM_VALUE, v.directives, Map.empty)) + e.enumValues + .flatMap(v => + validateDirectives( + schema, + Ast.DirectiveLocation.ENUM_VALUE, + v.directives, + Map.empty)) case s: ScalarType => validateDirectives(schema, Ast.DirectiveLocation.SCALAR, s.directives, Map.empty) case i: InputObjectType => - validateDirectives(schema, Ast.DirectiveLocation.INPUT_OBJECT, i.directives, Map.empty) ++ - i.inputFields.flatMap(f => validateDirectives(schema, Ast.DirectiveLocation.INPUT_FIELD_DEFINITION, f.directives, Map.empty)) + validateDirectives( + schema, + Ast.DirectiveLocation.INPUT_OBJECT, + i.directives, + Map.empty) ++ + i.inputFields + .flatMap(f => + validateDirectives( + schema, + Ast.DirectiveLocation.INPUT_FIELD_DEFINITION, + f.directives, + Map.empty)) case _ => Nil } def validateFieldDirectives(field: Field): List[Problem] = - validateDirectives(schema, Ast.DirectiveLocation.FIELD_DEFINITION, field.directives, Map.empty) ++ - field.args.flatMap(a => validateDirectives(schema, Ast.DirectiveLocation.ARGUMENT_DEFINITION, a.directives, Map.empty)) - - validateDirectives(schema, Ast.DirectiveLocation.SCHEMA, schema.schemaType.directives, Map.empty) ++ - (schema.schemaType match { - case twf: TypeWithFields => twf.fields.flatMap(validateFieldDirectives) - case _ => Nil - }) ++ - schema.types.flatMap(validateTypeDirectives) + validateDirectives( + schema, + Ast.DirectiveLocation.FIELD_DEFINITION, + field.directives, + Map.empty) ++ + field + .args + .flatMap(a => + validateDirectives( + schema, + Ast.DirectiveLocation.ARGUMENT_DEFINITION, + a.directives, + Map.empty)) + + validateDirectives( + schema, + Ast.DirectiveLocation.SCHEMA, + schema.schemaType.directives, + Map.empty) ++ + (schema.schemaType match { + case twf: TypeWithFields => twf.fields.flatMap(validateFieldDirectives) + case _ => Nil + }) ++ + schema.types.flatMap(validateTypeDirectives) } - def validateDirectivesForQuery(schema: Schema, op: UntypedOperation, frags: List[UntypedFragment], vars: Vars): Result[Unit] = { + def validateDirectivesForQuery( + schema: Schema, + op: UntypedOperation, + frags: List[UntypedFragment], + vars: Vars): Result[Unit] = { def queryWarnings(query: Query): List[Problem] = { def loop(query: Query): List[Problem] = query match { @@ -1352,23 +1516,27 @@ object Directive { case UntypedFragmentSpread(_, dirs) => validateDirectives(schema, Ast.DirectiveLocation.FRAGMENT_SPREAD, dirs, vars) case UntypedInlineFragment(_, dirs, child) => - validateDirectives(schema, Ast.DirectiveLocation.INLINE_FRAGMENT, dirs, vars) ++ loop(child) - case Select(_, _, child) => loop(child) - case Group(children) => children.flatMap(loop) - case Narrow(_, child) => loop(child) - case Unique(child) => loop(child) - case Filter(_, child) => loop(child) - case Limit(_, child) => loop(child) - case Offset(_, child) => loop(child) - case OrderBy(_, child) => loop(child) - case Introspect(_, child) => loop(child) - case Environment(_, child) => loop(child) - case Component(_, _, child) => loop(child) - case Effect(_, child) => loop(child) + validateDirectives( + schema, + Ast.DirectiveLocation.INLINE_FRAGMENT, + dirs, + vars) ++ loop(child) + case Select(_, _, child) => loop(child) + case Group(children) => children.flatMap(loop) + case Narrow(_, child) => loop(child) + case Unique(child) => loop(child) + case Filter(_, child) => loop(child) + case Limit(_, child) => loop(child) + case Offset(_, child) => loop(child) + case OrderBy(_, child) => loop(child) + case Introspect(_, child) => loop(child) + case Environment(_, child) => loop(child) + case Component(_, _, child) => loop(child) + case Effect(_, child) => loop(child) case TransformCursor(_, child) => loop(child) - case Count(_) => Nil - case Empty => Nil - } + case Count(_) => Nil + case Empty => Nil + } loop(query) } @@ -1380,14 +1548,25 @@ object Directive { case _: UntypedSubscription => Ast.DirectiveLocation.SUBSCRIPTION } - val varWarnings = op.variables.flatMap(v => validateDirectives(schema, Ast.DirectiveLocation.VARIABLE_DEFINITION, v.directives, vars)) + val varWarnings = op + .variables + .flatMap(v => + validateDirectives( + schema, + Ast.DirectiveLocation.VARIABLE_DEFINITION, + v.directives, + vars)) val opWarnings = validateDirectives(schema, opLocation, op.directives, vars) val childWarnings = queryWarnings(op.query) varWarnings ++ opWarnings ++ childWarnings } def fragmentWarnings(frag: UntypedFragment): List[Problem] = { - val defnWarnings = validateDirectives(schema, Ast.DirectiveLocation.FRAGMENT_DEFINITION, frag.directives, vars) + val defnWarnings = validateDirectives( + schema, + Ast.DirectiveLocation.FRAGMENT_DEFINITION, + frag.directives, + vars) val childWarnings = queryWarnings(frag.child) defnWarnings ++ childWarnings } @@ -1398,30 +1577,38 @@ object Directive { Result.fromProblems(opWarnings ++ fragWarnings) } - def validateDirectiveOccurrences(schema: Schema, location: Ast.DirectiveLocation, directives: List[Directive]): List[Problem] = { + def validateDirectiveOccurrences( + schema: Schema, + location: Ast.DirectiveLocation, + directives: List[Directive]): List[Problem] = { val (locationProblems, repetitionProblems) = - directives.foldLeft((List.empty[Problem], List.empty[Problem])) { case ((locs, reps), directive) => - val nme = directive.name - schema.directives.find(_.name == nme) match { - case None => (Problem(s"Undefined directive '$nme'") :: locs, reps) - case Some(defn) => - val locs0 = - if (defn.locations.contains(location)) locs - else Problem(s"Directive '$nme' is not allowed on $location") :: locs - - val reps0 = - if (!defn.isRepeatable && directives.count(_.name == nme) > 1) - Problem(s"Directive '$nme' may not occur more than once") :: reps - else reps - - (locs0, reps0) - } + directives.foldLeft((List.empty[Problem], List.empty[Problem])) { + case ((locs, reps), directive) => + val nme = directive.name + schema.directives.find(_.name == nme) match { + case None => (Problem(s"Undefined directive '$nme'") :: locs, reps) + case Some(defn) => + val locs0 = + if (defn.locations.contains(location)) locs + else Problem(s"Directive '$nme' is not allowed on $location") :: locs + + val reps0 = + if (!defn.isRepeatable && directives.count(_.name == nme) > 1) + Problem(s"Directive '$nme' may not occur more than once") :: reps + else reps + + (locs0, reps0) + } } locationProblems.reverse ++ repetitionProblems.reverse.distinct } - def validateDirectives(schema: Schema, location: Ast.DirectiveLocation, directives: List[Directive], vars: Vars): List[Problem] = { + def validateDirectives( + schema: Schema, + location: Ast.DirectiveLocation, + directives: List[Directive], + vars: Vars): List[Problem] = { val occurrenceProblems = validateDirectiveOccurrences(schema, location, directives) val argProblems = directives.flatMap { directive => @@ -1432,15 +1619,19 @@ object Directive { val infos = defn.args val unknownArgs = directive.args.filterNot(arg => infos.exists(_.name == arg.name)) if (unknownArgs.nonEmpty) - List(Problem(s"Unknown argument(s) ${unknownArgs.map(s => s"'${s.name}'").mkString("", ", ", "")} in directive $nme")) + List(Problem( + s"Unknown argument(s) ${unknownArgs.map(s => s"'${s.name}'").mkString("", ", ", "")} in directive $nme")) else { val argMap = directive.args.groupMapReduce(_.name)(_.value)((x, _) => x) - infos.traverse { info => - for { - value <- argMap.get(info.name).traverse(Value.elaborateValue(_, vars)) - _ <- checkValue(info, value, s"directive ${defn.name}") - } yield () - }.toProblems.toList + infos + .traverse { info => + for { + value <- argMap.get(info.name).traverse(Value.elaborateValue(_, vars)) + _ <- checkValue(info, value, s"directive ${defn.name}") + } yield () + } + .toProblems + .toList } } } @@ -1448,19 +1639,25 @@ object Directive { occurrenceProblems ++ argProblems } - def elaborateDirectives(schema: Schema, directives: List[Directive], vars: Vars): Result[List[Directive]] = + def elaborateDirectives( + schema: Schema, + directives: List[Directive], + vars: Vars): Result[List[Directive]] = directives.traverse { directive => val nme = directive.name schema.directives.find(_.name == nme) match { case None => Result.failure(s"Undefined directive '$nme'") case Some(defn) => val argMap = directive.args.groupMapReduce(_.name)(_.value)((x, _) => x) - defn.args.traverse { info => - for { - value0 <- argMap.get(info.name).traverse(Value.elaborateValue(_, vars)) - value1 <- checkValue(info, value0, s"directive ${defn.name}") - } yield Binding(info.name, value1) - }.map(eArgs => directive.copy(args = eArgs)) + defn + .args + .traverse { info => + for { + value0 <- argMap.get(info.name).traverse(Value.elaborateValue(_, vars)) + value1 <- checkValue(info, value0, s"directive ${defn.name}") + } yield Binding(info.name, value1) + } + .map(eArgs => directive.copy(args = eArgs)) } } } @@ -1469,8 +1666,8 @@ object Directive { * GraphQL schema parser */ trait SchemaParser { - def parseText(text: String)(implicit pos: SourcePos): Result[Schema] - def parseDocument(doc: Ast.Document)(implicit sourcePos: SourcePos): Result[Schema] + def parseText(text: String)(implicit pos: SourcePos): Result[Schema] + def parseDocument(doc: Ast.Document)(implicit sourcePos: SourcePos): Result[Schema] } object SchemaParser { @@ -1479,13 +1676,21 @@ object SchemaParser { private final class Impl(parser: GraphQLParser) extends SchemaParser { - import Ast.{Directive => _, EnumValueDefinition => _, SchemaExtension => _, Type => _, TypeExtension => _, Value => _, _} + import Ast.{ + Directive => _, + EnumValueDefinition => _, + SchemaExtension => _, + Type => _, + TypeExtension => _, + Value => _, + _ + } /** - * Parse a GraphQL schema String to a Schema value - * - * Yields a Schema value on the right and accumulates errors on the left. - */ + * Parse a GraphQL schema String to a Schema value + * + * Yields a Schema value on the right and accumulates errors on the left. + */ def parseText(text: String)(implicit pos: SourcePos): Result[Schema] = for { doc <- parser.parseText(text) @@ -1504,7 +1709,12 @@ object SchemaParser { var schemaExtensions: List[SchemaExtension] = Nil var typeExtensions: List[TypeExtension] = Nil - def complete(types0: List[NamedType], baseSchemaType0: Option[NamedType], directives0: List[DirectiveDef], schemaExtensions0: List[SchemaExtension], typeExtensions0: List[TypeExtension]): Unit = { + def complete( + types0: List[NamedType], + baseSchemaType0: Option[NamedType], + directives0: List[DirectiveDef], + schemaExtensions0: List[SchemaExtension], + typeExtensions0: List[TypeExtension]): Unit = { baseTypes = types0 baseSchemaType1 = baseSchemaType0 directives = directives0 ++ DirectiveDef.builtIns @@ -1513,19 +1723,25 @@ object SchemaParser { } } - val schemaExtnDefns: List[Ast.SchemaExtension] = doc.collect { case tpe: Ast.SchemaExtension => tpe } + val schemaExtnDefns: List[Ast.SchemaExtension] = doc.collect { + case tpe: Ast.SchemaExtension => tpe + } val typeDefns: List[TypeDefinition] = doc.collect { case tpe: TypeDefinition => tpe } - val dirDefns: List[DirectiveDefinition] = doc.collect { case dir: DirectiveDefinition => dir } - val extnDefns: List[Ast.TypeExtension] = doc.collect { case tpe: Ast.TypeExtension => tpe } + val dirDefns: List[DirectiveDefinition] = doc.collect { + case dir: DirectiveDefinition => dir + } + val extnDefns: List[Ast.TypeExtension] = doc.collect { + case tpe: Ast.TypeExtension => tpe + } for { - baseTypes <- mkTypeDefs(schema, typeDefns) + baseTypes <- mkTypeDefs(schema, typeDefns) schemaExtns <- mkSchemaExtensions(schema, schemaExtnDefns) - typeExtns <- mkExtensions(schema, extnDefns) - directives <- mkDirectiveDefs(schema, dirDefns) - schemaType <- mkSchemaType(schema, doc) - _ = schema.complete(baseTypes, schemaType, directives, schemaExtns, typeExtns) - _ <- Result.fromProblems(SchemaValidator.validateSchema(schema, typeDefns, extnDefns)) + typeExtns <- mkExtensions(schema, extnDefns) + directives <- mkDirectiveDefs(schema, dirDefns) + schemaType <- mkSchemaType(schema, doc) + _ = schema.complete(baseTypes, schemaType, directives, schemaExtns, typeExtns) + _ <- Result.fromProblems(SchemaValidator.validateSchema(schema, typeDefns, extnDefns)) } yield schema } @@ -1534,11 +1750,15 @@ object SchemaParser { // explicit Schema type, if any def mkSchemaType(schema: Schema, doc: Document): Result[Option[NamedType]] = { def build(dirs: List[Directive], ops: List[Field]): NamedType = { - val query = ops.find(_.name == "query").getOrElse(Field("query", None, Nil, defaultQueryType, Nil)) + val query = ops + .find(_.name == "query") + .getOrElse(Field("query", None, Nil, defaultQueryType, Nil)) ObjectType( name = "Schema", description = None, - fields = query :: List(ops.find(_.name == "mutation"), ops.find(_.name == "subscription")).flatten, + fields = query :: List( + ops.find(_.name == "mutation"), + ops.find(_.name == "subscription")).flatten, interfaces = Nil, directives = dirs ) @@ -1551,7 +1771,7 @@ object SchemaParser { case Nil => None.success case SchemaDefinition(rootOpTpes, dirs0) :: Nil => for { - ops <- rootOpTpes.traverse(mkRootOperation(schema)) + ops <- rootOpTpes.traverse(mkRootOperation(schema)) dirs <- dirs0.traverse(Directive.fromAst) } yield Some(build(dirs, ops)) @@ -1559,13 +1779,15 @@ object SchemaParser { } } - def mkSchemaExtensions(schema: Schema, extnDefns: List[Ast.SchemaExtension]): Result[List[SchemaExtension]] = + def mkSchemaExtensions( + schema: Schema, + extnDefns: List[Ast.SchemaExtension]): Result[List[SchemaExtension]] = extnDefns.traverse(mkSchemaExtension(schema)) def mkSchemaExtension(schema: Schema)(se: Ast.SchemaExtension): Result[SchemaExtension] = { val Ast.SchemaExtension(rootOpTpes, dirs0) = se for { - ops <- rootOpTpes.traverse(mkRootOperation(schema)) + ops <- rootOpTpes.traverse(mkRootOperation(schema)) dirs <- dirs0.traverse(Directive.fromAst) } yield SchemaExtension(ops, dirs) } @@ -1574,46 +1796,50 @@ object SchemaParser { val RootOperationTypeDefinition(optype, tpe, dirs0) = rootTpe for { dirs <- dirs0.traverse(Directive.fromAst) - tpe <- mkType(schema)(tpe) - _ <- Result.failure(s"Root operation types must be named types, found '$tpe'").whenA(!tpe.nonNull.isNamed) + tpe <- mkType(schema)(tpe) + _ <- Result + .failure(s"Root operation types must be named types, found '$tpe'") + .whenA(!tpe.nonNull.isNamed) } yield Field(optype.name, None, Nil, tpe, dirs) } - def mkExtensions(schema: Schema, extnDefns: List[Ast.TypeExtension]): Result[List[TypeExtension]] = + def mkExtensions( + schema: Schema, + extnDefns: List[Ast.TypeExtension]): Result[List[TypeExtension]] = extnDefns.traverse(mkExtension(schema)) def mkExtension(schema: Schema)(ed: Ast.TypeExtension): Result[TypeExtension] = ed match { case ScalarTypeExtension(Ast.Type.Named(Name(name)), dirs0) => for { - dirs <- dirs0.traverse(Directive.fromAst) + dirs <- dirs0.traverse(Directive.fromAst) } yield ScalarExtension(name, dirs) case InterfaceTypeExtension(Ast.Type.Named(Name(name)), fields0, ifs0, dirs0) => for { fields <- fields0.traverse(mkField(schema)) - ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } - dirs <- dirs0.traverse(Directive.fromAst) + ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) } yield InterfaceExtension(name, fields, ifs, dirs) case ObjectTypeExtension(Ast.Type.Named(Name(name)), fields0, ifs0, dirs0) => for { fields <- fields0.traverse(mkField(schema)) - ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } - dirs <- dirs0.traverse(Directive.fromAst) + ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) } yield ObjectExtension(name, fields, ifs, dirs) case UnionTypeExtension(Ast.Type.Named(Name(name)), dirs0, members0) => for { - dirs <- dirs0.traverse(Directive.fromAst) - members = members0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) + members = members0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } } yield UnionExtension(name, members, dirs) case EnumTypeExtension(Ast.Type.Named(Name(name)), dirs0, values0) => for { - values <- values0.traverse(mkEnumValue) - dirs <- dirs0.traverse(Directive.fromAst) + values <- values0.traverse(mkEnumValue) + dirs <- dirs0.traverse(Directive.fromAst) } yield EnumExtension(name, values, dirs) case InputObjectTypeExtension(Ast.Type.Named(Name(name)), dirs0, fields0) => for { fields <- fields0.traverse(mkInputValue(schema)) - dirs <- dirs0.traverse(Directive.fromAst) + dirs <- dirs0.traverse(Directive.fromAst) } yield InputObjectExtension(name, fields, dirs) } @@ -1635,41 +1861,49 @@ object SchemaParser { else for { fields <- fields0.traverse(mkField(schema)) - ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } - dirs <- dirs0.traverse(Directive.fromAst) + ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) } yield ObjectType(nme, desc, fields, ifs, dirs) case InterfaceTypeDefinition(Name(nme), desc, fields0, ifs0, dirs0) => - if (fields0.isEmpty) Result.failure(s"interface type $nme must define at least one field") + if (fields0.isEmpty) + Result.failure(s"interface type $nme must define at least one field") else for { fields <- fields0.traverse(mkField(schema)) - ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } - dirs <- dirs0.traverse(Directive.fromAst) + ifs = ifs0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) } yield InterfaceType(nme, desc, fields, ifs, dirs) case UnionTypeDefinition(Name(nme), desc, dirs0, members0) => if (members0.isEmpty) Result.failure(s"union type $nme must define at least one member") else { for { - dirs <- dirs0.traverse(Directive.fromAst) - members = members0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } + dirs <- dirs0.traverse(Directive.fromAst) + members = members0.map { case Ast.Type.Named(Name(nme)) => lazyRef(schema, nme) } } yield UnionType(nme, desc, members, dirs) } case EnumTypeDefinition(Name(nme), desc, dirs0, values0) => - if (values0.isEmpty) Result.failure(s"enum type $nme must define at least one enum value") + if (values0.isEmpty) + Result.failure(s"enum type $nme must define at least one enum value") else for { values <- values0.traverse(mkEnumValue) - dirs <- dirs0.traverse(Directive.fromAst) + dirs <- dirs0.traverse(Directive.fromAst) } yield EnumType(nme, desc, values, dirs) case InputObjectTypeDefinition(Name(nme), desc, fields0, dirs0) => - if (fields0.isEmpty) Result.failure(s"input object type $nme must define at least one input field") + if (fields0.isEmpty) + Result.failure(s"input object type $nme must define at least one input field") else for { fields <- fields0.traverse(mkInputValue(schema)) - oneOfFieldErrors = if (dirs0.exists(_.name.value == "oneOf")) fields.filterNot(_.tpe.isNullable) else Nil - _ <- if (oneOfFieldErrors.nonEmpty) Result.failure(s"oneOf input object type $nme may not have non-nullable field(s): ${oneOfFieldErrors.map(f => s"'${f.name}'").mkString(", ")}") - else Result.unit - dirs <- dirs0.traverse(Directive.fromAst) + oneOfFieldErrors = + if (dirs0.exists(_.name.value == "oneOf")) fields.filterNot(_.tpe.isNullable) + else Nil + _ <- + if (oneOfFieldErrors.nonEmpty) + Result.failure( + s"oneOf input object type $nme may not have non-nullable field(s): ${oneOfFieldErrors.map(f => s"'${f.name}'").mkString(", ")}") + else Result.unit + dirs <- dirs0.traverse(Directive.fromAst) } yield InputObjectType(nme, desc, fields, dirs) } @@ -1677,7 +1911,7 @@ object SchemaParser { val FieldDefinition(Name(nme), desc, args0, tpe0, dirs0) = f for { args <- args0.traverse(mkInputValue(schema)) - tpe <- mkType(schema)(tpe0) + tpe <- mkType(schema)(tpe0) dirs <- dirs0.traverse(Directive.fromAst) } yield Field(nme, desc, args, tpe, dirs) } @@ -1690,14 +1924,17 @@ object SchemaParser { case Ast.Type.List(tpe) => loop(tpe, true).map(tpe => wrap(ListType(tpe))) case Ast.Type.NonNull(Left(tpe)) => loop(tpe, false) case Ast.Type.NonNull(Right(tpe)) => loop(tpe, false) - case Ast.Type.Named(Name(nme)) => wrap(ScalarType.builtIn(nme).getOrElse(lazyRef(schema, nme))).success + case Ast.Type.Named(Name(nme)) => + wrap(ScalarType.builtIn(nme).getOrElse(lazyRef(schema, nme))).success } } loop(tpe, true) } - def mkDirectiveDefs(schema: Schema, defns: List[DirectiveDefinition]): Result[List[DirectiveDef]] = + def mkDirectiveDefs( + schema: Schema, + defns: List[DirectiveDefinition]): Result[List[DirectiveDef]] = defns.traverse(mkDirectiveDef(schema)) def mkDirectiveDef(schema: Schema)(dd: DirectiveDefinition): Result[DirectiveDef] = { @@ -1728,16 +1965,19 @@ object SchemaParser { object SchemaValidator { import SchemaRenderer.renderType - def validateSchema(schema: Schema, defns: List[TypeDefinition], typeExtnDefns: List[Ast.TypeExtension]): List[Problem] = + def validateSchema( + schema: Schema, + defns: List[TypeDefinition], + typeExtnDefns: List[Ast.TypeExtension]): List[Problem] = validateReferences(schema, defns) ++ - validateUniqueDefns(schema) ++ - validateUniqueFields(schema) ++ - validateUnionMembers(schema) ++ - validateUniqueEnumValues(schema) ++ - validateInterfaces(schema) ++ - validateImplementations(schema) ++ - validateTypeExtensions(defns, typeExtnDefns) ++ - Directive.validateDirectivesForSchema(schema) + validateUniqueDefns(schema) ++ + validateUniqueFields(schema) ++ + validateUnionMembers(schema) ++ + validateUniqueEnumValues(schema) ++ + validateInterfaces(schema) ++ + validateImplementations(schema) ++ + validateTypeExtensions(defns, typeExtnDefns) ++ + Directive.validateDirectivesForSchema(schema) def validateReferences(schema: Schema, defns: List[TypeDefinition]): List[Problem] = { def underlyingName(tpe: Ast.Type): String = @@ -1769,9 +2009,13 @@ object SchemaValidator { } def validateUniqueDefns(schema: Schema): List[Problem] = { - val dupes = schema.types.groupBy(_.name).collect { - case (nme, tpes) if tpes.length > 1 => nme - }.toSet + val dupes = schema + .types + .groupBy(_.name) + .collect { + case (nme, tpes) if tpes.length > 1 => nme + } + .toSet schema.types.map(_.name).distinct.collect { case nme if dupes.contains(nme) => Problem(s"Duplicate definition of type '$nme' found") @@ -1779,72 +2023,88 @@ object SchemaValidator { } def validateUniqueFields(schema: Schema): List[Problem] = { - val withFields = schema.types.collect { - case wf: TypeWithFields => wf - } + val withFields = schema.types.collect { case wf: TypeWithFields => wf } - val inputObjs = schema.types.collect { - case io: InputObjectType => io - } + val inputObjs = schema.types.collect { case io: InputObjectType => io } withFields.flatMap { tpe => - val dupes = tpe.fields.groupBy(_.name).collect { - case (nme, fields) if fields.length > 1 => nme - }.toSet + val dupes = tpe + .fields + .groupBy(_.name) + .collect { + case (nme, fields) if fields.length > 1 => nme + } + .toSet tpe.fields.map(_.name).distinct.collect { - case nme if dupes.contains(nme) => Problem(s"Duplicate definition of field '$nme' for type '${tpe.name}'") + case nme if dupes.contains(nme) => + Problem(s"Duplicate definition of field '$nme' for type '${tpe.name}'") } } ++ - inputObjs.flatMap { tpe => - val dupes = tpe.inputFields.groupBy(_.name).collect { - case (nme, fields) if fields.length > 1 => nme - }.toSet + inputObjs.flatMap { tpe => + val dupes = tpe + .inputFields + .groupBy(_.name) + .collect { + case (nme, fields) if fields.length > 1 => nme + } + .toSet - tpe.inputFields.map(_.name).distinct.collect { - case nme if dupes.contains(nme) => Problem(s"Duplicate definition of field '$nme' for type '${tpe.name}'") + tpe.inputFields.map(_.name).distinct.collect { + case nme if dupes.contains(nme) => + Problem(s"Duplicate definition of field '$nme' for type '${tpe.name}'") + } } - } } def validateUnionMembers(schema: Schema): List[Problem] = { - val unions = schema.types.collect { - case u: UnionType => u - } + val unions = schema.types.collect { case u: UnionType => u } unions.flatMap { tpe => - val dupes = tpe.members.groupBy(_.name).collect { - case (nme, vs) if vs.length > 1 => nme - }.toSet + val dupes = tpe + .members + .groupBy(_.name) + .collect { + case (nme, vs) if vs.length > 1 => nme + } + .toSet tpe.members.map(_.name).distinct.collect { - case nme if dupes.contains(nme) => Problem(s"Duplicate inclusion of union member '$nme' for type '${tpe.name}'") + case nme if dupes.contains(nme) => + Problem(s"Duplicate inclusion of union member '$nme' for type '${tpe.name}'") } ++ - tpe.members.flatMap { member => - schema.definition(member.name) match { - case None => List(Problem(s"Undefined type '${member.name}' included in union '${tpe.name}'")) - case Some(mtpe) => - mtpe match { - case (_: ObjectType) => Nil - case _ => List(Problem(s"Non-object type '${member.name}' included in union '${tpe.name}'")) - } + tpe.members.flatMap { member => + schema.definition(member.name) match { + case None => + List(Problem(s"Undefined type '${member.name}' included in union '${tpe.name}'")) + case Some(mtpe) => + mtpe match { + case _: ObjectType => Nil + case _ => + List( + Problem( + s"Non-object type '${member.name}' included in union '${tpe.name}'")) + } + } } - } } } def validateUniqueEnumValues(schema: Schema): List[Problem] = { - val enums = schema.types.collect { - case e: EnumType => e - } + val enums = schema.types.collect { case e: EnumType => e } enums.flatMap { tpe => - val dupes = tpe.enumValues.groupBy(_.name).collect { - case (nme, vs) if vs.length > 1 => nme - }.toSet + val dupes = tpe + .enumValues + .groupBy(_.name) + .collect { + case (nme, vs) if vs.length > 1 => nme + } + .toSet tpe.enumValues.map(_.name).distinct.collect { - case nme if dupes.contains(nme) => Problem(s"Duplicate definition of enum value '$nme' for type '${tpe.name}'") + case nme if dupes.contains(nme) => + Problem(s"Duplicate definition of enum value '$nme' for type '${tpe.name}'") } } } @@ -1868,7 +2128,7 @@ object SchemaValidator { @annotation.tailrec def loop(pendingIfs: Set[String]): Either[String, Set[String]] = { - if(pendingIfs.isEmpty) Right(Set.empty[String]) + if (pendingIfs.isEmpty) Right(Set.empty[String]) else { val hd = pendingIfs.head checkCycle(Set(hd), Set.empty[String]) match { @@ -1890,11 +2150,14 @@ object SchemaValidator { import impl.{name, fields, interfaces} val duplicateImplementsProblems = { - val dupes = interfaces.groupBy(identity).collect { case (i, occurrences) if occurrences.sizeCompare(1) > 0 => i.name } + val dupes = interfaces.groupBy(identity).collect { + case (i, occurrences) if occurrences.sizeCompare(1) > 0 => i.name + } if (dupes.isEmpty) Nil else { val plural = if (dupes.sizeCompare(1) > 0) "s:" else "" - List(Problem(s"Implements clause of type '$name' has duplicate occurrences of interface$plural ${dupes.map(d => s"'$d'").mkString(", ")}")) + List(Problem( + s"Implements clause of type '$name' has duplicate occurrences of interface$plural ${dupes.map(d => s"'$d'").mkString(", ")}")) } } @@ -1909,11 +2172,13 @@ object SchemaValidator { case _ :: tl => loop(tl, acc) } - val missingTransitives = loop(interfaces.map(_.dealias), Set.empty) -- interfaces.map(_.name) - name + val missingTransitives = + loop(interfaces.map(_.dealias), Set.empty) -- interfaces.map(_.name) - name if (missingTransitives.isEmpty) Nil else { val plural = if (missingTransitives.sizeCompare(1) > 0) "s:" else "" - List(Problem(s"Type '$name' does not directly implement transitively implemented interface$plural ${missingTransitives.map(i => s"'$i'").mkString(", ")}")) + List(Problem( + s"Type '$name' does not directly implement transitively implemented interface$plural ${missingTransitives.map(i => s"'$i'").mkString(", ")}")) } } @@ -1921,30 +2186,43 @@ object SchemaValidator { interfaces.flatMap(_.dealias match { case iface: InterfaceType => iface.fields.flatMap { ifField => - fields.find(_.name == ifField.name).map { implField => - val ifTpe = ifField.tpe - val implTpe = implField.tpe - - val rp = - if (implTpe <:< ifTpe) Nil - else List(Problem(s"Field '${implField.name}' of type '$name' has type '${renderType(implTpe)}', however implemented interface '${iface.name}' requires it to be a subtype of '${renderType(ifTpe)}'")) - - val argsMatch = - implField.args.corresponds(ifField.args) { case (arg0, arg1) => - arg0.name == arg1.name && arg0.tpe == arg1.tpe - } - - val ap = - if (argsMatch) Nil - else List(Problem(s"Field '${implField.name}' of type '$name' has has an argument list that does not conform to that specified by implemented interface '${iface.name}'")) - - rp ++ ap - }.getOrElse(List(Problem(s"Field '${ifField.name}' from interface '${iface.name}' is not defined by implementing type '$name'"))) + fields + .find(_.name == ifField.name) + .map { implField => + val ifTpe = ifField.tpe + val implTpe = implField.tpe + + val rp = + if (implTpe <:< ifTpe) Nil + else + List(Problem( + s"Field '${implField.name}' of type '$name' has type '${renderType(implTpe)}', however implemented interface '${iface.name}' requires it to be a subtype of '${renderType(ifTpe)}'")) + + val argsMatch = + implField.args.corresponds(ifField.args) { + case (arg0, arg1) => + arg0.name == arg1.name && arg0.tpe == arg1.tpe + } + + val ap = + if (argsMatch) Nil + else + List(Problem( + s"Field '${implField.name}' of type '$name' has has an argument list that does not conform to that specified by implemented interface '${iface.name}'")) + + rp ++ ap + } + .getOrElse(List(Problem( + s"Field '${ifField.name}' from interface '${iface.name}' is not defined by implementing type '$name'"))) } case undefined: TypeRef => - List(Problem(s"Undefined type '${undefined.name}' declared as implemented by type '$name'")) + List( + Problem( + s"Undefined type '${undefined.name}' declared as implemented by type '$name'")) case other => - List(Problem(s"Non-interface type '${other.name}' declared as implemented by type '$name'")) + List( + Problem( + s"Non-interface type '${other.name}' declared as implemented by type '$name'")) }) duplicateImplementsProblems ++ transitiveImplementsProblems ++ implementationProblems @@ -1954,44 +2232,49 @@ object SchemaValidator { impls.flatMap(validateImplementor) } - def validateTypeExtensions(defns: List[TypeDefinition], extnDefns: List[Ast.TypeExtension]): List[Problem] = { + def validateTypeExtensions( + defns: List[TypeDefinition], + extnDefns: List[Ast.TypeExtension]): List[Problem] = { extnDefns.mapFilter { extension => - val notFound = Some(Problem(s"Unable apply extension to non-existent ${extension.baseType.name}")) - defns.find(_.name == extension.baseType.astName).fold[Option[Problem]](notFound) { baseType => - def wrongTypeExtended(typ: String) = Problem(s"Attempted to apply $typ extension to ${baseType.name.value} but it is not a $typ").some - - extension match { - case _: Ast.ScalarTypeExtension => - baseType match { - case _: Ast.ScalarTypeDefinition => None - case _ => wrongTypeExtended("Scalar") + val notFound = + Some(Problem(s"Unable apply extension to non-existent ${extension.baseType.name}")) + defns.find(_.name == extension.baseType.astName).fold[Option[Problem]](notFound) { + baseType => + def wrongTypeExtended(typ: String) = Problem( + s"Attempted to apply $typ extension to ${baseType.name.value} but it is not a $typ").some + + extension match { + case _: Ast.ScalarTypeExtension => + baseType match { + case _: Ast.ScalarTypeDefinition => None + case _ => wrongTypeExtended("Scalar") + } + case _: Ast.InterfaceTypeExtension => + baseType match { + case _: Ast.InterfaceTypeDefinition => None + case _ => wrongTypeExtended("Interface") + } + case _: Ast.ObjectTypeExtension => + baseType match { + case _: Ast.ObjectTypeDefinition => None + case _ => wrongTypeExtended("Object") + } + case _: Ast.UnionTypeExtension => + baseType match { + case _: Ast.UnionTypeDefinition => None + case _ => wrongTypeExtended("Union") + } + case _: Ast.EnumTypeExtension => + baseType match { + case _: Ast.EnumTypeDefinition => None + case _ => wrongTypeExtended("Enum") + } + case _: Ast.InputObjectTypeExtension => + baseType match { + case _: Ast.InputObjectTypeDefinition => None + case _ => wrongTypeExtended("Input Object") + } } - case _: Ast.InterfaceTypeExtension => - baseType match { - case _: Ast.InterfaceTypeDefinition => None - case _ => wrongTypeExtended("Interface") - } - case _: Ast.ObjectTypeExtension => - baseType match { - case _: Ast.ObjectTypeDefinition => None - case _ => wrongTypeExtended("Object") - } - case _: Ast.UnionTypeExtension => - baseType match { - case _: Ast.UnionTypeDefinition => None - case _ => wrongTypeExtended("Union") - } - case _: Ast.EnumTypeExtension => - baseType match { - case _: Ast.EnumTypeDefinition => None - case _ => wrongTypeExtended("Enum") - } - case _: Ast.InputObjectTypeExtension => - baseType match { - case _: Ast.InputObjectTypeDefinition => None - case _ => wrongTypeExtended("Input Object") - } - } } } } @@ -2001,12 +2284,10 @@ object SchemaRenderer { def renderSchema(schema: Schema): String = { val schemaDefn = { val dirs0 = schema.baseSchemaType.directives - if ( - schema.queryType.name == "Query" && + if (schema.queryType.name == "Query" && schema.mutationType.forall(_.name == "Mutation") && schema.subscriptionType.forall(_.name == "Subscription") && - dirs0.isEmpty - ) "" + dirs0.isEmpty) "" else { val fields = schema.baseSchemaType match { @@ -2020,22 +2301,28 @@ object SchemaRenderer { } val schemaExtnDefns = - if(schema.schemaExtensions.isEmpty) "" - else "\n"+schema.schemaExtensions.map(renderSchemaExtension).mkString("\n") + if (schema.schemaExtensions.isEmpty) "" + else "\n" + schema.schemaExtensions.map(renderSchemaExtension).mkString("\n") val typeExtnDefns = - if(schema.typeExtensions.isEmpty) "" - else "\n"+schema.typeExtensions.map(renderTypeExtension).mkString("\n") + if (schema.typeExtensions.isEmpty) "" + else "\n" + schema.typeExtensions.map(renderTypeExtension).mkString("\n") val dirDefns = { val nonBuiltInDefns = schema.directives.filter { - case DirectiveDef("skip"|"include"|"deprecated"|"specifiedBy"|"oneOf", _, _, _, _) => false + case DirectiveDef( + "skip" | "include" | "deprecated" | "specifiedBy" | "oneOf", + _, + _, + _, + _) => + false case _ => true } - if(nonBuiltInDefns.isEmpty) "" - else "\n"+nonBuiltInDefns.map(renderDirectiveDefn).mkString("\n")+"\n" + if (nonBuiltInDefns.isEmpty) "" + else "\n" + nonBuiltInDefns.map(renderDirectiveDefn).mkString("\n") + "\n" } schemaDefn ++ @@ -2056,7 +2343,12 @@ object SchemaRenderer { def renderDirective(d: Directive): String = { val Directive(name, args0) = d - val args = if(args0.isEmpty) "" else args0.map { case Binding(nme, v) => s"$nme: ${renderValue(v)}" }.mkString("(", ", ", ")") + val args = + if (args0.isEmpty) "" + else + args0 + .map { case Binding(nme, v) => s"$nme: ${renderValue(v)}" } + .mkString("(", ", ", ")") s"@$name$args" } @@ -2092,7 +2384,7 @@ object SchemaRenderer { val ifs = if (ifs0.isEmpty) "" else " implements " + ifs0.map(_.name).mkString("&") val dirs = renderDirectives(dirs0) val fields = - if(fields0.isEmpty) "" + if (fields0.isEmpty) "" else s"""| { | ${fields0.map(renderField).mkString("\n ")} @@ -2104,7 +2396,7 @@ object SchemaRenderer { val ifs = if (ifs0.isEmpty) "" else " implements " + ifs0.map(_.name).mkString("&") val dirs = renderDirectives(dirs0) val fields = - if(fields0.isEmpty) "" + if (fields0.isEmpty) "" else s"""| { | ${fields0.map(renderField).mkString("\n ")} @@ -2115,7 +2407,7 @@ object SchemaRenderer { case UnionExtension(nme, members0, dirs0) => val dirs = renderDirectives(dirs0) val members = - if(members0.isEmpty) "" + if (members0.isEmpty) "" else s" = ${members0.map(_.name).mkString(" | ")}" s"extend union $nme$dirs$members" @@ -2123,7 +2415,7 @@ object SchemaRenderer { case EnumExtension(nme, values0, dirs0) => val dirs = renderDirectives(dirs0) val values = - if(values0.isEmpty) "" + if (values0.isEmpty) "" else s"""| { | ${values0.map(renderEnumValueDefinition).mkString("\n ")} @@ -2134,7 +2426,7 @@ object SchemaRenderer { case InputObjectExtension(nme, fields0, dirs0) => val dirs = renderDirectives(dirs0) val fields = - if(fields0.isEmpty) "" + if (fields0.isEmpty) "" else s"""| { | ${fields0.map(renderInputValue).mkString("\n ")} @@ -2201,12 +2493,12 @@ object SchemaRenderer { } def renderDirectiveDefn(directive: DirectiveDef): String = { - val DirectiveDef(nme, desc, args, repeatable, locations) = directive - val rpt = if (repeatable) " repeatable" else "" - if (args.isEmpty) - s"${renderDescription(desc)}directive @$nme$rpt on ${locations.mkString("|")}" - else - s"${renderDescription(desc)}directive @$nme(${args.map(renderInputValue).mkString(", ")})$rpt on ${locations.mkString("|")}" + val DirectiveDef(nme, desc, args, repeatable, locations) = directive + val rpt = if (repeatable) " repeatable" else "" + if (args.isEmpty) + s"${renderDescription(desc)}directive @$nme$rpt on ${locations.mkString("|")}" + else + s"${renderDescription(desc)}directive @$nme(${args.map(renderInputValue).mkString(", ")})$rpt on ${locations.mkString("|")}" } def renderEnumValueDefinition(v: EnumValueDefinition): String = { @@ -2231,9 +2523,9 @@ object SchemaRenderer { case EnumValue(e) => e case ListValue(elems) => elems.map(renderValue).mkString("[", ", ", "]") case ObjectValue(fields) => - fields.map { - case (name, value) => s"$name : ${renderValue(value)}" - }.mkString("{", ", ", "}") + fields + .map { case (name, value) => s"$name : ${renderValue(value)}" } + .mkString("{", ", ", "}") case _ => "null" } } diff --git a/modules/core/src/main/scala/validationfailure.scala b/modules/core/src/main/scala/validationfailure.scala index 86413774..f8cc5ab2 100644 --- a/modules/core/src/main/scala/validationfailure.scala +++ b/modules/core/src/main/scala/validationfailure.scala @@ -22,26 +22,25 @@ import cats._ import cats.data.NonEmptyList import cats.implicits._ -import ValidationFailure.Severity +import grackle.ValidationFailure.Severity abstract class ValidationFailure(val severity: Severity) extends AnsiColor { protected def formattedMessage: String private val prefix: String = severity match { - case Severity.Error => "🛑 " + case Severity.Error => "🛑 " case Severity.Warning => "⚠️ " - case Severity.Info => "ℹ️ " + case Severity.Info => "ℹ️ " } - private val padding: String = " "*prefix.length + private val padding: String = " " * prefix.length protected def graphql(a: Any) = s"$BLUE$a$RESET" protected def scala(a: Any) = s"$RED$a$RESET" protected def key: String = s"Color Key: ${scala("◼")} Scala | ${graphql("◼")} GraphQL" - final def toErrorMessage: String = s"""|$formattedMessage |$key @@ -64,20 +63,22 @@ abstract class ValidationFailure(val severity: Severity) extends AnsiColor { object ValidationFailure { sealed trait Severity extends Product object Severity { - case object Error extends Severity - case object Warning extends Severity - case object Info extends Severity + case object Error extends Severity + case object Warning extends Severity + case object Info extends Severity implicit val OrderSeverity: Order[Severity] = Order.by { - case Error => 3 + case Error => 3 case Warning => 2 - case Info => 1 + case Info => 1 } } } -final case class ValidationException(failures: NonEmptyList[ValidationFailure]) extends RuntimeException with NoStackTrace { +final case class ValidationException(failures: NonEmptyList[ValidationFailure]) + extends RuntimeException + with NoStackTrace { override def getMessage(): String = s"\n\n${failures.foldMap(_.toErrorMessage)}\n" } diff --git a/modules/core/src/main/scala/valuemapping.scala b/modules/core/src/main/scala/valuemapping.scala index 8832957e..0daf828e 100644 --- a/modules/core/src/main/scala/valuemapping.scala +++ b/modules/core/src/main/scala/valuemapping.scala @@ -22,28 +22,33 @@ import cats.MonadThrow import io.circe.Json import org.tpolecat.sourcepos.SourcePos -import syntax._ -import Cursor.{DeferredCursor} +import grackle.Cursor.DeferredCursor +import grackle.syntax._ -abstract class ValueMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with ValueMappingLike[F] +abstract class ValueMapping[F[_]](implicit val M: MonadThrow[F]) + extends Mapping[F] + with ValueMappingLike[F] trait ValueMappingLike[F[_]] extends Mapping[F] { import typeMappings._ def mkCursor(context: Context, focus: Any, parent: Option[Cursor], env: Env): Cursor = - if(context.tpe.isUnderlyingLeaf) + if (context.tpe.isUnderlyingLeaf) LeafCursor(context, focus, parent, env) else ValueCursor(context, focus, parent, env) def valueCursor[T](path: Path, env: Env, t: T): Cursor = - if(path.path.isEmpty) + if (path.path.isEmpty) mkCursor(Context(path.rootTpe), t, None, env) else { DeferredCursor(path, (context, parent) => mkCursor(context, t, Some(parent), env).success) } - override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] = + override def mkCursorForMappedField( + parent: Cursor, + fieldContext: Context, + fm: FieldMapping): Result[Cursor] = fm match { case ValueField(_, f, _) => val parentFocus: Any = parent match { @@ -63,33 +68,39 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { object ValueFieldMapping { implicit def wrap[T](fm: FieldMapping): ValueFieldMapping[T] = Wrap(fm) - case class Wrap[T](fm: FieldMapping)(implicit val pos: SourcePos) extends ValueFieldMapping[T] { + case class Wrap[T](fm: FieldMapping)(implicit val pos: SourcePos) + extends ValueFieldMapping[T] { def fieldName = fm.fieldName def hidden = fm.hidden def subtree = fm.subtree def unwrap = fm } } - case class ValueField[T](fieldName: String, f: T => Any, hidden: Boolean = false)(implicit val pos: SourcePos) extends ValueFieldMapping[T] { + case class ValueField[T](fieldName: String, f: T => Any, hidden: Boolean = false)( + implicit val pos: SourcePos) + extends ValueFieldMapping[T] { def subtree: Boolean = false def unwrap: FieldMapping = this } object ValueField { - def fromValue[T](fieldName: String, t: T, hidden: Boolean = false)(implicit pos: SourcePos): ValueField[Unit] = + def fromValue[T](fieldName: String, t: T, hidden: Boolean = false)( + implicit pos: SourcePos): ValueField[Unit] = new ValueField[Unit](fieldName, _ => t, hidden) } object ValueObjectMapping { case class DefaultValueObjectMapping( - predicate: MappingPredicate, - fieldMappings: Seq[FieldMapping], - classTag: ClassTag[_] - )(implicit val pos: SourcePos) extends ObjectMapping { + predicate: MappingPredicate, + fieldMappings: Seq[FieldMapping], + classTag: ClassTag[_] + )(implicit val pos: SourcePos) + extends ObjectMapping { override def showMappingType: String = "ValueObjectMapping" } class Builder(predicate: MappingPredicate, pos: SourcePos) { - def on[T](fieldMappings: ValueFieldMapping[T]*)(implicit classTag: ClassTag[T]): ObjectMapping = + def on[T](fieldMappings: ValueFieldMapping[T]*)( + implicit classTag: ClassTag[T]): ObjectMapping = DefaultValueObjectMapping(predicate, fieldMappings.map(_.unwrap), classTag)(pos) } @@ -103,17 +114,23 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { new Builder(MappingPredicate.PathMatch(path), pos) def apply[T]( - tpe: NamedType, - fieldMappings: List[ValueFieldMapping[T]] + tpe: NamedType, + fieldMappings: List[ValueFieldMapping[T]] )(implicit classTag: ClassTag[T], pos: SourcePos): ObjectMapping = - DefaultValueObjectMapping(MappingPredicate.TypeMatch(tpe), fieldMappings.map(_.unwrap), classTag) + DefaultValueObjectMapping( + MappingPredicate.TypeMatch(tpe), + fieldMappings.map(_.unwrap), + classTag) - def unapply(om: DefaultValueObjectMapping): Option[(MappingPredicate, Seq[FieldMapping], ClassTag[_])] = { + def unapply(om: DefaultValueObjectMapping) + : Option[(MappingPredicate, Seq[FieldMapping], ClassTag[_])] = { Some((om.predicate, om.fieldMappings, om.classTag)) } } - override protected def unpackPrefixedMapping(prefix: List[String], om: ObjectMapping): ObjectMapping = + override protected def unpackPrefixedMapping( + prefix: List[String], + om: ObjectMapping): ObjectMapping = om match { case vom: ValueObjectMapping.DefaultValueObjectMapping => vom.copy(predicate = MappingPredicate.PrefixedTypeMatch(prefix, om.predicate.tpe)) @@ -121,10 +138,10 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { } case class ValueCursor( - context: Context, - focus: Any, - parent: Option[Cursor], - env: Env + context: Context, + focus: Any, + parent: Option[Cursor], + env: Env ) extends Cursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) @@ -150,7 +167,8 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { } def asList[C](factory: Factory[Cursor, C]): Result[C] = (tpe, focus) match { - case (ListType(tpe), it: List[_]) => it.view.map(f => mkChild(context.asType(tpe), f)).to(factory).success + case (ListType(tpe), it: List[_]) => + it.view.map(f => mkChild(context.asType(tpe), f)).to(factory).success case _ => Result.internalError(s"Expected List type, found $tpe") } @@ -165,7 +183,8 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { } def asNullable: Result[Option[Cursor]] = (tpe, focus) match { - case (NullableType(tpe), o: Option[_]) => o.map(f => mkChild(context.asType(tpe), f)).success + case (NullableType(tpe), o: Option[_]) => + o.map(f => mkChild(context.asType(tpe), f)).success case (_: NullableType, _) => Result.internalError(s"Found non-nullable $focus for $tpe") case _ => Result.internalError(s"Expected Nullable type, found $focus for $tpe") } @@ -183,13 +202,13 @@ trait ValueMappingLike[F[_]] extends Mapping[F] { case _ => false }).success - def narrow(subtpe: TypeRef): Result[Cursor] = narrowsTo(subtpe).flatMap { n => - if(n) + if (n) mkChild(context.asType(subtpe)).success else - Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") + Result.internalError( + s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") } def field(fieldName: String, resultName: Option[String]): Result[Cursor] = diff --git a/modules/core/src/test/scala/GraphQLResponseTests.scala b/modules/core/src/test/scala/GraphQLResponseTests.scala index 6da661d5..dc311e49 100644 --- a/modules/core/src/test/scala/GraphQLResponseTests.scala +++ b/modules/core/src/test/scala/GraphQLResponseTests.scala @@ -23,7 +23,10 @@ object GraphQLResponseTests { def assertWeaklyEqual(x: Json, y: Json, strictPaths: List[List[String]] = Nil): Unit = assert(weaklyEqual(clue(x), clue(y), strictPaths)) - def assertWeaklyEqualIO(x: IO[Json], y: Json, strictPaths: List[List[String]] = Nil): IO[Unit] = + def assertWeaklyEqualIO( + x: IO[Json], + y: Json, + strictPaths: List[List[String]] = Nil): IO[Unit] = x.map(x0 => assertWeaklyEqual(x0, y, strictPaths)) def assertNoErrors(x: Json): Unit = @@ -35,21 +38,25 @@ object GraphQLResponseTests { def hasField(x: Json, fieldName: String): Boolean = (for { x0 <- x.asObject - _ <- x0(fieldName) + _ <- x0(fieldName) } yield true).getOrElse(false) def errorsOf(x: Json): Seq[Json] = (for { - x0 <- x.asObject + x0 <- x.asObject errorsField <- x0("errors") - errors <- errorsField.asArray + errors <- errorsField.asArray } yield errors).getOrElse(Nil) def noErrors(x: Json): Boolean = hasField(x, "data") && !hasField(x, "errors") || errorsOf(x).isEmpty def weaklyEqual(x: Json, y: Json, strictPaths: List[List[String]] = Nil): Boolean = { - def cmpObject(x: JsonObject, y: JsonObject, strictPaths: List[List[String]], path: List[String]): Boolean = { + def cmpObject( + x: JsonObject, + y: JsonObject, + strictPaths: List[List[String]], + path: List[String]): Boolean = { val xkeys = x.keys val ykeys = y.keys xkeys.sizeCompare(ykeys) == 0 && { @@ -66,7 +73,11 @@ object GraphQLResponseTests { } } - def cmpArray(xs: Vector[Json], ys: Vector[Json], strictPaths: List[List[String]], path: List[String]): Boolean = { + def cmpArray( + xs: Vector[Json], + ys: Vector[Json], + strictPaths: List[List[String]], + path: List[String]): Boolean = { xs.sizeCompare(ys) == 0 && { if (strictPaths.contains(path)) xs.zip(ys).forall { case (x, y) => cmpJson(x, y, strictPaths, path) } @@ -88,13 +99,17 @@ object GraphQLResponseTests { } } - def cmpJson(x: Json, y: Json, strictPaths: List[List[String]], path: List[String]): Boolean = { + def cmpJson( + x: Json, + y: Json, + strictPaths: List[List[String]], + path: List[String]): Boolean = { if (x.isObject && y.isObject) (for { x0 <- x.asObject y0 <- y.asObject } yield cmpObject(x0, y0, strictPaths, path)).getOrElse(false) - else if(x.isArray && y.isArray) + else if (x.isArray && y.isArray) (for { x0 <- x.asArray y0 <- y.asArray @@ -105,4 +120,3 @@ object GraphQLResponseTests { cmpJson(x, y, strictPaths, Nil) } } - diff --git a/modules/core/src/test/scala/arb/AstArb.scala b/modules/core/src/test/scala/arb/AstArb.scala index 19fa1bb2..6289f8ad 100644 --- a/modules/core/src/test/scala/arb/AstArb.scala +++ b/modules/core/src/test/scala/arb/AstArb.scala @@ -15,20 +15,21 @@ package arb +import org.scalacheck.{Arbitrary, Gen} + import grackle._ -import org.scalacheck.{ Arbitrary, Gen } trait AstArb { import Arbitrary.arbitrary - import Gen._ import Ast._ + import Gen._ def shortListOf[A](gen: Gen[A]): Gen[List[A]] = choose(0, 5).flatMap(listOfN(_, gen)) implicit lazy val arbName: Arbitrary[Name] = Arbitrary { - val initial = ('A' to 'Z').toList ++ ('a' to 'z').toList :+ '_' + val initial = ('A' to 'Z').toList ++ ('a' to 'z').toList :+ '_' val subsequent = initial ++ ('0' to '9').toList for { h <- oneOf(initial) @@ -47,7 +48,7 @@ trait AstArb { oneOf("String", "Float").map(s => Type.Named(Name(s))) } - implicit val arbTypeList: Arbitrary[Type.List] = + implicit val arbTypeList: Arbitrary[Type.List] = Arbitrary { arbitrary[Type].map(Type.List.apply) } @@ -70,7 +71,7 @@ trait AstArb { arbitrary[Double].map(Value.FloatValue.apply), alphaStr.map(Value.StringValue.apply), arbitrary[Boolean].map(Value.BooleanValue.apply), - Gen.const(Value.NullValue), + Gen.const(Value.NullValue) // TODO: enum // TODO: list // TODO: object @@ -88,12 +89,15 @@ trait AstArb { def genValueForType(t: Type): Gen[Value] = t match { - case Type.Named(Name("String")) => oneOf(alphaStr.map(Value.StringValue.apply), const(Value.NullValue)) - case Type.Named(Name("Float")) => oneOf(arbitrary[Double].map(Value.FloatValue.apply), const(Value.NullValue)) + case Type.Named(Name("String")) => + oneOf(alphaStr.map(Value.StringValue.apply), const(Value.NullValue)) + case Type.Named(Name("Float")) => + oneOf(arbitrary[Double].map(Value.FloatValue.apply), const(Value.NullValue)) // no other named types are known yet case Type.Named(_) => fail[Value] - case Type.NonNull(t) => genValueForType(t.merge).flatMap { case Value.NullValue => fail ; case v => const(v) } + case Type.NonNull(t) => + genValueForType(t.merge).flatMap { case Value.NullValue => fail; case v => const(v) } case Type.List(t) => shortListOf(genValueForType(t)).map(vs => Value.ListValue(vs)) @@ -102,17 +106,17 @@ trait AstArb { implicit lazy val arbVariableDefinition: Arbitrary[VariableDefinition] = Arbitrary { for { - variable <- arbitrary[Name] - tpe <- arbitrary[Type] + variable <- arbitrary[Name] + tpe <- arbitrary[Type] defaultValue <- option(genValueForType(tpe)) - directives <- shortListOf(arbitrary[Directive]) + directives <- shortListOf(arbitrary[Directive]) } yield VariableDefinition(variable, tpe, defaultValue, directives) } implicit lazy val arbSelectionFragmentSpread: Arbitrary[Selection.FragmentSpread] = Arbitrary { for { - name <- arbitrary[Name] + name <- arbitrary[Name] directives <- shortListOf(arbitrary[Directive]) } yield Selection.FragmentSpread(name, directives) } @@ -123,5 +127,4 @@ trait AstArb { // TODO: field, inlinefragment } - } diff --git a/modules/core/src/test/scala/compiler/CascadeSuite.scala b/modules/core/src/test/scala/compiler/CascadeSuite.scala index ba155439..db01ae78 100644 --- a/modules/core/src/test/scala/compiler/CascadeSuite.scala +++ b/modules/core/src/test/scala/compiler/CascadeSuite.scala @@ -15,17 +15,17 @@ package compiler -import PartialFunction.condOpt +import scala.PartialFunction.condOpt import cats.effect.IO import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import QueryCompiler._ -import Value._ final class CascadeSuite extends CatsEffectSuite { test("elaboration of simple query") { @@ -44,9 +44,13 @@ final class CascadeSuite extends CatsEffectSuite { val expected = Environment( - Env.NonEmptyEnv(Map("filter" -> CascadeMapping.CascadedFilter(Some("foo"), None, Some(23), Some(10)))), - Select("foo", - Select("cascaded", + Env.NonEmptyEnv( + Map( + "filter" -> CascadeMapping.CascadedFilter(Some("foo"), None, Some(23), Some(10)))), + Select( + "foo", + Select( + "cascaded", Group( List( Select("foo"), @@ -95,7 +99,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -136,7 +140,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -188,7 +192,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -284,7 +288,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -380,7 +384,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -502,7 +506,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -585,7 +589,7 @@ final class CascadeSuite extends CatsEffectSuite { val res = CascadeMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } } @@ -631,38 +635,34 @@ object CascadeMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("foo", _ => Some(())) - ) + fieldMappings = List( + ValueField("foo", _ => Some(())) + ) ), ValueObjectMapping[Unit]( tpe = FooType, - fieldMappings = - List( - ValueField("cascaded", identity), - ValueField("reset", identity), - ValueField("bar", _ => Some(())) - ) + fieldMappings = List( + ValueField("cascaded", identity), + ValueField("reset", identity), + ValueField("bar", _ => Some(())) + ) ), ValueObjectMapping[Unit]( tpe = BarType, - fieldMappings = - List( - ValueField("cascaded", identity), - ValueField("reset", identity), - ValueField("foo", _ => Some(())) - ) + fieldMappings = List( + ValueField("cascaded", identity), + ValueField("reset", identity), + ValueField("foo", _ => Some(())) + ) ), ValueObjectMapping[CascadedFilter]( tpe = FilterValueType, - fieldMappings = - List( - CursorField("foo", getFilterValue(_).map(_.foo)), - CursorField("bar", getFilterValue(_).map(_.bar)), - CursorField("fooBar", getFilterValue(_).map(_.fooBar)), - CursorField("limit", getFilterValue(_).map(_.limit)) - ) + fieldMappings = List( + CursorField("foo", getFilterValue(_).map(_.foo)), + CursorField("bar", getFilterValue(_).map(_.bar)), + CursorField("fooBar", getFilterValue(_).map(_.fooBar)), + CursorField("limit", getFilterValue(_).map(_.limit)) + ) ) ) @@ -689,7 +689,11 @@ object CascadeMapping extends ValueMapping[IO] { object TriInt extends TriValue[Int](condOpt(_) { case IntValue(i) => i }) object TriBoolean extends TriValue[Boolean](condOpt(_) { case BooleanValue(b) => b }) - case class CascadedFilter(foo: Option[String], bar: Option[Boolean], fooBar: Option[Int], limit: Option[Int]) { + case class CascadedFilter( + foo: Option[String], + bar: Option[Boolean], + fooBar: Option[Int], + limit: Option[Int]) { def combine[T](current: Option[T], next: Tri[T]): Option[T] = next match { case Left(()) => current @@ -743,18 +747,24 @@ object CascadeMapping extends ValueMapping[IO] { } override val selectElaborator = SelectElaborator { - case (QueryType | BarType, "foo", List(Binding("filter", FooFilter(filter)), Binding("limit", TriInt(limit)))) => + case ( + QueryType | BarType, + "foo", + List(Binding("filter", FooFilter(filter)), Binding("limit", TriInt(limit)))) => for { current0 <- Elab.env[CascadedFilter]("filter") - current = current0.getOrElse(CascadedFilter.empty) - _ <- Elab.env("filter", filter(current).withLimit(limit)) + current = current0.getOrElse(CascadedFilter.empty) + _ <- Elab.env("filter", filter(current).withLimit(limit)) } yield () - case (FooType, "bar", List(Binding("filter", BarFilter(filter)), Binding("limit", TriInt(limit)))) => + case ( + FooType, + "bar", + List(Binding("filter", BarFilter(filter)), Binding("limit", TriInt(limit)))) => for { current0 <- Elab.env[CascadedFilter]("filter") - current = current0.getOrElse(CascadedFilter.empty) - _ <- Elab.env("filter", filter(current).withLimit(limit)) + current = current0.getOrElse(CascadedFilter.empty) + _ <- Elab.env("filter", filter(current).withLimit(limit)) } yield () case (FooType | BarType, "reset", Nil) => diff --git a/modules/core/src/test/scala/compiler/CompilerSuite.scala b/modules/core/src/test/scala/compiler/CompilerSuite.scala index 000ee750..b3c4c732 100644 --- a/modules/core/src/test/scala/compiler/CompilerSuite.scala +++ b/modules/core/src/test/scala/compiler/CompilerSuite.scala @@ -21,13 +21,17 @@ import cats.implicits._ import munit.CatsEffectSuite import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.QueryCompiler.ComponentElaborator.TrivialJoin +import grackle.UntypedOperation._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._, UntypedOperation._ -import QueryCompiler._, ComponentElaborator.TrivialJoin final class CompilerSuite extends CatsEffectSuite { - val queryParser = QueryParser(GraphQLParser(GraphQLParser.defaultConfig.copy(terseError = false))) + val queryParser = QueryParser( + GraphQLParser(GraphQLParser.defaultConfig.copy(terseError = false))) test("simple query") { val query = """ @@ -39,7 +43,11 @@ final class CompilerSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("character", None, List(Binding("id", StringValue("1000"))), Nil, + UntypedSelect( + "character", + None, + List(Binding("id", StringValue("1000"))), + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ) @@ -59,8 +67,16 @@ final class CompilerSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("update_character", None, List(Binding("id", StringValue("1000")), Binding("name", StringValue("Luke"))), Nil, - UntypedSelect("character", None, Nil, Nil, + UntypedSelect( + "update_character", + None, + List(Binding("id", StringValue("1000")), Binding("name", StringValue("Luke"))), + Nil, + UntypedSelect( + "character", + None, + Nil, + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ) ) @@ -79,7 +95,11 @@ final class CompilerSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("character", None, List(Binding("id", StringValue("1000"))), Nil, + UntypedSelect( + "character", + None, + List(Binding("id", StringValue("1000"))), + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ) @@ -101,10 +121,16 @@ final class CompilerSuite extends CatsEffectSuite { val expected = UntypedSelect( - "character", None, List(Binding("id", StringValue("1000"))), Nil, + "character", + None, + List(Binding("id", StringValue("1000"))), + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ~ UntypedSelect( - "friends", None, Nil, Nil, + "friends", + None, + Nil, + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ) ) @@ -130,14 +156,25 @@ final class CompilerSuite extends CatsEffectSuite { val expected = UntypedSelect( - "hero", None, List(Binding("episode", EnumValue("NEWHOPE"))), Nil, + "hero", + None, + List(Binding("episode", EnumValue("NEWHOPE"))), + Nil, UntypedSelect("name", None, Nil, Nil, Empty) ~ - UntypedSelect("friends", None, Nil, Nil, - UntypedSelect("name", None, Nil, Nil, Empty) ~ - UntypedSelect("friends", None, Nil, Nil, - UntypedSelect("name", None, Nil, Nil, Empty) + UntypedSelect( + "friends", + None, + Nil, + Nil, + UntypedSelect("name", None, Nil, Nil, Empty) ~ + UntypedSelect( + "friends", + None, + Nil, + Nil, + UntypedSelect("name", None, Nil, Nil, Empty) + ) ) - ) ) val res = queryParser.parseText(query).map(_._1) @@ -157,13 +194,28 @@ final class CompilerSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("user", None, List(Binding("id", IntValue(4))), Nil, - Group(List( - UntypedSelect("id", None, Nil, Nil, Empty), - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("profilePic", Some("smallPic"), List(Binding("size", IntValue(64))), Nil, Empty), - UntypedSelect("profilePic", Some("bigPic"), List(Binding("size", IntValue(1024))), Nil, Empty) - )) + UntypedSelect( + "user", + None, + List(Binding("id", IntValue(4))), + Nil, + Group( + List( + UntypedSelect("id", None, Nil, Nil, Empty), + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect( + "profilePic", + Some("smallPic"), + List(Binding("size", IntValue(64))), + Nil, + Empty), + UntypedSelect( + "profilePic", + Some("bigPic"), + List(Binding("size", IntValue(1024))), + Nil, + Empty) + )) ) val res = queryParser.parseText(query).map(_._1) @@ -189,14 +241,34 @@ final class CompilerSuite extends CatsEffectSuite { val expected = UntypedSelect( - "__schema", None, Nil, Nil, - UntypedSelect("queryType", None, Nil, Nil, UntypedSelect("name", None, Nil, Nil, Empty)) ~ - UntypedSelect("mutationType", None, Nil, Nil, UntypedSelect("name", None, Nil, Nil, Empty)) ~ - UntypedSelect("subscriptionType", None, Nil, Nil, UntypedSelect("name", None, Nil, Nil, Empty)) + "__schema", + None, + Nil, + Nil, + UntypedSelect( + "queryType", + None, + Nil, + Nil, + UntypedSelect("name", None, Nil, Nil, Empty)) ~ + UntypedSelect( + "mutationType", + None, + Nil, + Nil, + UntypedSelect("name", None, Nil, Nil, Empty)) ~ + UntypedSelect( + "subscriptionType", + None, + Nil, + Nil, + UntypedSelect("name", None, Nil, Nil, Empty)) ) val res = queryParser.parseText(query).map(_._1) - assertEquals(res, Result.Success(List(UntypedQuery(Some("IntrospectionQuery"), expected, Nil, Nil)))) + assertEquals( + res, + Result.Success(List(UntypedQuery(Some("IntrospectionQuery"), expected, Nil, Nil)))) } test("simple selector elaborated query") { @@ -213,17 +285,19 @@ final class CompilerSuite extends CatsEffectSuite { val expected = Select( - "character", None, + "character", + None, Unique( - Filter(Eql(AtomicMapping.CharacterType / "id", Const("1000")), - Select("name") ~ + Filter( + Eql(AtomicMapping.CharacterType / "id", Const("1000")), + Select("name") ~ Select( "friends", Select("name") ) ) ) - ) + ) val res = AtomicMapping.compiler.compile(query) @@ -237,7 +311,8 @@ final class CompilerSuite extends CatsEffectSuite { } """ - val expected = Problem("Non-leaf field 'character' of Query must have a non-empty subselection set") + val expected = + Problem("Non-leaf field 'character' of Query must have a non-empty subselection set") val res = AtomicMapping.compiler.compile(query) @@ -332,23 +407,34 @@ final class CompilerSuite extends CatsEffectSuite { """ val expected = - Component(ComponentA, TrivialJoin, - Select("componenta", + Component( + ComponentA, + TrivialJoin, + Select( + "componenta", Select("fielda1") ~ - Select("fielda2", - Component(ComponentB, TrivialJoin, - Select("componentb", - Select("fieldb1") ~ - Select("fieldb2", - Component(ComponentC, TrivialJoin, - Select("componentc", - Select("fieldc1") + Select( + "fielda2", + Component( + ComponentB, + TrivialJoin, + Select( + "componentb", + Select("fieldb1") ~ + Select( + "fieldb2", + Component( + ComponentC, + TrivialJoin, + Select( + "componentc", + Select("fieldc1") + ) + ) ) - ) ) ) ) - ) ) ) @@ -404,7 +490,7 @@ final class CompilerSuite extends CatsEffectSuite { |* must be char: '}'""".stripMargin val res = queryParser.parseText(query) - //println(res.toProblems.toList.head.message) + // println(res.toProblems.toList.head.message) assertEquals(res, Result.failure(expected)) } @@ -534,24 +620,21 @@ object TestComposedMapping extends ComposedMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - Delegate("componenta", ComponentA) - ) + fieldMappings = List( + Delegate("componenta", ComponentA) + ) ), ObjectMapping( tpe = FieldA2Type, - fieldMappings = - List( - Delegate("componentb", ComponentB) - ) + fieldMappings = List( + Delegate("componentb", ComponentB) + ) ), ObjectMapping( tpe = FieldB2Type, - fieldMappings = - List( - Delegate("componentc", ComponentC) - ) + fieldMappings = List( + Delegate("componentc", ComponentC) + ) ) ) } diff --git a/modules/core/src/test/scala/compiler/DirectivesSuite.scala b/modules/core/src/test/scala/compiler/DirectivesSuite.scala index c7007a88..486a31a5 100644 --- a/modules/core/src/test/scala/compiler/DirectivesSuite.scala +++ b/modules/core/src/test/scala/compiler/DirectivesSuite.scala @@ -18,16 +18,22 @@ package compiler import munit.CatsEffectSuite import grackle._ +import grackle.Ast.DirectiveLocation._ +import grackle.Query._ import grackle.syntax._ -import Ast.DirectiveLocation._ -import Query._ final class DirectivesSuite extends CatsEffectSuite { val schemaParser = SchemaParser(GraphQLParser(GraphQLParser.defaultConfig)) def testDirectiveDefs(s: Schema): List[DirectiveDef] = s.directives.filter { - case DirectiveDef("skip"|"include"|"deprecated"|"specifiedBy"|"oneOf", _, _, _, _) => false + case DirectiveDef( + "skip" | "include" | "deprecated" | "specifiedBy" | "oneOf", + _, + _, + _, + _) => + false case _ => true } @@ -61,7 +67,8 @@ final class DirectivesSuite extends CatsEffectSuite { } test("Directive definition with multiple locations (1)") { - val expected = DirectiveDef("foo", None, Nil, false, List(FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT)) + val expected = + DirectiveDef("foo", None, Nil, false, List(FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT)) val schema = Schema(""" type Query { @@ -75,7 +82,8 @@ final class DirectivesSuite extends CatsEffectSuite { } test("Directive definition with multiple locations (2)") { - val expected = DirectiveDef("foo", None, Nil, false, List(FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT)) + val expected = + DirectiveDef("foo", None, Nil, false, List(FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT)) val schema = Schema(""" type Query { @@ -160,7 +168,10 @@ final class DirectivesSuite extends CatsEffectSuite { None, ScalarType.StringType, None, - List(Directive("deprecated", List(Binding("reason", Value.StringValue("Use arg1 instead"))))) + List( + Directive( + "deprecated", + List(Binding("reason", Value.StringValue("Use arg1 instead"))))) ), InputValue("arg1", None, ScalarType.StringType, None, Nil) ), @@ -185,25 +196,25 @@ final class DirectivesSuite extends CatsEffectSuite { test("Schema with directives") { val schema = - """|schema @foo { - | query: Query - |} - |scalar Scalar @foo - |interface Interface @foo { - | field(e: Enum, i: Input): Int @foo - |} - |type Object implements Interface @foo { - | field(e: Enum, i: Input): Int @foo - |} - |union Union @foo = Object - |enum Enum @foo { - | VALUE @foo - |} - |input Input @foo { - | field: Int @foo - |} - |directive @foo on SCHEMA|SCALAR|OBJECT|FIELD_DEFINITION|ARGUMENT_DEFINITION|INTERFACE|UNION|ENUM|ENUM_VALUE|INPUT_OBJECT|INPUT_FIELD_DEFINITION - |""".stripMargin + """|schema @foo { + | query: Query + |} + |scalar Scalar @foo + |interface Interface @foo { + | field(e: Enum, i: Input): Int @foo + |} + |type Object implements Interface @foo { + | field(e: Enum, i: Input): Int @foo + |} + |union Union @foo = Object + |enum Enum @foo { + | VALUE @foo + |} + |input Input @foo { + | field: Int @foo + |} + |directive @foo on SCHEMA|SCALAR|OBJECT|FIELD_DEFINITION|ARGUMENT_DEFINITION|INTERFACE|UNION|ENUM|ENUM_VALUE|INPUT_OBJECT|INPUT_FIELD_DEFINITION + |""".stripMargin val res = schemaParser.parseText(schema) val ser = res.map(_.toString) @@ -214,7 +225,11 @@ final class DirectivesSuite extends CatsEffectSuite { test("Query with directive") { val expected = Operation( - UntypedSelect("foo", None, Nil, List(Directive("dir", Nil)), + UntypedSelect( + "foo", + None, + Nil, + List(Directive("dir", Nil)), UntypedSelect("id", None, Nil, List(Directive("dir", Nil)), Empty) ), DirectiveMapping.QueryType, @@ -237,7 +252,11 @@ final class DirectivesSuite extends CatsEffectSuite { test("Mutation with directive") { val expected = Operation( - UntypedSelect("foo", None, Nil, List(Directive("dir", Nil)), + UntypedSelect( + "foo", + None, + Nil, + List(Directive("dir", Nil)), UntypedSelect("id", None, Nil, List(Directive("dir", Nil)), Empty) ), DirectiveMapping.MutationType, @@ -260,7 +279,11 @@ final class DirectivesSuite extends CatsEffectSuite { test("Subscription with directive") { val expected = Operation( - UntypedSelect("foo", None, Nil, List(Directive("dir", Nil)), + UntypedSelect( + "foo", + None, + Nil, + List(Directive("dir", Nil)), UntypedSelect("id", None, Nil, List(Directive("dir", Nil)), Empty) ), DirectiveMapping.SubscriptionType, @@ -280,11 +303,18 @@ final class DirectivesSuite extends CatsEffectSuite { assertEquals(res, expected.success) } - test("Fragment with directive") { // TOD: will need new elaborator to expose fragment directives + test( + "Fragment with directive" + ) { // TOD: will need new elaborator to expose fragment directives val expected = Operation( - UntypedSelect("foo", None, Nil, Nil, - Narrow(DirectiveMapping.BazType, + UntypedSelect( + "foo", + None, + Nil, + Nil, + Narrow( + DirectiveMapping.BazType, UntypedSelect("baz", None, Nil, List(Directive("dir", Nil)), Empty) ) ), @@ -308,11 +338,18 @@ final class DirectivesSuite extends CatsEffectSuite { assertEquals(res, expected.success) } - test("Inline fragment with directive") { // TOD: will need new elaborator to expose fragment directives + test( + "Inline fragment with directive" + ) { // TOD: will need new elaborator to expose fragment directives val expected = Operation( - UntypedSelect("foo", None, Nil, Nil, - Narrow(DirectiveMapping.BazType, + UntypedSelect( + "foo", + None, + Nil, + Nil, + Narrow( + DirectiveMapping.BazType, UntypedSelect("baz", None, Nil, List(Directive("dir", Nil)), Empty) ) ), diff --git a/modules/core/src/test/scala/compiler/EnvironmentSuite.scala b/modules/core/src/test/scala/compiler/EnvironmentSuite.scala index cb9eaed9..d7d1d795 100644 --- a/modules/core/src/test/scala/compiler/EnvironmentSuite.scala +++ b/modules/core/src/test/scala/compiler/EnvironmentSuite.scala @@ -20,10 +20,10 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Value._ -import QueryCompiler._ object EnvironmentMapping extends ValueMapping[IO] { val schema = @@ -51,28 +51,25 @@ object EnvironmentMapping extends ValueMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - ValueField[Unit]("nested", _ => ()), - ValueField[Unit]("nestedSum", _ => ()) - ) + fieldMappings = List( + ValueField[Unit]("nested", _ => ()), + ValueField[Unit]("nestedSum", _ => ()) + ) ), ObjectMapping( tpe = NestedType, - fieldMappings = - List( - CursorField("sum", sum), - CursorField("url", url), - ValueField[Unit]("nested", _ => ()) - ) + fieldMappings = List( + CursorField("sum", sum), + CursorField("url", url), + ValueField[Unit]("nested", _ => ()) + ) ), ObjectMapping( tpe = NestedSumType, - fieldMappings = - List( - CursorField("sum", sum), - ValueField[Unit]("nestedSum", _ => ()) - ) + fieldMappings = List( + CursorField("sum", sum), + ValueField[Unit]("nestedSum", _ => ()) + ) ) ) @@ -80,12 +77,12 @@ object EnvironmentMapping extends ValueMapping[IO] { (for { x <- c.env[Int]("x") y <- c.env[Int]("y") - } yield x+y).toResult(s"Missing argument") + } yield x + y).toResult(s"Missing argument") def url(c: Cursor): Result[String] = { - c.env[Boolean]("secure").map(secure => - if(secure) "https://localhost/" else "http://localhost/" - ).toResult(s"Missing argument") + c.env[Boolean]("secure") + .map(secure => if (secure) "https://localhost/" else "http://localhost/") + .toResult(s"Missing argument") } override val selectElaborator = SelectElaborator { @@ -93,7 +90,10 @@ object EnvironmentMapping extends ValueMapping[IO] { Elab.env("x" -> x, "y" -> y) case (NestedType, "sum", List(Binding("x", IntValue(x)), Binding("y", IntValue(y)))) => Elab.env("x" -> x, "y" -> y) - case (NestedSumType, "nestedSum", List(Binding("x", IntValue(x)), Binding("y", IntValue(y)))) => + case ( + NestedSumType, + "nestedSum", + List(Binding("x", IntValue(x)), Binding("y", IntValue(y)))) => Elab.env("x" -> x, "y" -> y) } } diff --git a/modules/core/src/test/scala/compiler/FieldMergeSuite.scala b/modules/core/src/test/scala/compiler/FieldMergeSuite.scala index 66f833c6..c5823c94 100644 --- a/modules/core/src/test/scala/compiler/FieldMergeSuite.scala +++ b/modules/core/src/test/scala/compiler/FieldMergeSuite.scala @@ -22,16 +22,22 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.PathTerm.UniquePath +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import PathTerm.UniquePath -import Query._ -import Predicate._, Value._ -import QueryCompiler._ final class FieldMergeSuite extends CatsEffectSuite { def runOperation(op: Result[Operation]): IO[List[Json]] = { val op0 = op.toOption.get - FieldMergeMapping.interpreter.run(op0.query, op0.rootTpe, Env.empty).evalMap(FieldMergeMapping.mkResponse).compile.toList + FieldMergeMapping + .interpreter + .run(op0.query, op0.rootTpe, Env.empty) + .evalMap(FieldMergeMapping.mkResponse) + .compile + .toList } test("merge fields") { @@ -76,31 +82,41 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty), - Select("id", None, Empty), - Select("name", Some("foo"), Empty), - Select("name", Some("bar"), Empty), - Select("friends", None, - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty), - Select("id", None, Empty) - )) - ), - Select("friends", Some("baz"), - Group(List( - Select("name", None, Empty), - Select("name", Some("quux"), Empty), - Select("profilePic", None, Empty), - Select("id", None, Empty) - )) - ) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty), + Select("id", None, Empty), + Select("name", Some("foo"), Empty), + Select("name", Some("bar"), Empty), + Select( + "friends", + None, + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty), + Select("id", None, Empty) + )) + ), + Select( + "friends", + Some("baz"), + Group( + List( + Select("name", None, Empty), + Select("name", Some("quux"), Empty), + Select("profilePic", None, Empty), + Select("id", None, Empty) + )) + ) + )) ) ) ) @@ -167,13 +183,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", Some("profilePic"), Empty), - Select("profilePic", Some("name"), Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", Some("profilePic"), Empty), + Select("profilePic", Some("name"), Empty) + )) ) ) ) @@ -247,13 +267,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -311,22 +335,29 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Group(List( - Select("user", None, - Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("name", None, Empty) + Group( + List( + Select( + "user", + None, + Unique( + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select("name", None, Empty) + ) ) - ) - ), - Select("user", Some("foo"), - Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("profilePic", None, Empty) + ), + Select( + "user", + Some("foo"), + Unique( + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select("profilePic", None, Empty) + ) ) ) - ) - )) + )) val expectedResult = json""" { @@ -363,22 +394,29 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Group(List( - Select("user", None, - Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("name", None, Empty) + Group( + List( + Select( + "user", + None, + Unique( + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select("name", None, Empty) + ) ) - ) - ), - Select("user", Some("foo"), - Unique( - Filter(Eql(UniquePath(List("id")), Const("2")), - Select("profilePic", None, Empty) + ), + Select( + "user", + Some("foo"), + Unique( + Filter( + Eql(UniquePath(List("id")), Const("2")), + Select("profilePic", None, Empty) + ) ) ) - ) - )) + )) val expectedResult = json""" { @@ -415,13 +453,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -437,7 +479,8 @@ final class FieldMergeSuite extends CatsEffectSuite { } """ - val compiled = FieldMergeMapping.compiler.compile(query, untypedVars = Some(json"""{ "id": "1" }""")) + val compiled = + FieldMergeMapping.compiler.compile(query, untypedVars = Some(json"""{ "id": "1" }""")) assertEquals(compiled.map(_.query), Result.Success(expected)) @@ -461,7 +504,9 @@ final class FieldMergeSuite extends CatsEffectSuite { val expected = "Cannot merge fields named 'user' with different arguments" - val compiled = FieldMergeMapping.compiler.compile(query, untypedVars = Some(json"""{ "id1": "1", "id2": "2" }""")) + val compiled = FieldMergeMapping + .compiler + .compile(query, untypedVars = Some(json"""{ "id1": "1", "id2": "2" }""")) assertEquals(compiled.map(_.query), Result.failure(expected)) } @@ -481,12 +526,12 @@ final class FieldMergeSuite extends CatsEffectSuite { val expected = "Cannot merge fields named 'user' with different arguments" - val compiled = FieldMergeMapping.compiler.compile(query, untypedVars = Some(json"""{ "id1": "1" }""")) + val compiled = + FieldMergeMapping.compiler.compile(query, untypedVars = Some(json"""{ "id1": "1" }""")) assertEquals(compiled.map(_.query), Result.failure(expected)) } - test("fields with skip (1)") { val query = """ query { @@ -502,10 +547,15 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select( + "friends", + None, Select("name", None, Empty) ) ) @@ -553,10 +603,15 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select( + "friends", + None, Select("name", None, Empty) ) ) @@ -604,10 +659,15 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select( + "friends", + None, Select("name", None, Empty) ) ) @@ -679,14 +739,20 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Select( + "friends", + None, + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -737,15 +803,20 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("favourite", None, - Group(List( - Narrow(FieldMergeMapping.UserType, Select("name", Some("label"), Empty)), - Narrow(FieldMergeMapping.PageType, Select("title", Some("label"), Empty)) - )) + Select( + "favourite", + None, + Group( + List( + Narrow(FieldMergeMapping.UserType, Select("name", Some("label"), Empty)), + Narrow(FieldMergeMapping.PageType, Select("title", Some("label"), Empty)) + )) ) ) ) @@ -817,23 +888,34 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("favourite", None, - Group(List( - Narrow(FieldMergeMapping.UserType, - Select("friends", Some("likers"), - Select("name", None, Empty) - ) - ), - Narrow(FieldMergeMapping.PageType, - Select("likers", None, - Select("name", None, Empty) + Select( + "favourite", + None, + Group( + List( + Narrow( + FieldMergeMapping.UserType, + Select( + "friends", + Some("likers"), + Select("name", None, Empty) + ) + ), + Narrow( + FieldMergeMapping.PageType, + Select( + "likers", + None, + Select("name", None, Empty) + ) ) - ) - )) + )) ) ) ) @@ -961,10 +1043,13 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("profiles", None, + Select( + "profiles", + None, Group( List( - Narrow(FieldMergeMapping.UserType, + Narrow( + FieldMergeMapping.UserType, Select("name", None, Empty) ), Select("id", None, Empty) @@ -1019,7 +1104,9 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), @@ -1058,7 +1145,9 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), @@ -1097,7 +1186,9 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), @@ -1142,13 +1233,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -1190,13 +1285,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Filter( + Eql(UniquePath(List("id")), Const("1")), + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -1242,16 +1341,22 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("favourite", None, - Narrow(FieldMergeMapping.UserType, - Group(List( - Select("name", None, Empty), - Select("profilePic", None, Empty) - )) + Select( + "favourite", + None, + Narrow( + FieldMergeMapping.UserType, + Group( + List( + Select("name", None, Empty), + Select("profilePic", None, Empty) + )) ) ) ) @@ -1337,21 +1442,28 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("favourite", None, - Group(List( - Narrow(FieldMergeMapping.UserType, Select("name", None, Empty)), - Narrow(FieldMergeMapping.ProfileType, Select("id", None, Empty)), - Narrow(FieldMergeMapping.UserType, - Group(List( - Select("id", None, Empty), - Select("name", None, Empty) - )) - ) - )) + Select( + "favourite", + None, + Group( + List( + Narrow(FieldMergeMapping.UserType, Select("name", None, Empty)), + Narrow(FieldMergeMapping.ProfileType, Select("id", None, Empty)), + Narrow( + FieldMergeMapping.UserType, + Group( + List( + Select("id", None, Empty), + Select("name", None, Empty) + )) + ) + )) ) ) ) @@ -1408,20 +1520,27 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("favourite", None, - Group(List( - Narrow(FieldMergeMapping.UserType, - Group(List( - Select("id", None, Empty), - Select("name", None, Empty) - )) - ), - Narrow(FieldMergeMapping.ProfileType, Select("id", None, Empty)) - )) + Select( + "favourite", + None, + Group( + List( + Narrow( + FieldMergeMapping.UserType, + Group( + List( + Select("id", None, Empty), + Select("name", None, Empty) + )) + ), + Narrow(FieldMergeMapping.ProfileType, Select("id", None, Empty)) + )) ) ) ) @@ -1496,21 +1615,31 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, - Group(List( - Select("friends", None, - Select("name", None, Empty) - ), - Narrow(FieldMergeMapping.UserType, - Select("friends", None, - Select("id", None, Empty) + Select( + "friends", + None, + Group( + List( + Select( + "friends", + None, + Select("name", None, Empty) + ), + Narrow( + FieldMergeMapping.UserType, + Select( + "friends", + None, + Select("id", None, Empty) + ) ) - ) - )) + )) ) ) ) @@ -1575,19 +1704,27 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), - Select("friends", None, - Group(List( - Select("id", None, Empty), - Narrow(FieldMergeMapping.UserType, - Select("friends", None, - Select("name", None, Empty) + Select( + "friends", + None, + Group( + List( + Select("id", None, Empty), + Narrow( + FieldMergeMapping.UserType, + Select( + "friends", + None, + Select("name", None, Empty) + ) ) - ) - )) + )) ) ) ) @@ -1676,13 +1813,17 @@ final class FieldMergeSuite extends CatsEffectSuite { """ val expected = - Select("user", None, + Select( + "user", + None, Unique( Filter( Eql(UniquePath(List("id")), Const("1")), Group( List( - Select("friends", None, + Select( + "friends", + None, Group( List( Select("name", None, Empty), @@ -1750,9 +1891,12 @@ final class FieldMergeSuite extends CatsEffectSuite { val expected = Group( List( - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(UniquePath(List("id")), Const("1")), + Filter( + Eql(UniquePath(List("id")), Const("1")), Group( List( Select("name", None, Empty), @@ -1762,7 +1906,9 @@ final class FieldMergeSuite extends CatsEffectSuite { ) ) ), - Select("profiles", None, + Select( + "profiles", + None, Select("id", None, Empty) ) ) @@ -1811,16 +1957,25 @@ object FieldMergeData { def id: String } - case class User(id: String, name: String, age: Int, profilePic: String, friendIds: List[String], favouriteId: Option[String]) extends Profile { + case class User( + id: String, + name: String, + age: Int, + profilePic: String, + friendIds: List[String], + favouriteId: Option[String]) + extends Profile { def friends: List[User] = friendIds.flatMap(fid => profiles.collect { case u: User if u.id == fid => u }) def mutualFriends: List[User] = - friendIds.flatMap(fid => profiles.collect { case u: User if u.id == fid && u.friendIds.contains(id) => u }) + friendIds.flatMap(fid => + profiles.collect { case u: User if u.id == fid && u.friendIds.contains(id) => u }) def favourite: Option[Profile] = favouriteId.flatMap(id => profiles.find(_.id == id)) } - case class Page(id: String, title: String, likerIds: List[String], creatorId: String) extends Profile { + case class Page(id: String, title: String, likerIds: List[String], creatorId: String) + extends Profile { def likers: List[User] = likerIds.flatMap(lid => profiles.collect { case u: User if u.id == lid => u }) def creator: User = @@ -1878,44 +2033,41 @@ object FieldMergeMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("user", _ => profiles.collect { case u: User => u }), - ValueField("profiles", _ => profiles) - ) + fieldMappings = List( + ValueField("user", _ => profiles.collect { case u: User => u }), + ValueField("profiles", _ => profiles) + ) ), ValueObjectMapping[Profile]( tpe = ProfileType, - fieldMappings = - List( - ValueField("id", _.id) - ) + fieldMappings = List( + ValueField("id", _.id) + ) ), ValueObjectMapping[User]( tpe = UserType, - fieldMappings = - List( - ValueField("name", _.name), - ValueField("age", _.age), - ValueField("profilePic", _.profilePic), - ValueField("friends", _.friends), - ValueField("mutualFriends", _.mutualFriends), - ValueField("favourite", _.favourite), - ) + fieldMappings = List( + ValueField("name", _.name), + ValueField("age", _.age), + ValueField("profilePic", _.profilePic), + ValueField("friends", _.friends), + ValueField("mutualFriends", _.mutualFriends), + ValueField("favourite", _.favourite) + ) ), ValueObjectMapping[Page]( tpe = PageType, - fieldMappings = - List( - ValueField("title", _.title), - ValueField("likers", _.likers), - ValueField("creator", _.creator) - ) + fieldMappings = List( + ValueField("title", _.title), + ValueField("likers", _.likers), + ValueField("creator", _.creator) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "user", List(Binding("id", IDValue(id)))) => - Elab.transformChild(child => Unique(Filter(Eql(FieldMergeMapping.UserType / "id", Const(id)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(FieldMergeMapping.UserType / "id", Const(id)), child))) } } diff --git a/modules/core/src/test/scala/compiler/FragmentSuite.scala b/modules/core/src/test/scala/compiler/FragmentSuite.scala index 469bbe53..fa52476b 100644 --- a/modules/core/src/test/scala/compiler/FragmentSuite.scala +++ b/modules/core/src/test/scala/compiler/FragmentSuite.scala @@ -22,15 +22,21 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ final class FragmentSuite extends CatsEffectSuite { def runOperation(op: Result[Operation]): IO[List[Json]] = { val op0 = op.toOption.get - FragmentMapping.interpreter.run(op0.query, op0.rootTpe, Env.empty).evalMap(FragmentMapping.mkResponse).compile.toList + FragmentMapping + .interpreter + .run(op0.query, op0.rootTpe, Env.empty) + .evalMap(FragmentMapping.mkResponse) + .compile + .toList } test("simple fragment query") { @@ -54,28 +60,35 @@ final class FragmentSuite extends CatsEffectSuite { """ val expected = - Select("user", + Select( + "user", Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Group(List( - Select("friends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ), - Select("mutualFriends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ) - )) + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Group( + List( + Select( + "friends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ), + Select( + "mutualFriends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ) + )) ) ) - ) + ) val expectedResult = json""" { @@ -144,25 +157,32 @@ final class FragmentSuite extends CatsEffectSuite { """ val expected = - Select("user", + Select( + "user", Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Group(List( - Select("friends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ), - Select("mutualFriends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ) - )) + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Group( + List( + Select( + "friends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ), + Select( + "mutualFriends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ) + )) ) ) ) @@ -238,25 +258,32 @@ final class FragmentSuite extends CatsEffectSuite { """ val expected = - Select("user", + Select( + "user", Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Group(List( - Select("friends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ), - Select("mutualFriends", - Group(List( - Select("id"), - Select("name"), - Select("profilePic") - )) - ) - )) + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Group( + List( + Select( + "friends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ), + Select( + "mutualFriends", + Group( + List( + Select("id"), + Select("name"), + Select("profilePic") + )) + ) + )) ) ) ) @@ -327,13 +354,15 @@ final class FragmentSuite extends CatsEffectSuite { val Page = FragmentMapping.schema.ref("Page") val expected = - Select("profiles", - Group(List( - Select("id"), - Introspect(FragmentMapping.schema, Select("__typename")), - Narrow(User, Select("name")), - Narrow(Page, Select("title")) - )) + Select( + "profiles", + Group( + List( + Select("id"), + Introspect(FragmentMapping.schema, Select("__typename")), + Narrow(User, Select("name")), + Narrow(Page, Select("title")) + )) ) val expectedResult = json""" @@ -398,12 +427,14 @@ final class FragmentSuite extends CatsEffectSuite { val Page = FragmentMapping.schema.ref("Page") val expected = - Select("profiles", - Group(List( - Select("id"), - Narrow(User, Select("name")), - Narrow(Page, Select("title")) - )) + Select( + "profiles", + Group( + List( + Select("id"), + Narrow(User, Select("name")), + Narrow(Page, Select("title")) + )) ) val expectedResult = json""" @@ -478,58 +509,66 @@ final class FragmentSuite extends CatsEffectSuite { val Page = FragmentMapping.schema.ref("Page") val expected = - Group(List( - Select("user", - Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Select("favourite", - Group(List( - Introspect(FragmentMapping.schema, Select("__typename")), - Narrow( - User, - Group(List( - Select("id"), - Select("name") - )) - ), - Narrow( - Page, - Group(List( - Select("id"), - Select("title") - )) - ) - )) + Group( + List( + Select( + "user", + Unique( + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Select( + "favourite", + Group(List( + Introspect(FragmentMapping.schema, Select("__typename")), + Narrow( + User, + Group(List( + Select("id"), + Select("name") + )) + ), + Narrow( + Page, + Group(List( + Select("id"), + Select("title") + )) + ) + )) + ) ) ) - ) - ), - Select("user", Some("page"), - Unique( - Filter(Eql(FragmentMapping.PageType / "id", Const("2")), - Select("favourite", - Group(List( - Introspect(FragmentMapping.schema, Select("__typename")), - Narrow( - User, - Group(List( - Select("id"), - Select("name") - )) - ), - Narrow( - Page, - Group(List( - Select("id"), - Select("title") - )) - ) - )) + ), + Select( + "user", + Some("page"), + Unique( + Filter( + Eql(FragmentMapping.PageType / "id", Const("2")), + Select( + "favourite", + Group(List( + Introspect(FragmentMapping.schema, Select("__typename")), + Narrow( + User, + Group(List( + Select("id"), + Select("name") + )) + ), + Narrow( + Page, + Group(List( + Select("id"), + Select("title") + )) + ) + )) + ) ) ) ) - ) - )) + )) val expectedResult = json""" { @@ -592,27 +631,35 @@ final class FragmentSuite extends CatsEffectSuite { val Page = FragmentMapping.schema.ref("Page") val expected = - Select("user", None, + Select( + "user", + None, Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Select("favourite", None, - Group(List( - Introspect(FragmentMapping.schema, Select("__typename", None, Empty)), - Narrow( - User, - Group(List( - Select("id", None, Empty), - Select("name", None, Empty) - )) - ), - Narrow( - Page, - Group(List( - Select("id", None, Empty), - Select("title", None, Empty) - )) - ) - )) + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Select( + "favourite", + None, + Group( + List( + Introspect(FragmentMapping.schema, Select("__typename", None, Empty)), + Narrow( + User, + Group( + List( + Select("id", None, Empty), + Select("name", None, Empty) + )) + ), + Narrow( + Page, + Group( + List( + Select("id", None, Empty), + Select("title", None, Empty) + )) + ) + )) ) ) ) @@ -657,10 +704,13 @@ final class FragmentSuite extends CatsEffectSuite { """ val expected = - Select("user", + Select( + "user", Unique( - Filter(Eql(FragmentMapping.UserType / "id", Const("1")), - Select("friends", + Filter( + Eql(FragmentMapping.UserType / "id", Const("1")), + Select( + "friends", Select("id") ) ) @@ -1102,7 +1152,6 @@ final class FragmentSuite extends CatsEffectSuite { assertIO(res, expected) } - test("fragment recursion (1)") { val query = """ query withFragments { @@ -1268,11 +1317,18 @@ object FragmentData { def id: String } - case class User(id: String, name: String, profilePic: String, friendIds: List[String], favouriteId: Option[String]) extends Profile { + case class User( + id: String, + name: String, + profilePic: String, + friendIds: List[String], + favouriteId: Option[String]) + extends Profile { def friends: List[User] = friendIds.flatMap(fid => profiles.collect { case u: User if u.id == fid => u }) def mutualFriends: List[User] = - friendIds.flatMap(fid => profiles.collect { case u: User if u.id == fid && u.friendIds.contains(id) => u }) + friendIds.flatMap(fid => + profiles.collect { case u: User if u.id == fid && u.friendIds.contains(id) => u }) def favourite: Option[Profile] = favouriteId.flatMap(id => profiles.find(_.id == id)) } @@ -1328,42 +1384,39 @@ object FragmentMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("user", _ => profiles.collect { case u: User => u }), - ValueField("profiles", _ => profiles) - ) + fieldMappings = List( + ValueField("user", _ => profiles.collect { case u: User => u }), + ValueField("profiles", _ => profiles) + ) ), ValueObjectMapping[Profile]( tpe = ProfileType, - fieldMappings = - List( - ValueField("id", _.id) - ) + fieldMappings = List( + ValueField("id", _.id) + ) ), ValueObjectMapping[User]( tpe = UserType, - fieldMappings = - List( - ValueField("name", _.name), - ValueField("profilePic", _.profilePic), - ValueField("friends", _.friends), - ValueField("mutualFriends", _.mutualFriends), - ValueField("favourite", _.favourite), - ) + fieldMappings = List( + ValueField("name", _.name), + ValueField("profilePic", _.profilePic), + ValueField("friends", _.friends), + ValueField("mutualFriends", _.mutualFriends), + ValueField("favourite", _.favourite) + ) ), ValueObjectMapping[Page]( tpe = PageType, - fieldMappings = - List( - ValueField("title", _.title), - ValueField("likers", _.likers) - ) + fieldMappings = List( + ValueField("title", _.title), + ValueField("likers", _.likers) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "user", List(Binding("id", IDValue(id)))) => - Elab.transformChild(child => Unique(Filter(Eql(FragmentMapping.UserType / "id", Const(id)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(FragmentMapping.UserType / "id", Const(id)), child))) } } diff --git a/modules/core/src/test/scala/compiler/InputValuesSuite.scala b/modules/core/src/test/scala/compiler/InputValuesSuite.scala index 131a9c40..b0f27f39 100644 --- a/modules/core/src/test/scala/compiler/InputValuesSuite.scala +++ b/modules/core/src/test/scala/compiler/InputValuesSuite.scala @@ -19,9 +19,9 @@ import cats.data.NonEmptyChain import munit.CatsEffectSuite import grackle._ +import grackle.Query._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Value._ final class InputValuesSuite extends CatsEffectSuite { test("null value") { @@ -40,14 +40,30 @@ final class InputValuesSuite extends CatsEffectSuite { """ val expected = - Group(List( - UntypedSelect("field", Some("one"), List(Binding("arg", AbsentValue)), Nil, UntypedSelect("subfield", None, Nil, Nil, Empty)), - UntypedSelect("field", Some("two"), List(Binding("arg", NullValue)), Nil, UntypedSelect("subfield", None, Nil, Nil, Empty)), - UntypedSelect("field", Some("three"), List(Binding("arg", IntValue(23))), Nil, UntypedSelect("subfield", None, Nil, Nil, Empty)) - )) + Group( + List( + UntypedSelect( + "field", + Some("one"), + List(Binding("arg", AbsentValue)), + Nil, + UntypedSelect("subfield", None, Nil, Nil, Empty)), + UntypedSelect( + "field", + Some("two"), + List(Binding("arg", NullValue)), + Nil, + UntypedSelect("subfield", None, Nil, Nil, Empty)), + UntypedSelect( + "field", + Some("three"), + List(Binding("arg", IntValue(23))), + Nil, + UntypedSelect("subfield", None, Nil, Nil, Empty)) + )) val compiled = InputValuesMapping.compiler.compile(query, None) - //println(compiled) + // println(compiled) assertEquals(compiled.map(_.query), Result.Success(expected)) } @@ -64,17 +80,26 @@ final class InputValuesSuite extends CatsEffectSuite { """ val expected = - Group(List( - UntypedSelect("listField", Some("one"), List(Binding("arg", ListValue(Nil))), Nil, - UntypedSelect("subfield", None, Nil, Nil, Empty) - ), - UntypedSelect("listField", Some("two"), List(Binding("arg", ListValue(List(StringValue("foo"), StringValue("bar"))))), Nil, - UntypedSelect("subfield", None, Nil, Nil, Empty) - ) - )) + Group( + List( + UntypedSelect( + "listField", + Some("one"), + List(Binding("arg", ListValue(Nil))), + Nil, + UntypedSelect("subfield", None, Nil, Nil, Empty) + ), + UntypedSelect( + "listField", + Some("two"), + List(Binding("arg", ListValue(List(StringValue("foo"), StringValue("bar"))))), + Nil, + UntypedSelect("subfield", None, Nil, Nil, Empty) + ) + )) val compiled = InputValuesMapping.compiler.compile(query, None) - //println(compiled) + // println(compiled) assertEquals(compiled.map(_.query), Result.Success(expected)) } @@ -88,22 +113,27 @@ final class InputValuesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("objectField", None, - List(Binding("arg", - ObjectValue(List( - ("foo", IntValue(23)), - ("bar", BooleanValue(true)), - ("baz", StringValue("quux")), - ("defaulted", StringValue("quux")), - ("nullable", AbsentValue) - )) - )), + UntypedSelect( + "objectField", + None, + List( + Binding( + "arg", + ObjectValue( + List( + ("foo", IntValue(23)), + ("bar", BooleanValue(true)), + ("baz", StringValue("quux")), + ("defaulted", StringValue("quux")), + ("nullable", AbsentValue) + )) + )), Nil, UntypedSelect("subfield", None, Nil, Nil, Empty) ) val compiled = InputValuesMapping.compiler.compile(query, None) - //println(compiled) + // println(compiled) assertEquals(compiled.map(_.query), Result.Success(expected)) } @@ -116,10 +146,11 @@ final class InputValuesSuite extends CatsEffectSuite { } """ - val expected = Problem("Unknown field(s) 'wibble' for input object value of type InObj in field 'objectField' of type 'Query'") + val expected = Problem( + "Unknown field(s) 'wibble' for input object value of type InObj in field 'objectField' of type 'Query'") val compiled = InputValuesMapping.compiler.compile(query, None) - //println(compiled) + // println(compiled) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } @@ -133,14 +164,19 @@ final class InputValuesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("oneOfField", None, - List(Binding("arg", - ObjectValue(List( - ("a", IntValue(42)), - ("b", AbsentValue), - ("c", AbsentValue) - )) - )), + UntypedSelect( + "oneOfField", + None, + List( + Binding( + "arg", + ObjectValue( + List( + ("a", IntValue(42)), + ("b", AbsentValue), + ("c", AbsentValue) + )) + )), Nil, UntypedSelect("subfield", None, Nil, Nil, Empty) ) @@ -158,7 +194,8 @@ final class InputValuesSuite extends CatsEffectSuite { } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query', but found 'a', 'b'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query', but found 'a', 'b'") val compiled = OneOfInputValuesMapping.compiler.compile(query, None) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) @@ -173,7 +210,8 @@ final class InputValuesSuite extends CatsEffectSuite { } """ - val expected = Problem("Value for member field 'a' must be non-null for OneOfInObj in field 'oneOfField' of type 'Query'") + val expected = Problem( + "Value for member field 'a' must be non-null for OneOfInObj in field 'oneOfField' of type 'Query'") val compiled = OneOfInputValuesMapping.compiler.compile(query, None) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) @@ -188,7 +226,8 @@ final class InputValuesSuite extends CatsEffectSuite { } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query'") val compiled = OneOfInputValuesMapping.compiler.compile(query, None) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) @@ -203,7 +242,8 @@ final class InputValuesSuite extends CatsEffectSuite { } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query', but found 'a', 'b'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfField' of type 'Query', but found 'a', 'b'") val compiled = OneOfInputValuesMapping.compiler.compile(query, None) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) diff --git a/modules/core/src/test/scala/compiler/PredicatesSuite.scala b/modules/core/src/test/scala/compiler/PredicatesSuite.scala index ef7a3667..0cfb3edc 100644 --- a/modules/core/src/test/scala/compiler/PredicatesSuite.scala +++ b/modules/core/src/test/scala/compiler/PredicatesSuite.scala @@ -20,16 +20,21 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ object ItemData { case class Item(label: String, tags: List[String]) val items = - List(Item("A", List("A")), Item("AB", List("A", "B")), Item("BC", List("B", "C")), Item("C", List("C"))) + List( + Item("A", List("A")), + Item("AB", List("A", "B")), + Item("BC", List("B", "C")), + Item("C", List("C"))) } object ItemMapping extends ValueMapping[IO] { @@ -57,24 +62,22 @@ object ItemMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("itemByTag", _ => items), - ValueField("itemByTagCount", _ => items), - ValueField("itemByTagCountVA", _ => items), - ValueField("itemByTagCountCA", _ => items) - ) + fieldMappings = List( + ValueField("itemByTag", _ => items), + ValueField("itemByTagCount", _ => items), + ValueField("itemByTagCountVA", _ => items), + ValueField("itemByTagCountCA", _ => items) + ) ), ValueObjectMapping[Item]( tpe = ItemType, - fieldMappings = - List( - ValueField("label", _.label), - ValueField("tags", _.tags), - CursorField("tagCount", tagCount), - ValueField("tagCountVA", _.tags.size, hidden = true), - CursorField("tagCountCA", tagCount, hidden = true) - ) + fieldMappings = List( + ValueField("label", _.label), + ValueField("tags", _.tags), + CursorField("tagCount", tagCount), + ValueField("tagCountVA", _.tags.size, hidden = true), + CursorField("tagCountCA", tagCount, hidden = true) + ) ) ) diff --git a/modules/core/src/test/scala/compiler/PreserveArgsElaborator.scala b/modules/core/src/test/scala/compiler/PreserveArgsElaborator.scala index 14980bd3..695d08de 100644 --- a/modules/core/src/test/scala/compiler/PreserveArgsElaborator.scala +++ b/modules/core/src/test/scala/compiler/PreserveArgsElaborator.scala @@ -16,8 +16,8 @@ package compiler import grackle._ -import Query._ -import QueryCompiler._ +import grackle.Query._ +import grackle.QueryCompiler._ object PreserveArgsElaborator extends SelectElaborator { case class Preserved(args: List[Binding], directives: List[Directive]) @@ -26,11 +26,16 @@ object PreserveArgsElaborator extends SelectElaborator { def loop(query: Query): Query = query match { case Select(`fieldName`, alias, child) => - UntypedSelect(fieldName, alias, preserved.args, directives = preserved.directives, child) + UntypedSelect( + fieldName, + alias, + preserved.args, + directives = preserved.directives, + child) case Environment(env, child) if env.contains("preserved") => loop(child) - case e@Environment(_, child) => e.copy(child = loop(child)) + case e @ Environment(_, child) => e.copy(child = loop(child)) case g: Group => g.copy(queries = g.queries.map(loop)) - case t@TransformCursor(_, child) => t.copy(child = loop(child)) + case t @ TransformCursor(_, child) => t.copy(child = loop(child)) case other => other } @@ -41,7 +46,7 @@ object PreserveArgsElaborator extends SelectElaborator { query match { case UntypedSelect(fieldName, _, _, _, _) => for { - t <- super.transform(query) + t <- super.transform(query) preserved <- Elab.envE[Preserved]("preserved") } yield subst(t, fieldName, preserved) @@ -49,6 +54,10 @@ object PreserveArgsElaborator extends SelectElaborator { } } - def select(ref: TypeRef, name: String, args: List[Binding], directives: List[Directive]): Elab[Unit] = + def select( + ref: TypeRef, + name: String, + args: List[Binding], + directives: List[Directive]): Elab[Unit] = Elab.env("preserved", Preserved(args, directives)) } diff --git a/modules/core/src/test/scala/compiler/ProblemSuite.scala b/modules/core/src/test/scala/compiler/ProblemSuite.scala index a8ff80cf..a6903678 100644 --- a/modules/core/src/test/scala/compiler/ProblemSuite.scala +++ b/modules/core/src/test/scala/compiler/ProblemSuite.scala @@ -15,9 +15,9 @@ package compiler -import io.circe.syntax._ import io.circe.JsonObject import io.circe.literal._ +import io.circe.syntax._ import munit.CatsEffectSuite import grackle.Problem @@ -98,32 +98,39 @@ final class ProblemSuite extends CatsEffectSuite { test("toString (full)") { assertEquals( - Problem("foo", List(1 -> 2, 5 -> 6), List("bar", "baz")).toString, "foo (at bar/baz: 1..2, 5..6)" + Problem("foo", List(1 -> 2, 5 -> 6), List("bar", "baz")).toString, + "foo (at bar/baz: 1..2, 5..6)" ) } test("toString (no path)") { assertEquals( - Problem("foo", List(1 -> 2, 5 -> 6), Nil).toString, "foo (at 1..2, 5..6)" + Problem("foo", List(1 -> 2, 5 -> 6), Nil).toString, + "foo (at 1..2, 5..6)" ) } test("toString (no locations)") { assertEquals( - Problem("foo", Nil, List("bar", "baz")).toString, "foo (at bar/baz)" + Problem("foo", Nil, List("bar", "baz")).toString, + "foo (at bar/baz)" ) } test("toString (message only)") { assertEquals( - Problem("foo", Nil, Nil).toString, "foo" + Problem("foo", Nil, Nil).toString, + "foo" ) } test("extension") { - val p = Problem("foo", extensions = Some(JsonObject("bar" -> 42.asJson, "baz" -> List("a", "b").asJson))) + val p = Problem( + "foo", + extensions = Some(JsonObject("bar" -> 42.asJson, "baz" -> List("a", "b").asJson))) assertEquals( - p.asJson, json""" + p.asJson, + json""" { "message" : "foo", "extensions" : { diff --git a/modules/core/src/test/scala/compiler/QuerySizeSuite.scala b/modules/core/src/test/scala/compiler/QuerySizeSuite.scala index e742c602..fb7b8d51 100644 --- a/modules/core/src/test/scala/compiler/QuerySizeSuite.scala +++ b/modules/core/src/test/scala/compiler/QuerySizeSuite.scala @@ -17,9 +17,9 @@ package compiler import cats.data.NonEmptyChain import munit.CatsEffectSuite +import starwars.StarWarsMapping import grackle.{Problem, Result} -import starwars.StarWarsMapping class QuerySizeSuite extends CatsEffectSuite { @@ -35,7 +35,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((2,1))) + assertEquals(res, (2, 1)) } test("also depth 2 query") { @@ -51,7 +51,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((2,2))) + assertEquals(res, (2, 2)) } test("depth 3 query") { @@ -68,7 +68,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((3,1))) + assertEquals(res, (3, 1)) } test("depth 4 query") { @@ -89,7 +89,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((4,3))) + assertEquals(res, (4, 3)) } test("aliased depth 2 query") { @@ -105,7 +105,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((2,1))) + assertEquals(res, (2, 1)) } test("grouplist depth 2 query") { @@ -122,7 +122,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((2,1))) + assertEquals(res, (2, 1)) } test("fragments depth 3 query") { @@ -147,7 +147,7 @@ class QuerySizeSuite extends CatsEffectSuite { val compiledQuery = StarWarsMapping.compiler.compile(query).toOption.get.query val res = StarWarsMapping.querySizeValidator.querySize(compiledQuery, Map.empty) - assertEquals(res, ((3,5))) + assertEquals(res, (3, 5)) } test("width 2 query") { @@ -240,7 +240,6 @@ class QuerySizeSuite extends CatsEffectSuite { } """ - val expected = Problem("Query is too wide: width is 6 leaves, maximum is 5") val res = StarWarsMapping.compiler.compile(query) diff --git a/modules/core/src/test/scala/compiler/ScalarsSuite.scala b/modules/core/src/test/scala/compiler/ScalarsSuite.scala index cf54a8dc..2dc7126b 100644 --- a/modules/core/src/test/scala/compiler/ScalarsSuite.scala +++ b/modules/core/src/test/scala/compiler/ScalarsSuite.scala @@ -17,21 +17,22 @@ package compiler import java.time.{Duration, LocalDate, LocalTime, ZonedDateTime} import java.util.UUID + import scala.util.Try import cats.{Eq, Order} import cats.effect.IO import cats.implicits._ +import io.circe.Encoder import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ - -import io.circe.Encoder object MovieData { sealed trait Genre extends Product with Serializable @@ -44,7 +45,7 @@ object MovieData { def fromString(s: String): Option[Genre] = s.trim.toUpperCase match { - case "DRAMA" => Some(Drama) + case "DRAMA" => Some(Drama) case "ACTION" => Some(Action) case "COMEDY" => Some(Comedy) case _ => None @@ -73,27 +74,107 @@ object MovieData { import Genre.{Action, Comedy, Drama} case class Movie( - id: UUID, - title: String, - genre: Genre, - releaseDate: LocalDate, - showTime: LocalTime, - nextShowing: ZonedDateTime, - duration: Duration + id: UUID, + title: String, + genre: Genre, + releaseDate: LocalDate, + showTime: LocalTime, + nextShowing: ZonedDateTime, + duration: Duration ) val movies = List( - Movie(UUID.fromString("6a7837fc-b463-4d32-b628-0f4b3065cb21"), "Celine et Julie Vont en Bateau", Drama, LocalDate.parse("1974-10-07"), LocalTime.parse("19:35:00"), ZonedDateTime.parse("2020-05-22T19:35:00Z"), Duration.ofMillis(12300000)), - Movie(UUID.fromString("11daf8c0-11c3-4453-bfe1-cb6e6e2f9115"), "Duelle", Drama, LocalDate.parse("1975-09-15"), LocalTime.parse("19:20:00"), ZonedDateTime.parse("2020-05-27T19:20:00Z"), Duration.ofMillis(7260000)), - Movie(UUID.fromString("aea9756f-621b-42d5-b130-71f3916c4ba3"), "L'Amour fou", Drama, LocalDate.parse("1969-01-15"), LocalTime.parse("21:00:00"), ZonedDateTime.parse("2020-05-27T21:00:00Z"), Duration.ofMillis(15120000)), - Movie(UUID.fromString("2ddb041f-86c2-4bd3-848c-990a3862634e"), "Last Year at Marienbad", Drama, LocalDate.parse("1961-06-25"), LocalTime.parse("20:30:00"), ZonedDateTime.parse("2020-05-26T20:30:00Z"), Duration.ofMillis(5640000)), - Movie(UUID.fromString("8ae5b13b-044c-4ff0-8b71-ccdb7d77cd88"), "Zazie dans le Métro", Comedy, LocalDate.parse("1960-10-28"), LocalTime.parse("20:15:00"), ZonedDateTime.parse("2020-05-25T20:15:00Z"), Duration.ofMillis(5340000)), - Movie(UUID.fromString("9dce9deb-9188-4cc2-9685-9842b8abdd34"), "Alphaville", Action, LocalDate.parse("1965-05-05"), LocalTime.parse("19:45:00"), ZonedDateTime.parse("2020-05-19T19:45:00Z"), Duration.ofMillis(5940000)), - Movie(UUID.fromString("1bf00ac6-91ab-4e51-b686-3fd5e2324077"), "Stalker", Drama, LocalDate.parse("1979-05-13"), LocalTime.parse("15:30:00"), ZonedDateTime.parse("2020-05-19T15:30:00Z"), Duration.ofMillis(9660000)), - Movie(UUID.fromString("6a878e06-6563-4a0c-acd9-d28dcfb2e91a"), "Weekend", Comedy, LocalDate.parse("1967-12-29"), LocalTime.parse("22:30:00"), ZonedDateTime.parse("2020-05-19T22:30:00Z"), Duration.ofMillis(6300000)), - Movie(UUID.fromString("2a40415c-ea6a-413f-bbef-a80ae280c4ff"), "Daisies", Comedy, LocalDate.parse("1966-12-30"), LocalTime.parse("21:30:00"), ZonedDateTime.parse("2020-05-15T21:30:00Z"), Duration.ofMillis(4560000)), - Movie(UUID.fromString("2f6dcb0a-4122-4a21-a1c6-534744dd6b85"), "Le Pont du Nord", Drama, LocalDate.parse("1982-01-13"), LocalTime.parse("20:45:00"), ZonedDateTime.parse("2020-05-11T20:45:00Z"), Duration.ofMillis(7620000)) + Movie( + UUID.fromString("6a7837fc-b463-4d32-b628-0f4b3065cb21"), + "Celine et Julie Vont en Bateau", + Drama, + LocalDate.parse("1974-10-07"), + LocalTime.parse("19:35:00"), + ZonedDateTime.parse("2020-05-22T19:35:00Z"), + Duration.ofMillis(12300000) + ), + Movie( + UUID.fromString("11daf8c0-11c3-4453-bfe1-cb6e6e2f9115"), + "Duelle", + Drama, + LocalDate.parse("1975-09-15"), + LocalTime.parse("19:20:00"), + ZonedDateTime.parse("2020-05-27T19:20:00Z"), + Duration.ofMillis(7260000) + ), + Movie( + UUID.fromString("aea9756f-621b-42d5-b130-71f3916c4ba3"), + "L'Amour fou", + Drama, + LocalDate.parse("1969-01-15"), + LocalTime.parse("21:00:00"), + ZonedDateTime.parse("2020-05-27T21:00:00Z"), + Duration.ofMillis(15120000) + ), + Movie( + UUID.fromString("2ddb041f-86c2-4bd3-848c-990a3862634e"), + "Last Year at Marienbad", + Drama, + LocalDate.parse("1961-06-25"), + LocalTime.parse("20:30:00"), + ZonedDateTime.parse("2020-05-26T20:30:00Z"), + Duration.ofMillis(5640000) + ), + Movie( + UUID.fromString("8ae5b13b-044c-4ff0-8b71-ccdb7d77cd88"), + "Zazie dans le Métro", + Comedy, + LocalDate.parse("1960-10-28"), + LocalTime.parse("20:15:00"), + ZonedDateTime.parse("2020-05-25T20:15:00Z"), + Duration.ofMillis(5340000) + ), + Movie( + UUID.fromString("9dce9deb-9188-4cc2-9685-9842b8abdd34"), + "Alphaville", + Action, + LocalDate.parse("1965-05-05"), + LocalTime.parse("19:45:00"), + ZonedDateTime.parse("2020-05-19T19:45:00Z"), + Duration.ofMillis(5940000) + ), + Movie( + UUID.fromString("1bf00ac6-91ab-4e51-b686-3fd5e2324077"), + "Stalker", + Drama, + LocalDate.parse("1979-05-13"), + LocalTime.parse("15:30:00"), + ZonedDateTime.parse("2020-05-19T15:30:00Z"), + Duration.ofMillis(9660000) + ), + Movie( + UUID.fromString("6a878e06-6563-4a0c-acd9-d28dcfb2e91a"), + "Weekend", + Comedy, + LocalDate.parse("1967-12-29"), + LocalTime.parse("22:30:00"), + ZonedDateTime.parse("2020-05-19T22:30:00Z"), + Duration.ofMillis(6300000) + ), + Movie( + UUID.fromString("2a40415c-ea6a-413f-bbef-a80ae280c4ff"), + "Daisies", + Comedy, + LocalDate.parse("1966-12-30"), + LocalTime.parse("21:30:00"), + ZonedDateTime.parse("2020-05-15T21:30:00Z"), + Duration.ofMillis(4560000) + ), + Movie( + UUID.fromString("2f6dcb0a-4122-4a21-a1c6-534744dd6b85"), + "Le Pont du Nord", + Drama, + LocalDate.parse("1982-01-13"), + LocalTime.parse("20:45:00"), + ZonedDateTime.parse("2020-05-11T20:45:00Z"), + Duration.ofMillis(7620000) + ) ).sortBy(_.id.toString) } @@ -144,28 +225,26 @@ object MovieMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("movieById", _ => movies), - ValueField("moviesByGenre", _ => movies), - ValueField("moviesReleasedBetween", _ => movies), - ValueField("moviesLongerThan", _ => movies), - ValueField("moviesShownLaterThan", _ => movies), - ValueField("moviesShownBetween", _ => movies) - ) + fieldMappings = List( + ValueField("movieById", _ => movies), + ValueField("moviesByGenre", _ => movies), + ValueField("moviesReleasedBetween", _ => movies), + ValueField("moviesLongerThan", _ => movies), + ValueField("moviesShownLaterThan", _ => movies), + ValueField("moviesShownBetween", _ => movies) + ) ), ValueObjectMapping[Movie]( tpe = MovieType, - fieldMappings = - List( - ValueField("id", _.id), - ValueField("title", _.title), - ValueField("genre", _.genre), - ValueField("releaseDate", _.releaseDate), - ValueField("showTime", _.showTime), - ValueField("nextShowing", _.nextShowing), - ValueField("duration", _.duration) - ) + fieldMappings = List( + ValueField("id", _.id), + ValueField("title", _.title), + ValueField("genre", _.genre), + ValueField("releaseDate", _.releaseDate), + ValueField("showTime", _.showTime), + ValueField("nextShowing", _.nextShowing), + ValueField("duration", _.duration) + ) ), LeafMapping[UUID](UUIDType), LeafMapping[Genre](GenreType), @@ -210,7 +289,10 @@ object MovieMapping extends ValueMapping[IO] { Elab.transformChild(child => Unique(Filter(Eql(MovieType / "id", Const(id)), child))) case (QueryType, "moviesByGenre", List(Binding("genre", GenreValue(genre)))) => Elab.transformChild(child => Filter(Eql(MovieType / "genre", Const(genre)), child)) - case (QueryType, "moviesReleasedBetween", List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => + case ( + QueryType, + "moviesReleasedBetween", + List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => Elab.transformChild(child => Filter( And( @@ -218,23 +300,23 @@ object MovieMapping extends ValueMapping[IO] { Lt(MovieType / "releaseDate", Const(to)) ), child - ) - ) + )) case (QueryType, "moviesLongerThan", List(Binding("duration", IntervalValue(duration)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "duration", Const(duration))), child - ) - ) + )) case (QueryType, "moviesShownLaterThan", List(Binding("time", TimeValue(time)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "showTime", Const(time))), child - ) - ) - case (QueryType, "moviesShownBetween", List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => + )) + case ( + QueryType, + "moviesShownBetween", + List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => Elab.transformChild(child => Filter( And( @@ -242,8 +324,7 @@ object MovieMapping extends ValueMapping[IO] { Lt(MovieType / "nextShowing", Const(to)) ), child - ) - ) + )) } } @@ -420,7 +501,6 @@ final class ScalarsSuite extends CatsEffectSuite { assertIO(res, expected) } - test("query with LocalTime argument") { val query = """ query { diff --git a/modules/core/src/test/scala/compiler/SkipIncludeSuite.scala b/modules/core/src/test/scala/compiler/SkipIncludeSuite.scala index 846a7fdc..ea874afb 100644 --- a/modules/core/src/test/scala/compiler/SkipIncludeSuite.scala +++ b/modules/core/src/test/scala/compiler/SkipIncludeSuite.scala @@ -19,8 +19,8 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Query._ import grackle.syntax._ -import Query._ final class SkipIncludeSuite extends CatsEffectSuite { test("skip/include field") { @@ -49,10 +49,11 @@ final class SkipIncludeSuite extends CatsEffectSuite { """ val expected = - Group(List( - Select("field", Some("b"), Select("subfieldB")), - Select("field", Some("c"), Select("subfieldA")) - )) + Group( + List( + Select("field", Some("b"), Select("subfieldB")), + Select("field", Some("c"), Select("subfieldA")) + )) val compiled = SkipIncludeMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -90,22 +91,29 @@ final class SkipIncludeSuite extends CatsEffectSuite { """ val expected = - Group(List( - Select("field", Some("a")), - Select("field", Some("b"), - Group(List( - Select("subfieldA"), - Select("subfieldB") - )) - ), - Select("field", Some("c"), - Group(List( - Select("subfieldA"), - Select("subfieldB") - )) - ), - Select("field", Some("d")) - )) + Group( + List( + Select("field", Some("a")), + Select( + "field", + Some("b"), + Group( + List( + Select("subfieldA"), + Select("subfieldB") + )) + ), + Select( + "field", + Some("c"), + Group( + List( + Select("subfieldA"), + Select("subfieldB") + )) + ), + Select("field", Some("d")) + )) val compiled = SkipIncludeMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -136,11 +144,13 @@ final class SkipIncludeSuite extends CatsEffectSuite { """ val expected = - Select("field", - Group(List( - Select("subfieldB", Some("b")), - Select("subfieldA", Some("c")) - )) + Select( + "field", + Group( + List( + Select("subfieldB", Some("b")), + Select("subfieldA", Some("c")) + )) ) val compiled = SkipIncludeMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -186,22 +196,29 @@ final class SkipIncludeSuite extends CatsEffectSuite { """ val expected = - Group(List( - Select("field", Some("a")), - Select("field", Some("b"), - Group(List( - Select("subfieldA"), - Select("subfieldB") - )) - ), - Select("field", Some("c"), - Group(List( - Select("subfieldA"), - Select("subfieldB") - )) - ), - Select("field", Some("d")) - )) + Group( + List( + Select("field", Some("a")), + Select( + "field", + Some("b"), + Group( + List( + Select("subfieldA"), + Select("subfieldB") + )) + ), + Select( + "field", + Some("c"), + Group( + List( + Select("subfieldA"), + Select("subfieldB") + )) + ), + Select("field", Some("d")) + )) val compiled = SkipIncludeMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -230,11 +247,13 @@ final class SkipIncludeSuite extends CatsEffectSuite { """ val expected = - Select("field", - Group(List( - Select("subfieldB", Some("b")), - Select("subfieldA", Some("c")) - )) + Select( + "field", + Group( + List( + Select("subfieldB", Some("b")), + Select("subfieldA", Some("c")) + )) ) val compiled = SkipIncludeMapping.compiler.compile(query, untypedVars = Some(variables)) diff --git a/modules/core/src/test/scala/compiler/VariablesSuite.scala b/modules/core/src/test/scala/compiler/VariablesSuite.scala index be3aef04..a0969441 100644 --- a/modules/core/src/test/scala/compiler/VariablesSuite.scala +++ b/modules/core/src/test/scala/compiler/VariablesSuite.scala @@ -20,9 +20,9 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Query._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Value._ final class VariablesSuite extends CatsEffectSuite { test("simple variables query") { @@ -43,12 +43,17 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("user", None, List(Binding("id", IDValue("4"))), Nil, - Group(List( - UntypedSelect("id", None, Nil, Nil, Empty), - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("profilePic", None, List(Binding("size", IntValue(60))), Nil, Empty) - )) + UntypedSelect( + "user", + None, + List(Binding("id", IDValue("4"))), + Nil, + Group( + List( + UntypedSelect("id", None, Nil, Nil, Empty), + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect("profilePic", None, List(Binding("size", IntValue(60))), Nil, Empty) + )) ) val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -72,7 +77,9 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("users", None, + UntypedSelect( + "users", + None, List(Binding("ids", ListValue(List(IDValue("1"), IDValue("2"), IDValue("3"))))), Nil, UntypedSelect("name", None, Nil, Nil, Empty) @@ -99,7 +106,9 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("usersByType", None, + UntypedSelect( + "usersByType", + None, List(Binding("userType", EnumValue("ADMIN"))), Nil, UntypedSelect("name", None, Nil, Nil, Empty) @@ -126,7 +135,9 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("usersLoggedInByDate", None, + UntypedSelect( + "usersLoggedInByDate", + None, List(Binding("date", StringValue("2021-02-22"))), Nil, UntypedSelect("name", None, Nil, Nil, Empty) @@ -153,7 +164,9 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("queryWithBigDecimal", None, + UntypedSelect( + "queryWithBigDecimal", + None, List(Binding("input", IntValue(2021))), Nil, UntypedSelect("name", None, Nil, Nil, Empty) @@ -185,21 +198,27 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("search", None, - List(Binding("pattern", - ObjectValue(List( - ("name", StringValue("Foo")), - ("age", IntValue(23)), - ("id", IDValue("123")), - ("userType", AbsentValue), - ("date", AbsentValue) - )) - )), + UntypedSelect( + "search", + None, + List( + Binding( + "pattern", + ObjectValue( + List( + ("name", StringValue("Foo")), + ("age", IntValue(23)), + ("id", IDValue("123")), + ("userType", AbsentValue), + ("date", AbsentValue) + )) + )), Nil, - Group(List( - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("id", None, Nil, Nil, Empty) - )) + Group( + List( + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect("id", None, Nil, Nil, Empty) + )) ) val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -228,7 +247,8 @@ final class VariablesSuite extends CatsEffectSuite { } """ - val expected = Problem("Unknown field(s) 'quux' in input object value of type Pattern in variable values") + val expected = Problem( + "Unknown field(s) 'quux' in input object value of type Pattern in variable values") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -251,7 +271,9 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("users", None, + UntypedSelect( + "users", + None, List(Binding("ids", ListValue(List(IDValue("1"), IDValue("2"), IDValue("3"))))), Nil, UntypedSelect("name", None, Nil, Nil, Empty) @@ -279,21 +301,27 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("search", None, - List(Binding("pattern", - ObjectValue(List( - ("name", StringValue("Foo")), - ("age", IntValue(23)), - ("id", IDValue("123")), - ("userType", AbsentValue), - ("date", AbsentValue) - )) - )), + UntypedSelect( + "search", + None, + List( + Binding( + "pattern", + ObjectValue( + List( + ("name", StringValue("Foo")), + ("age", IntValue(23)), + ("id", IDValue("123")), + ("userType", AbsentValue), + ("date", AbsentValue) + )) + )), Nil, - Group(List( - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("id", None, Nil, Nil, Empty) - )) + Group( + List( + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect("id", None, Nil, Nil, Empty) + )) ) val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -318,21 +346,27 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("search", None, - List(Binding("pattern", - ObjectValue(List( - ("name", StringValue("Foo")), - ("age", IntValue(23)), - ("id", IDValue("123")), - ("userType", EnumValue("ADMIN")), - ("date", AbsentValue) - )) - )), + UntypedSelect( + "search", + None, + List( + Binding( + "pattern", + ObjectValue( + List( + ("name", StringValue("Foo")), + ("age", IntValue(23)), + ("id", IDValue("123")), + ("userType", EnumValue("ADMIN")), + ("date", AbsentValue) + )) + )), Nil, - Group(List( - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("id", None, Nil, Nil, Empty) - )) + Group( + List( + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect("id", None, Nil, Nil, Empty) + )) ) val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -357,21 +391,27 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("search", None, - List(Binding("pattern", - ObjectValue(List( - ("name", StringValue("Foo")), - ("age", IntValue(23)), - ("id", IDValue("123")), - ("userType", AbsentValue), - ("date", StringValue("2021-02-22")) - )) - )), + UntypedSelect( + "search", + None, + List( + Binding( + "pattern", + ObjectValue( + List( + ("name", StringValue("Foo")), + ("age", IntValue(23)), + ("id", IDValue("123")), + ("userType", AbsentValue), + ("date", StringValue("2021-02-22")) + )) + )), Nil, - Group(List( - UntypedSelect("name", None, Nil, Nil, Empty), - UntypedSelect("id", None, Nil, Nil, Empty) - )) + Group( + List( + UntypedSelect("name", None, Nil, Nil, Empty), + UntypedSelect("id", None, Nil, Nil, Empty) + )) ) val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) @@ -396,7 +436,11 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("user", None, List(Binding("id", IDValue("4"))), Nil, + UntypedSelect( + "user", + None, + List(Binding("id", IDValue("4"))), + Nil, UntypedSelect("id", None, Nil, Nil, Empty) ) @@ -423,7 +467,6 @@ final class VariablesSuite extends CatsEffectSuite { assertEquals(compiled, expected) } - test("variable not defined (2)") { val query = """ query getZuckProfile($devicePicSize: Int) { @@ -477,7 +520,6 @@ final class VariablesSuite extends CatsEffectSuite { assertEquals(compiled.map(_.query), expected) } - test("variable not defined (5)") { val query = """ query getZuckProfile($skipName: Boolean) { @@ -526,7 +568,11 @@ final class VariablesSuite extends CatsEffectSuite { val expected = Operation( - UntypedSelect("user", None, List(Binding("id", IDValue("4"))), Nil, + UntypedSelect( + "user", + None, + List(Binding("id", IDValue("4"))), + Nil, Group( List( UntypedSelect("id", None, Nil, Nil, Empty), @@ -557,14 +603,19 @@ final class VariablesSuite extends CatsEffectSuite { """ val expected = - UntypedSelect("oneOfTest", None, - List(Binding("input", - ObjectValue(List( - ("a", AbsentValue), - ("b", BooleanValue(true)), - ("c", AbsentValue) - )) - )), + UntypedSelect( + "oneOfTest", + None, + List( + Binding( + "input", + ObjectValue( + List( + ("a", AbsentValue), + ("b", BooleanValue(true)), + ("c", AbsentValue) + )) + )), Nil, Empty ) @@ -589,7 +640,8 @@ final class VariablesSuite extends CatsEffectSuite { } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } @@ -608,7 +660,8 @@ final class VariablesSuite extends CatsEffectSuite { } """ - val expected = Problem("Value for member field 'a' must be non-null for OneOfInObj in field 'oneOfTest' of type 'Query'") + val expected = Problem( + "Value for member field 'a' must be non-null for OneOfInObj in field 'oneOfTest' of type 'Query'") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } @@ -623,7 +676,8 @@ final class VariablesSuite extends CatsEffectSuite { { "input": { } } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query'") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } @@ -643,7 +697,8 @@ final class VariablesSuite extends CatsEffectSuite { } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } @@ -658,7 +713,8 @@ final class VariablesSuite extends CatsEffectSuite { { "a": 23 } """ - val expected = Problem("Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") + val expected = Problem( + "Exactly one key must be specified for oneOf input object OneOfInObj in field 'oneOfTest' of type 'Query', but found 'a', 'b'") val compiled = VariablesMapping.compiler.compile(query, untypedVars = Some(variables)) assertEquals(compiled.map(_.query), Result.Failure(NonEmptyChain.one(expected))) } diff --git a/modules/core/src/test/scala/composed/ComposedData.scala b/modules/core/src/test/scala/composed/ComposedData.scala index c53c2a31..0c777d7a 100644 --- a/modules/core/src/test/scala/composed/ComposedData.scala +++ b/modules/core/src/test/scala/composed/ComposedData.scala @@ -19,17 +19,18 @@ import cats.effect.IO import cats.implicits._ import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ /* Currency component */ object CurrencyData { case class Currency( - code: String, - exchangeRate: Double + code: String, + exchangeRate: Double ) val EUR = Currency("EUR", 1.12) @@ -59,24 +60,23 @@ object CurrencyMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("fx", _ => currencies) - ) + fieldMappings = List( + ValueField("fx", _ => currencies) + ) ), ValueObjectMapping[Currency]( tpe = CurrencyType, - fieldMappings = - List( - ValueField("code", _.code), - ValueField("exchangeRate", _.exchangeRate) - ) + fieldMappings = List( + ValueField("code", _.code), + ValueField("exchangeRate", _.exchangeRate) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "fx", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CurrencyType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CurrencyType / "code", Const(code)), child))) } } @@ -84,9 +84,9 @@ object CurrencyMapping extends ValueMapping[IO] { object CountryData { case class Country( - code: String, - name: String, - currencyCode: String + code: String, + name: String, + currencyCode: String ) val DEU = Country("DEU", "Germany", "EUR") @@ -118,25 +118,24 @@ object CountryMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("country", _ => countries), - ValueField("countries", _ => countries) - ) + fieldMappings = List( + ValueField("country", _ => countries), + ValueField("countries", _ => countries) + ) ), ValueObjectMapping[Country]( tpe = CountryType, - fieldMappings = - List( - ValueField("code", _.code), - ValueField("name", _.name) - ) + fieldMappings = List( + ValueField("code", _.code), + ValueField("name", _.name) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CurrencyMapping.CurrencyType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CurrencyMapping.CurrencyType / "code", Const(code)), child))) } } @@ -167,35 +166,37 @@ object ComposedMapping extends ComposedMapping[IO] { override val selectElaborator = SelectElaborator { case (QueryType, "fx", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CurrencyType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CurrencyType / "code", Const(code)), child))) case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CountryType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CountryType / "code", Const(code)), child))) } val typeMappings = List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - Delegate("country", CountryMapping), - Delegate("countries", CountryMapping), - Delegate("fx", CurrencyMapping) - ) - ), + fieldMappings = List( + Delegate("country", CountryMapping), + Delegate("countries", CountryMapping), + Delegate("fx", CurrencyMapping) + ) + ), ObjectMapping( tpe = CountryType, - fieldMappings = - List( - Delegate("currency", CurrencyMapping, countryCurrencyJoin) - ) + fieldMappings = List( + Delegate("currency", CurrencyMapping, countryCurrencyJoin) + ) ) ) def countryCurrencyJoin(q: Query, c: Cursor): Result[Query] = (c.focus, q) match { case (c: CountryData.Country, Select("currency", _, child)) => - Select("fx", Unique(Filter(Eql(CurrencyType / "code", Const(c.currencyCode)), child))).success + Select( + "fx", + Unique(Filter(Eql(CurrencyType / "code", Const(c.currencyCode)), child))).success case _ => Result.internalError(s"Unexpected cursor focus type in countryCurrencyJoin") } diff --git a/modules/core/src/test/scala/composed/ComposedListSuite.scala b/modules/core/src/test/scala/composed/ComposedListSuite.scala index 3518aae6..0a109a04 100644 --- a/modules/core/src/test/scala/composed/ComposedListSuite.scala +++ b/modules/core/src/test/scala/composed/ComposedListSuite.scala @@ -21,10 +21,11 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ object CollectionData { case class Collection(name: String, itemIds: List[String]) @@ -58,26 +59,25 @@ object CollectionMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("collection", _ => collections.head), - ValueField("collections", _ => collections), - ValueField("collectionByName", _ => collections) - ) + fieldMappings = List( + ValueField("collection", _ => collections.head), + ValueField("collections", _ => collections), + ValueField("collectionByName", _ => collections) + ) ), ValueObjectMapping[Collection]( tpe = CollectionType, - fieldMappings = - List( - ValueField("name", _.name), - ValueField("itemIds", _.itemIds) - ) + fieldMappings = List( + ValueField("name", _.name), + ValueField("itemIds", _.itemIds) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "collectionByName", List(Binding("name", StringValue(name)))) => - Elab.transformChild(child => Unique(Filter(Eql(CollectionType / "name", Const(name)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CollectionType / "name", Const(name)), child))) } } @@ -107,18 +107,16 @@ object ItemMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("itemById", _ => items) - ) + fieldMappings = List( + ValueField("itemById", _ => items) + ) ), ValueObjectMapping[Item]( tpe = ItemType, - fieldMappings = - List( - ValueField("id", _.id), - ValueField("name", _.name) - ) + fieldMappings = List( + ValueField("id", _.id), + ValueField("name", _.name) + ) ) ) @@ -156,34 +154,36 @@ object ComposedListMapping extends ComposedMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - Delegate("collection", CollectionMapping), - Delegate("collections", CollectionMapping), - Delegate("collectionByName", CollectionMapping), - Delegate("itemById", ItemMapping) - ) + fieldMappings = List( + Delegate("collection", CollectionMapping), + Delegate("collections", CollectionMapping), + Delegate("collectionByName", CollectionMapping), + Delegate("itemById", ItemMapping) + ) ), ObjectMapping( tpe = CollectionType, - fieldMappings = - List( - Delegate("items", ItemMapping, collectionItemJoin) - ) + fieldMappings = List( + Delegate("items", ItemMapping, collectionItemJoin) + ) ) - ) + ) override val selectElaborator = SelectElaborator { case (QueryType, "itemById", List(Binding("id", IDValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(ItemType / "id", Const(id)), child))) case (QueryType, "collectionByName", List(Binding("name", StringValue(name)))) => - Elab.transformChild(child => Unique(Filter(Eql(CollectionType / "name", Const(name)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CollectionType / "name", Const(name)), child))) } def collectionItemJoin(q: Query, c: Cursor): Result[Query] = (c.focus, q) match { case (c: CollectionData.Collection, Select("items", _, child)) => - Group(c.itemIds.map(id => Select("itemById", Unique(Filter(Eql(ItemType / "id", Const(id)), child))))).success + Group(c + .itemIds + .map(id => + Select("itemById", Unique(Filter(Eql(ItemType / "id", Const(id)), child))))).success case _ => Result.internalError(s"Unexpected cursor focus type in collectionItemJoin") } diff --git a/modules/core/src/test/scala/directives/DirectiveValidationSuite.scala b/modules/core/src/test/scala/directives/DirectiveValidationSuite.scala index 5da87448..49d082bb 100644 --- a/modules/core/src/test/scala/directives/DirectiveValidationSuite.scala +++ b/modules/core/src/test/scala/directives/DirectiveValidationSuite.scala @@ -19,12 +19,12 @@ import cats.MonadThrow import cats.data.Chain import cats.effect.IO import cats.implicits._ +import compiler.PreserveArgsElaborator import munit.CatsEffectSuite -import compiler.PreserveArgsElaborator import grackle._ +import grackle.Query._ import grackle.syntax._ -import Query._ final class DirectiveValidationSuite extends CatsEffectSuite { test("Schema with validly located directives") { @@ -194,7 +194,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite { Problem("""Unknown argument(s) 'x' in directive withRequiredArg"""), Problem("""Expected String found '1' for 'reason' in directive deprecated"""), Problem("""Unknown argument(s) 'x' in directive deprecated"""), - Problem("""Expected String found 'null' for 'reason' in directive deprecated"""), + Problem("""Expected String found 'null' for 'reason' in directive deprecated""") ) assertEquals(schema.toProblems, problems) @@ -204,7 +204,11 @@ final class DirectiveValidationSuite extends CatsEffectSuite { val expected = List( Operation( - UntypedSelect("foo", None, Nil, List(Directive("onField", Nil)), + UntypedSelect( + "foo", + None, + Nil, + List(Directive("onField", Nil)), Group( List( UntypedSelect("bar", Some("baz"), Nil, List(Directive("onField", Nil)), Empty), @@ -213,17 +217,17 @@ final class DirectiveValidationSuite extends CatsEffectSuite { ) ), ExecutableDirectiveMapping.QueryType, - List(Directive("onQuery",List())) + List(Directive("onQuery", List())) ), Operation( - UntypedSelect("foo",None, Nil, List(Directive("onField", Nil)), Empty), + UntypedSelect("foo", None, Nil, List(Directive("onField", Nil)), Empty), ExecutableDirectiveMapping.MutationType, - List(Directive("onMutation",List())) + List(Directive("onMutation", List())) ), Operation( - UntypedSelect("foo",None, Nil, List(Directive("onField", Nil)), Empty), + UntypedSelect("foo", None, Nil, List(Directive("onField", Nil)), Empty), ExecutableDirectiveMapping.SubscriptionType, - List(Directive("onSubscription",List())) + List(Directive("onSubscription", List())) ) ) @@ -251,7 +255,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite { |""".stripMargin val res = ExecutableDirectiveMapping.compileAllOperations(query) - //println(res) + // println(res) assertEquals(res, expected.success) } @@ -301,7 +305,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite { |""".stripMargin val res = ExecutableDirectiveMapping.compileAllOperations(query) - //println(res) + // println(res) assertEquals(res.toProblems, problems) } @@ -332,7 +336,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite { |""".stripMargin val res = ExecutableDirectiveMapping.compileAllOperations(query) - //println(res) + // println(res) assertEquals(res.toProblems, problems) } diff --git a/modules/core/src/test/scala/directives/QueryDirectivesSuite.scala b/modules/core/src/test/scala/directives/QueryDirectivesSuite.scala index b29965a0..8db4783e 100644 --- a/modules/core/src/test/scala/directives/QueryDirectivesSuite.scala +++ b/modules/core/src/test/scala/directives/QueryDirectivesSuite.scala @@ -21,8 +21,9 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Query._ +import grackle.QueryCompiler._ import grackle.syntax._ -import Query._, QueryCompiler._ final class QueryDirectivesSuite extends CatsEffectSuite { test("simple query") { @@ -50,7 +51,7 @@ final class QueryDirectivesSuite extends CatsEffectSuite { val res = QueryDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -79,7 +80,7 @@ final class QueryDirectivesSuite extends CatsEffectSuite { val res = QueryDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -108,7 +109,7 @@ final class QueryDirectivesSuite extends CatsEffectSuite { val res = QueryDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -142,7 +143,7 @@ final class QueryDirectivesSuite extends CatsEffectSuite { val res = QueryDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } } @@ -168,34 +169,36 @@ object QueryDirectivesMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("user", _ => ()) - ) + fieldMappings = List( + ValueField("user", _ => ()) + ) ), ValueObjectMapping[Unit]( tpe = UserType, - fieldMappings = - List( - ValueField("name", _ => "Mary"), - ValueField("handle", _ => "mary"), - ValueField("age", _ => 42) - ) + fieldMappings = List( + ValueField("name", _ => "Mary"), + ValueField("handle", _ => "mary"), + ValueField("age", _ => 42) + ) ) ) object upperCaseElaborator extends Phase { override def transform(query: Query): Elab[Query] = query match { - case UntypedSelect(nme, alias, _, directives, _) if directives.exists(_.name == "upperCase") => + case UntypedSelect(nme, alias, _, directives, _) + if directives.exists(_.name == "upperCase") => for { - c <- Elab.context - fc <- Elab.liftR(c.forField(nme, alias)) - res <- if (fc.tpe =:= ScalarType.StringType) - super.transform(query).map(TransformCursor(toUpperCase, _)) - else - // We could make this fail the whole query by yielding Elab.failure here - Elab.warning(s"'upperCase' directive may only be applied to fields of type String") *> super.transform(query) + c <- Elab.context + fc <- Elab.liftR(c.forField(nme, alias)) + res <- + if (fc.tpe =:= ScalarType.StringType) + super.transform(query).map(TransformCursor(toUpperCase, _)) + else + // We could make this fail the whole query by yielding Elab.failure here + Elab.warning( + s"'upperCase' directive may only be applied to fields of type String") *> super + .transform(query) } yield res case _ => super.transform(query) diff --git a/modules/core/src/test/scala/directives/SchemaDirectivesSuite.scala b/modules/core/src/test/scala/directives/SchemaDirectivesSuite.scala index c264c5b9..e16feaec 100644 --- a/modules/core/src/test/scala/directives/SchemaDirectivesSuite.scala +++ b/modules/core/src/test/scala/directives/SchemaDirectivesSuite.scala @@ -16,14 +16,16 @@ package directives import cats.effect.IO +import directives.SchemaDirectivesMapping.AuthStatus import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.Cursor._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Cursor._, Query._, QueryCompiler._, Value._ - -import SchemaDirectivesMapping.AuthStatus final class SchemaDirectivesSuite extends CatsEffectSuite { test("No auth, success") { @@ -47,7 +49,7 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { val res = SchemaDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -74,7 +76,7 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { val res = SchemaDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -103,9 +105,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("USER"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("USER"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -130,9 +134,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("USER"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("USER"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -163,9 +169,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("ADMIN"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("ADMIN"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -199,9 +207,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("USER"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("USER"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -230,9 +240,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("ADMIN"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("ADMIN"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -251,9 +263,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("USER"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("USER"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -276,7 +290,7 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { val res = SchemaDirectivesMapping.compileAndRun(query) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -295,9 +309,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("ADMIN"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("ADMIN"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } @@ -318,9 +334,11 @@ final class SchemaDirectivesSuite extends CatsEffectSuite { } """ - val res = SchemaDirectivesMapping.compileAndRun(query, env = Env("authStatus" -> AuthStatus("USER"))) + val res = SchemaDirectivesMapping.compileAndRun( + query, + env = Env("authStatus" -> AuthStatus("USER"))) - //res.flatMap(IO.println) *> + // res.flatMap(IO.println) *> assertIO(res, expected) } } @@ -361,28 +379,25 @@ object SchemaDirectivesMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("products", _ => List("Cheese", "Wine", "Bread")), - ValueField("user", _ => ()) - ) + fieldMappings = List( + ValueField("products", _ => List("Cheese", "Wine", "Bread")), + ValueField("user", _ => ()) + ) ), ValueObjectMapping[Unit]( tpe = MutationType, - fieldMappings = - List( - ValueField("needsBackup", _ => true), - ValueField("backup", _ => true) - ) + fieldMappings = List( + ValueField("needsBackup", _ => true), + ValueField("backup", _ => true) + ) ), ValueObjectMapping[Unit]( tpe = UserType, - fieldMappings = - List( - ValueField("name", _ => "Mary"), - ValueField("email", _ => "mary@example.com"), - ValueField("phone", _ => Some("123456789")), - ) + fieldMappings = List( + ValueField("name", _ => "Mary"), + ValueField("email", _ => "mary@example.com"), + ValueField("phone", _ => Some("123456789")) + ) ) ) @@ -390,13 +405,20 @@ object SchemaDirectivesMapping extends ValueMapping[IO] { object permissionsElaborator extends Phase { override def transform(query: Query): Elab[Query] = { - def checkPermissions(c: Context, name: String, status: Option[AuthStatus], query: Query, nullAndWarn: Boolean): Elab[Query] = { + def checkPermissions( + c: Context, + name: String, + status: Option[AuthStatus], + query: Query, + nullAndWarn: Boolean): Elab[Query] = { val dirs = c.tpe.directives ++ c.tpe.fieldInfo(name).map(_.directives).getOrElse(Nil) val requiresAuth = dirs.exists(_.name == "authenticated") val roles = - dirs.filter(_.name == "hasRole").flatMap(_.args.filter(_.name == "requires")).map(_.value).collect { - case EnumValue(role) => role - } + dirs + .filter(_.name == "hasRole") + .flatMap(_.args.filter(_.name == "requires")) + .map(_.value) + .collect { case EnumValue(role) => role } val requiresRole = if (roles.contains("ADMIN")) Some(AuthStatus("ADMIN")) else if (roles.contains("USER")) Some(AuthStatus("USER")) @@ -420,10 +442,10 @@ object SchemaDirectivesMapping extends ValueMapping[IO] { query match { case s: UntypedSelect => for { - c <- Elab.context + c <- Elab.context status <- Elab.env[AuthStatus]("authStatus") query0 <- super.transform(query) - res <- checkPermissions(c, s.name, status, query0, s.name == "phone") + res <- checkPermissions(c, s.name, status, query0, s.name == "phone") } yield res case _ => diff --git a/modules/core/src/test/scala/effects/ValueEffectData.scala b/modules/core/src/test/scala/effects/ValueEffectData.scala index 7ba860f0..f858f993 100644 --- a/modules/core/src/test/scala/effects/ValueEffectData.scala +++ b/modules/core/src/test/scala/effects/ValueEffectData.scala @@ -42,23 +42,22 @@ class ValueEffectMapping[F[_]: Sync](ref: SignallingRef[F, Int]) extends ValueMa val typeMappings = List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - // Compute a ValueCursor - RootEffect.computeCursor("foo")((p, e) => - ref.update(_+1).as( + fieldMappings = List( + // Compute a ValueCursor + RootEffect.computeCursor("foo")((p, e) => + ref + .update(_ + 1) + .as( Result(valueCursor(p, e, Struct(42, "hi"))) - ) - ) - ) + )) + ) ), ValueObjectMapping[Struct]( tpe = StructType, - fieldMappings = - List( - ValueField("n", _.n), - ValueField("s", _.s), - ) + fieldMappings = List( + ValueField("n", _.n), + ValueField("s", _.s) + ) ) ) } diff --git a/modules/core/src/test/scala/effects/ValueEffectSuite.scala b/modules/core/src/test/scala/effects/ValueEffectSuite.scala index 9605140f..06a4b5c0 100644 --- a/modules/core/src/test/scala/effects/ValueEffectSuite.scala +++ b/modules/core/src/test/scala/effects/ValueEffectSuite.scala @@ -45,10 +45,10 @@ final class ValueEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new ValueEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new ValueEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -77,10 +77,10 @@ final class ValueEffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new ValueEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new ValueEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) diff --git a/modules/core/src/test/scala/introspection/IntrospectionSuite.scala b/modules/core/src/test/scala/introspection/IntrospectionSuite.scala index b6e17c47..6bea0455 100644 --- a/modules/core/src/test/scala/introspection/IntrospectionSuite.scala +++ b/modules/core/src/test/scala/introspection/IntrospectionSuite.scala @@ -16,14 +16,13 @@ package introspection import cats.effect.IO -import io.circe.{ ACursor, Json } +import io.circe.{ACursor, Json} import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.QueryCompiler.IntrospectionLevel._ import grackle.syntax._ -import QueryCompiler.IntrospectionLevel -import IntrospectionLevel._ final class IntrospectionSuite extends CatsEffectSuite { def standardTypeName(name: String): Boolean = name match { @@ -41,7 +40,7 @@ final class IntrospectionSuite extends CatsEffectSuite { def go(ac: ACursor, i: Int = 0): ACursor = { val acʹ = ac.downN(i) acʹ.focus match { - case None => ac + case None => ac case Some(j) => if (f(j)) go(ac, i + 1) else go(acʹ.delete, i) } } @@ -1143,100 +1142,100 @@ final class IntrospectionSuite extends CatsEffectSuite { test("standard introspection query") { val query = """ - |query IntrospectionQuery { - | __schema { - | queryType { name } - | mutationType { name } - | subscriptionType { name } - | types { - | ...FullType - | } - | directives { - | name - | description - | locations - | args(includeDeprecated: true) { - | ...InputValue - | } - | isRepeatable - | } - | } - |} - | - |fragment FullType on __Type { - | kind - | name - | description - | fields(includeDeprecated: true) { - | name - | description - | args(includeDeprecated: true) { - | ...InputValue - | } - | type { - | ...TypeRef - | } - | isDeprecated - | deprecationReason - | } - | inputFields(includeDeprecated: true) { - | ...InputValue - | } - | interfaces { - | ...TypeRef - | } - | enumValues(includeDeprecated: true) { - | name - | description - | isDeprecated - | deprecationReason - | } - | possibleTypes { - | ...TypeRef - | } - |} - | - |fragment InputValue on __InputValue { - | name - | description - | type { ...TypeRef } - | defaultValue - | isDeprecated - | deprecationReason - |} - | - |fragment TypeRef on __Type { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | } - | } - | } - | } - | } - | } - | } - |} + |query IntrospectionQuery { + | __schema { + | queryType { name } + | mutationType { name } + | subscriptionType { name } + | types { + | ...FullType + | } + | directives { + | name + | description + | locations + | args(includeDeprecated: true) { + | ...InputValue + | } + | isRepeatable + | } + | } + |} + | + |fragment FullType on __Type { + | kind + | name + | description + | fields(includeDeprecated: true) { + | name + | description + | args(includeDeprecated: true) { + | ...InputValue + | } + | type { + | ...TypeRef + | } + | isDeprecated + | deprecationReason + | } + | inputFields(includeDeprecated: true) { + | ...InputValue + | } + | interfaces { + | ...TypeRef + | } + | enumValues(includeDeprecated: true) { + | name + | description + | isDeprecated + | deprecationReason + | } + | possibleTypes { + | ...TypeRef + | } + |} + | + |fragment InputValue on __InputValue { + | name + | description + | type { ...TypeRef } + | defaultValue + | isDeprecated + | deprecationReason + |} + | + |fragment TypeRef on __Type { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | } + | } + | } + | } + | } + | } + | } + |} """.stripMargin.trim val expected = json""" @@ -1903,63 +1902,57 @@ object TestMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("users", identity), - ValueField("kind", identity), - ValueField("deprecation", identity), - ) + fieldMappings = List( + ValueField("users", identity), + ValueField("kind", identity), + ValueField("deprecation", identity) + ) ), ValueObjectMapping[Unit]( tpe = UserType, - fieldMappings = - List( - //ValueField("id", identity), - ValueField("name", identity), - ValueField("age", identity), - ValueField("birthday", identity) - ) + fieldMappings = List( + // ValueField("id", identity), + ValueField("name", identity), + ValueField("age", identity), + ValueField("birthday", identity) + ) ), ValueObjectMapping[Unit]( tpe = ProfileType, - fieldMappings = - List( - ValueField("id", identity) - ) + fieldMappings = List( + ValueField("id", identity) + ) ), ValueObjectMapping[Unit]( tpe = DateType, - fieldMappings = - List( - ValueField("day", identity), - ValueField("month", identity), - ValueField("year", identity) - ) + fieldMappings = List( + ValueField("day", identity), + ValueField("month", identity), + ValueField("year", identity) + ) ), ValueObjectMapping[Unit]( tpe = KindTestType, - fieldMappings = - List( - ValueField("scalar", identity), - ValueField("object", identity), - ValueField("interface", identity), - ValueField("union", identity), - ValueField("enum", identity), - ValueField("list", identity), - ValueField("nonnull", identity), - ValueField("nonnulllistnonnull", identity) - ) + fieldMappings = List( + ValueField("scalar", identity), + ValueField("object", identity), + ValueField("interface", identity), + ValueField("union", identity), + ValueField("enum", identity), + ValueField("list", identity), + ValueField("nonnull", identity), + ValueField("nonnulllistnonnull", identity) + ) ), ValueObjectMapping[Unit]( tpe = DeprecationTestType, - fieldMappings = - List( - ValueField("user", identity), - ValueField("flags", identity) - ) + fieldMappings = List( + ValueField("user", identity), + ValueField("flags", identity) + ) ), LeafMapping[String](FlagsType) - ) + ) } object SmallData { @@ -2009,42 +2002,37 @@ object SmallMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("users", _ => users), - ValueField("profiles", _ => users) - ) + fieldMappings = List( + ValueField("users", _ => users), + ValueField("profiles", _ => users) + ) ), ObjectMapping( tpe = SubscriptionType, - fieldMappings = - List( - ValueField.fromValue("dummy", 0) - ) + fieldMappings = List( + ValueField.fromValue("dummy", 0) + ) ), ObjectMapping( tpe = MutationType, - fieldMappings = - List( - ValueField.fromValue("dummy", 1) - ) + fieldMappings = List( + ValueField.fromValue("dummy", 1) + ) ), ValueObjectMapping[User]( tpe = UserType, - fieldMappings = - List( - ValueField("name", _.name), - ValueField("age", _.age) - ) + fieldMappings = List( + ValueField("name", _.name), + ValueField("age", _.age) + ) ), ValueObjectMapping[Profile]( tpe = ProfileType, - fieldMappings = - List( - ValueField("id", _.id) - ) + fieldMappings = List( + ValueField("id", _.id) + ) ) - ) + ) } object InputMapping extends ValueMapping[IO] { @@ -2083,20 +2071,18 @@ object InputMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("user", _ => users.head), - ValueField("userWithOneOf", _ => users.head) - ) + fieldMappings = List( + ValueField("user", _ => users.head), + ValueField("userWithOneOf", _ => users.head) + ) ), ValueObjectMapping[User]( tpe = UserType, - fieldMappings = - List( - ValueField("id", _.id), - ValueField("name", _.name), - ValueField("age", _.age) - ) + fieldMappings = List( + ValueField("id", _.id), + ValueField("name", _.name), + ValueField("age", _.age) + ) ) - ) + ) } diff --git a/modules/core/src/test/scala/laws/ResultSuite.scala b/modules/core/src/test/scala/laws/ResultSuite.scala index ec2be086..ea9add25 100644 --- a/modules/core/src/test/scala/laws/ResultSuite.scala +++ b/modules/core/src/test/scala/laws/ResultSuite.scala @@ -35,27 +35,36 @@ class ResultSuite extends DisciplineSuite { implicit val grackleLawsCogenForProblem: Cogen[Problem] = Cogen[String].contramap(_.message) - implicit def grackleArbitraryFnForResult[T](implicit arbF: Arbitrary[T => T]): Arbitrary[Result[T] => Result[T]] = + implicit def grackleArbitraryFnForResult[T]( + implicit arbF: Arbitrary[T => T]): Arbitrary[Result[T] => Result[T]] = Arbitrary(arbF.arbitrary.map(f => (r: Result[T]) => r.map(f))) - implicit def grackleLawsArbitraryForResult[T](implicit T: Arbitrary[T]): Arbitrary[Result[T]] = + implicit def grackleLawsArbitraryForResult[T]( + implicit T: Arbitrary[T]): Arbitrary[Result[T]] = Arbitrary( Gen.oneOf( T.arbitrary.map(Result.Success(_)), for { ps <- getArbitrary[NonEmptyChain[Problem]] - t <- T.arbitrary + t <- T.arbitrary } yield Result.Warning(ps, t), getArbitrary[NonEmptyChain[Problem]].map(Result.Failure(_)), getArbitrary[Throwable].map(Result.InternalError(_)) ) ) - checkAll("MonadError[Result] @ Int", MonadErrorTests[Result, Either[Throwable, NonEmptyChain[Problem]]].monadError[Int, Int, Int]) + checkAll( + "MonadError[Result] @ Int", + MonadErrorTests[Result, Either[Throwable, NonEmptyChain[Problem]]] + .monadError[Int, Int, Int]) - checkAll("Traverse[Result] @ Int with Option", TraverseTests[Result].traverse[Int, Int, Int, Int, Option, Option]) + checkAll( + "Traverse[Result] @ Int with Option", + TraverseTests[Result].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Parallel[Result] @ Int", ParallelTests[Result].parallel[Either[Throwable, NonEmptyChain[Problem]], Int]) + checkAll( + "Parallel[Result] @ Int", + ParallelTests[Result].parallel[Either[Throwable, NonEmptyChain[Problem]], Int]) checkAll("Semigroup[Result[List[T: Semigroup]]]", SemigroupTests[Result[List[Int]]].semigroup) diff --git a/modules/core/src/test/scala/mapping/MappingValidatorSuite.scala b/modules/core/src/test/scala/mapping/MappingValidatorSuite.scala index 1a254cf0..905258de 100644 --- a/modules/core/src/test/scala/mapping/MappingValidatorSuite.scala +++ b/modules/core/src/test/scala/mapping/MappingValidatorSuite.scala @@ -16,13 +16,12 @@ package validator import cats.syntax.all._ +import compiler.TestMapping import munit.CatsEffectSuite import grackle.{Path, ValidationException} import grackle.syntax._ -import compiler.TestMapping - final class ValidatorSuite extends CatsEffectSuite { test("missing type mapping") { @@ -263,14 +262,14 @@ final class ValidatorSuite extends CatsEffectSuite { CursorField[String]("baz", _ => ???, Nil) ) ), - ObjectMapping( - schema.ref("Baz"), - List( - CursorField[String]("quux", _ => ???, Nil) - ) - ), - LeafMapping[String](schema.ref("Foo")) - ) + ObjectMapping( + schema.ref("Baz"), + List( + CursorField[String]("quux", _ => ???, Nil) + ) + ), + LeafMapping[String](schema.ref("Foo")) + ) } val es = M.validate() @@ -345,7 +344,6 @@ final class ValidatorSuite extends CatsEffectSuite { } - test("nonexistent type (type mapping)") { object M extends TestMapping { @@ -408,7 +406,7 @@ final class ValidatorSuite extends CatsEffectSuite { schema.ref("Foo"), List( CursorField[String]("bar", _ => ???, Nil), - CursorField[String]("quz", _ => ???, Nil), + CursorField[String]("quz", _ => ???, Nil) ) ) ) @@ -448,8 +446,8 @@ final class ValidatorSuite extends CatsEffectSuite { schema.ref("Foo"), List( CursorField[String]("bar", _ => ???, Nil), - CursorField[String]("baz", _ => ???, Nil, hidden = true), - ), + CursorField[String]("baz", _ => ???, Nil, hidden = true) + ) ) ) } @@ -487,8 +485,8 @@ final class ValidatorSuite extends CatsEffectSuite { ObjectMapping( schema.ref("Foo"), List( - CursorField[String]("bar", _ => ???, Nil, hidden = true), - ), + CursorField[String]("bar", _ => ???, Nil, hidden = true) + ) ) ) } @@ -500,7 +498,6 @@ final class ValidatorSuite extends CatsEffectSuite { } } - test("unsafeValidate") { object M extends TestMapping { val schema = diff --git a/modules/core/src/test/scala/minimizer/MinimizerSuite.scala b/modules/core/src/test/scala/minimizer/MinimizerSuite.scala index 939b694b..1f7ac480 100644 --- a/modules/core/src/test/scala/minimizer/MinimizerSuite.scala +++ b/modules/core/src/test/scala/minimizer/MinimizerSuite.scala @@ -17,22 +17,23 @@ package minimizer import munit.CatsEffectSuite -import grackle.{ GraphQLParser, QueryMinimizer, Result } +import grackle.{GraphQLParser, QueryMinimizer, Result} final class MinimizerSuite extends CatsEffectSuite { val parser = GraphQLParser(GraphQLParser.defaultConfig) val minimizer = QueryMinimizer(parser) - def run(query: String, expected: String, echo: Boolean = false)(implicit loc: munit.Location): Unit = { + def run(query: String, expected: String, echo: Boolean = false)( + implicit loc: munit.Location): Unit = { - val Result.Success(minimized) = minimizer.minimizeText(query) : @unchecked + val Result.Success(minimized) = minimizer.minimizeText(query): @unchecked if (echo) println(minimized) assertNoDiff(minimized, expected) - val Some(parsed0) = parser.parseText(query).toOption : @unchecked - val Some(parsed1) = parser.parseText(minimized).toOption : @unchecked + val Some(parsed0) = parser.parseText(query).toOption: @unchecked + val Some(parsed1) = parser.parseText(minimized).toOption: @unchecked assertEquals(parsed0, parsed1) } @@ -96,7 +97,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name}}}""" + val expected = + """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name}}}""" run(query, expected) } @@ -133,7 +135,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """{user(id:4){id,name,smallPic:profilePic(size:64),bigPic:profilePic(size:1024)}}""" + val expected = + """{user(id:4){id,name,smallPic:profilePic(size:64),bigPic:profilePic(size:1024)}}""" run(query, expected) } @@ -166,7 +169,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query getZuckProfile($devicePicSize:Int){user(id:4){id,name,profilePic(size:$devicePicSize)}}""" + val expected = + """query getZuckProfile($devicePicSize:Int){user(id:4){id,name,profilePic(size:$devicePicSize)}}""" run(query, expected) } @@ -192,12 +196,12 @@ final class MinimizerSuite extends CatsEffectSuite { #comment at end of document """ - val expected = """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name}}}""" + val expected = + """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name}}}""" run(query, expected) } - test("minimize simple fragment query") { val query = """ query withFragments { @@ -218,7 +222,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query withFragments{user(id:1){friends{...friendFields},mutualFriends{...friendFields}}},fragment friendFields on User{id,name,profilePic}""" + val expected = + """query withFragments{user(id:1){friends{...friendFields},mutualFriends{...friendFields}}},fragment friendFields on User{id,name,profilePic}""" run(query, expected) } @@ -247,7 +252,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query withNestedFragments{user(id:1){friends{...friendFields},mutualFriends{...friendFields}}},fragment friendFields on User{id,name,...standardProfilePic},fragment standardProfilePic on User{profilePic}""" + val expected = + """query withNestedFragments{user(id:1){friends{...friendFields},mutualFriends{...friendFields}}},fragment friendFields on User{id,name,...standardProfilePic},fragment standardProfilePic on User{profilePic}""" run(query, expected) } @@ -272,7 +278,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query FragmentTyping{profiles{id,__typename,...userFragment,...pageFragment}},fragment userFragment on User{name},fragment pageFragment on Page{title}""" + val expected = + """query FragmentTyping{profiles{id,__typename,...userFragment,...pageFragment}},fragment userFragment on User{name},fragment pageFragment on Page{title}""" run(query, expected) } @@ -292,7 +299,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query inlineFragmentTyping{profiles{id,...on User{name},...on Page{title}}}""" + val expected = + """query inlineFragmentTyping{profiles{id,...on User{name},...on Page{title}}}""" run(query, expected) } @@ -327,7 +335,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query FragmentUnionTyping{user:user(id:"1"){favourite{__typename,...userFragment,...pageFragment}},page:user(id:"2"){favourite{__typename,...userFragment,...pageFragment}}},fragment userFragment on User{id,name},fragment pageFragment on Page{id,title}""" + val expected = + """query FragmentUnionTyping{user:user(id:"1"){favourite{__typename,...userFragment,...pageFragment}},page:user(id:"2"){favourite{__typename,...userFragment,...pageFragment}}},fragment userFragment on User{id,name},fragment pageFragment on Page{id,title}""" run(query, expected) } @@ -350,7 +359,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query($yup:Boolean,$nope:Boolean){a:field@skip(if:$yup){subfieldA},b:field@skip(if:$nope){subfieldB},c:field@include(if:$yup){subfieldA},d:field@include(if:$nope){subfieldB}}""" + val expected = + """query($yup:Boolean,$nope:Boolean){a:field@skip(if:$yup){subfieldA},b:field@skip(if:$nope){subfieldB},c:field@include(if:$yup){subfieldA},d:field@include(if:$nope){subfieldB}}""" run(query, expected) } @@ -378,7 +388,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query($yup:Boolean,$nope:Boolean){a:field{...frag@skip(if:$yup)},b:field{...frag@skip(if:$nope)},c:field{...frag@include(if:$yup)},d:field{...frag@include(if:$nope)}},fragment frag on Value{subfieldA,subfieldB}""" + val expected = + """query($yup:Boolean,$nope:Boolean){a:field{...frag@skip(if:$yup)},b:field{...frag@skip(if:$nope)},c:field{...frag@include(if:$yup)},d:field{...frag@include(if:$nope)}},fragment frag on Value{subfieldA,subfieldB}""" run(query, expected) } @@ -399,7 +410,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query($yup:Boolean,$nope:Boolean){field{...frag}},fragment frag on Value{a:subfieldA@skip(if:$yup),b:subfieldB@skip(if:$nope),c:subfieldA@include(if:$yup),d:subfieldB@include(if:$nope)}""" + val expected = + """query($yup:Boolean,$nope:Boolean){field{...frag}},fragment frag on Value{a:subfieldA@skip(if:$yup),b:subfieldB@skip(if:$nope),c:subfieldA@include(if:$yup),d:subfieldB@include(if:$nope)}""" run(query, expected) } @@ -434,7 +446,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query($yup:Boolean,$nope:Boolean){a:field{...on Value@skip(if:$yup){subfieldA,subfieldB}},b:field{...on Value@skip(if:$nope){subfieldA,subfieldB}},c:field{...on Value@include(if:$yup){subfieldA,subfieldB}},d:field{...on Value@include(if:$nope){subfieldA,subfieldB}}}""" + val expected = + """query($yup:Boolean,$nope:Boolean){a:field{...on Value@skip(if:$yup){subfieldA,subfieldB}},b:field{...on Value@skip(if:$nope){subfieldA,subfieldB}},c:field{...on Value@include(if:$yup){subfieldA,subfieldB}},d:field{...on Value@include(if:$nope){subfieldA,subfieldB}}}""" run(query, expected) } @@ -453,7 +466,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query($yup:Boolean,$nope:Boolean){field{...on Value{a:subfieldA@skip(if:$yup),b:subfieldB@skip(if:$nope),c:subfieldA@include(if:$yup),d:subfieldB@include(if:$nope)}}}""" + val expected = + """query($yup:Boolean,$nope:Boolean){field{...on Value{a:subfieldA@skip(if:$yup),b:subfieldB@skip(if:$nope),c:subfieldA@include(if:$yup),d:subfieldB@include(if:$nope)}}}""" run(query, expected) } @@ -473,7 +487,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query{field{subfield},field(arg:null){subfield},field(arg:23){subfield}}""" + val expected = + """query{field{subfield},field(arg:null){subfield},field(arg:23){subfield}}""" run(query, expected) } @@ -490,7 +505,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query{listField(arg:[]){subfield},listField(arg:["foo","bar"]){subfield}}""" + val expected = + """query{listField(arg:[]){subfield},listField(arg:["foo","bar"]){subfield}}""" run(query, expected) } @@ -524,7 +540,8 @@ final class MinimizerSuite extends CatsEffectSuite { } """ - val expected = """query{movieById(id:"6a7837fc-b463-4d32-b628-0f4b3065cb21"){id,title,genre,releaseDate,showTime,nextShowing,duration}}""" + val expected = + """query{movieById(id:"6a7837fc-b463-4d32-b628-0f4b3065cb21"){id,title,genre,releaseDate,showTime,nextShowing,duration}}""" run(query, expected) } @@ -546,102 +563,103 @@ final class MinimizerSuite extends CatsEffectSuite { test("standard introspection query") { val query = """ - |query IntrospectionQuery { - | __schema { - | queryType { name } - | mutationType { name } - | subscriptionType { name } - | types { - | ...FullType - | } - | directives { - | name - | description - | locations - | args { - | ...InputValue - | } - | } - | } - |} - | - |fragment FullType on __Type { - | kind - | name - | description - | fields(includeDeprecated: true) { - | name - | description - | args { - | ...InputValue - | } - | type { - | ...TypeRef - | } - | isDeprecated - | deprecationReason - | } - | inputFields(includeDeprecated: true) { - | ...InputValue - | } - | interfaces { - | ...TypeRef - | } - | enumValues(includeDeprecated: true) { - | name - | description - | isDeprecated - | deprecationReason - | } - | possibleTypes { - | ...TypeRef - | } - |} - | - |fragment InputValue on __InputValue { - | name - | description - | type { ...TypeRef } - | defaultValue - | isDeprecated - | deprecationReason - |} - | - |fragment TypeRef on __Type { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | ofType { - | kind - | name - | } - | } - | } - | } - | } - | } - | } - |} + |query IntrospectionQuery { + | __schema { + | queryType { name } + | mutationType { name } + | subscriptionType { name } + | types { + | ...FullType + | } + | directives { + | name + | description + | locations + | args { + | ...InputValue + | } + | } + | } + |} + | + |fragment FullType on __Type { + | kind + | name + | description + | fields(includeDeprecated: true) { + | name + | description + | args { + | ...InputValue + | } + | type { + | ...TypeRef + | } + | isDeprecated + | deprecationReason + | } + | inputFields(includeDeprecated: true) { + | ...InputValue + | } + | interfaces { + | ...TypeRef + | } + | enumValues(includeDeprecated: true) { + | name + | description + | isDeprecated + | deprecationReason + | } + | possibleTypes { + | ...TypeRef + | } + |} + | + |fragment InputValue on __InputValue { + | name + | description + | type { ...TypeRef } + | defaultValue + | isDeprecated + | deprecationReason + |} + | + |fragment TypeRef on __Type { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | ofType { + | kind + | name + | } + | } + | } + | } + | } + | } + | } + |} """.stripMargin.trim - val expected = """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name},types{...FullType},directives{name,description,locations,args{...InputValue}}}},fragment FullType on __Type{kind,name,description,fields(includeDeprecated:true){name,description,args{...InputValue},type{...TypeRef},isDeprecated,deprecationReason},inputFields(includeDeprecated:true){...InputValue},interfaces{...TypeRef},enumValues(includeDeprecated:true){name,description,isDeprecated,deprecationReason},possibleTypes{...TypeRef}},fragment InputValue on __InputValue{name,description,type{...TypeRef},defaultValue,isDeprecated,deprecationReason},fragment TypeRef on __Type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}}""" + val expected = + """query IntrospectionQuery{__schema{queryType{name},mutationType{name},subscriptionType{name},types{...FullType},directives{name,description,locations,args{...InputValue}}}},fragment FullType on __Type{kind,name,description,fields(includeDeprecated:true){name,description,args{...InputValue},type{...TypeRef},isDeprecated,deprecationReason},inputFields(includeDeprecated:true){...InputValue},interfaces{...TypeRef},enumValues(includeDeprecated:true){name,description,isDeprecated,deprecationReason},possibleTypes{...TypeRef}},fragment InputValue on __InputValue{name,description,type{...TypeRef},defaultValue,isDeprecated,deprecationReason},fragment TypeRef on __Type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}}""" run(query, expected) } diff --git a/modules/core/src/test/scala/parser/ParserSuite.scala b/modules/core/src/test/scala/parser/ParserSuite.scala index 0f853e2a..384c5fa8 100644 --- a/modules/core/src/test/scala/parser/ParserSuite.scala +++ b/modules/core/src/test/scala/parser/ParserSuite.scala @@ -17,9 +17,14 @@ package parser import munit.CatsEffectSuite -import grackle.{Ast, GraphQLParser, Result} +import grackle.{GraphQLParser, Result} +import grackle.Ast._ +import grackle.Ast.OperationDefinition._ +import grackle.Ast.OperationType._ +import grackle.Ast.Selection._ +import grackle.Ast.Type.Named +import grackle.Ast.Value._ import grackle.syntax._ -import Ast._, OperationType._, OperationDefinition._, Selection._, Value._, Type.Named final class ParserSuite extends CatsEffectSuite { val parser = mkParser() @@ -34,9 +39,17 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query, None, Nil, Nil, + Operation( + Query, + None, + Nil, + Nil, List( - Field(None, Name("character"), List((Name("id"), IntValue(1000))), Nil, + Field( + None, + Name("character"), + List((Name("id"), IntValue(1000))), + Nil, List( Field(None, Name("name"), Nil, Nil, Nil) ) @@ -58,16 +71,22 @@ final class ParserSuite extends CatsEffectSuite { val expected = Operation( - Query,None,Nil,Nil, + Query, + None, + Nil, + Nil, List( - Field(None,Name("wibble"), + Field( + None, + Name("wibble"), List( - (Name("foo"),StringValue("a")), - (Name("bar"),StringValue("b")), - (Name("baz"),IntValue(3)) - ), Nil, + (Name("foo"), StringValue("a")), + (Name("bar"), StringValue("b")), + (Name("baz"), IntValue(3)) + ), + Nil, List( - Field(None,Name("quux"),Nil,Nil,Nil) + Field(None, Name("quux"), Nil, Nil, Nil) ) ) ) @@ -90,16 +109,22 @@ final class ParserSuite extends CatsEffectSuite { val expected = Operation( - Query,None,Nil,Nil, + Query, + None, + Nil, + Nil, List( - Field(None,Name("wibble"), + Field( + None, + Name("wibble"), List( - (Name("foo"),StringValue("a")), - (Name("bar"),StringValue("b")), - (Name("baz"),IntValue(3)) - ), Nil, + (Name("foo"), StringValue("a")), + (Name("bar"), StringValue("b")), + (Name("baz"), IntValue(3)) + ), + Nil, List( - Field(None,Name("quux"),Nil,Nil,Nil) + Field(None, Name("quux"), Nil, Nil, Nil) ) ) ) @@ -129,23 +154,43 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query,Some(Name("IntrospectionQuery")),Nil,Nil, + Operation( + Query, + Some(Name("IntrospectionQuery")), + Nil, + Nil, List( - Field(None,Name("__schema"),Nil,Nil, + Field( + None, + Name("__schema"), + Nil, + Nil, List( - Field(None,Name("queryType"),Nil,Nil, + Field( + None, + Name("queryType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ), - Field(None,Name("mutationType"),Nil,Nil, + Field( + None, + Name("mutationType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ), - Field(None,Name("subscriptionType"),Nil,Nil, + Field( + None, + Name("subscriptionType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ) ) @@ -177,15 +222,27 @@ final class ParserSuite extends CatsEffectSuite { val expected = QueryShorthand( List( - Field(None,Name("hero"),List((Name("episode"),EnumValue(Name("NEWHOPE")))),Nil, + Field( + None, + Name("hero"), + List((Name("episode"), EnumValue(Name("NEWHOPE")))), + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil), - Field(None,Name("friends"),Nil,Nil, + Field(None, Name("name"), Nil, Nil, Nil), + Field( + None, + Name("friends"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil), - Field(None,Name("friends"),Nil,Nil, + Field(None, Name("name"), Nil, Nil, Nil), + Field( + None, + Name("friends"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ) ) @@ -216,12 +273,26 @@ final class ParserSuite extends CatsEffectSuite { val expected = QueryShorthand( List( - Field(None, Name("user"), List((Name("id"), IntValue(4))), Nil, + Field( + None, + Name("user"), + List((Name("id"), IntValue(4))), + Nil, List( Field(None, Name("id"), Nil, Nil, Nil), Field(None, Name("name"), Nil, Nil, Nil), - Field(Some(Name("smallPic")), Name("profilePic"), List((Name("size"), IntValue(64))), Nil, Nil), - Field(Some(Name("bigPic")), Name("profilePic"), List((Name("size"), IntValue(1024))), Nil, Nil) + Field( + Some(Name("smallPic")), + Name("profilePic"), + List((Name("size"), IntValue(64))), + Nil, + Nil), + Field( + Some(Name("bigPic")), + Name("profilePic"), + List((Name("size"), IntValue(1024))), + Nil, + Nil) ) ) ) @@ -248,10 +319,17 @@ final class ParserSuite extends CatsEffectSuite { val expected = QueryShorthand( List( - Field(Some(Name("luke")), Name("character"), List((Name("id"), StringValue("1000"))), Nil, - List( - Field(None, Name("name"), Nil, Nil, Nil))), - Field(Some(Name("darth")), Name("character"), List((Name("id"), StringValue("1001"))), Nil, + Field( + Some(Name("luke")), + Name("character"), + List((Name("id"), StringValue("1000"))), + Nil, + List(Field(None, Name("name"), Nil, Nil, Nil))), + Field( + Some(Name("darth")), + Name("character"), + List((Name("id"), StringValue("1001"))), + Nil, List( Field(None, Name("name"), Nil, Nil, Nil) ) @@ -277,15 +355,26 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query, Some(Name("getZuckProfile")), + Operation( + Query, + Some(Name("getZuckProfile")), List(VariableDefinition(Name("devicePicSize"), Named(Name("Int")), None, Nil)), Nil, List( - Field(None, Name("user"), List((Name("id"), IntValue(4))), Nil, + Field( + None, + Name("user"), + List((Name("id"), IntValue(4))), + Nil, List( Field(None, Name("id"), Nil, Nil, Nil), Field(None, Name("name"), Nil, Nil, Nil), - Field(None, Name("profilePic"), List((Name("size"), Variable(Name("devicePicSize")))), Nil, Nil) + Field( + None, + Name("profilePic"), + List((Name("size"), Variable(Name("devicePicSize")))), + Nil, + Nil) ) ) ) @@ -309,15 +398,31 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query, Some(Name("getZuckProfile")), - List(VariableDefinition(Name("devicePicSize"), Named(Name("Int")), Some(IntValue(10)), Nil)), + Operation( + Query, + Some(Name("getZuckProfile")), + List( + VariableDefinition( + Name("devicePicSize"), + Named(Name("Int")), + Some(IntValue(10)), + Nil)), Nil, List( - Field(None, Name("user"), List((Name("id"), IntValue(4))), Nil, + Field( + None, + Name("user"), + List((Name("id"), IntValue(4))), + Nil, List( Field(None, Name("id"), Nil, Nil, Nil), Field(None, Name("name"), Nil, Nil, Nil), - Field(None, Name("profilePic"), List((Name("size"), Variable(Name("devicePicSize")))), Nil, Nil) + Field( + None, + Name("profilePic"), + List((Name("size"), Variable(Name("devicePicSize")))), + Nil, + Nil) ) ) ) @@ -341,15 +446,31 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query, Some(Name("getZuckProfile")), - List(VariableDefinition(Name("devicePicSize"), Named(Name("Int")), None, List(Directive(Name("dir"), Nil)))), + Operation( + Query, + Some(Name("getZuckProfile")), + List( + VariableDefinition( + Name("devicePicSize"), + Named(Name("Int")), + None, + List(Directive(Name("dir"), Nil)))), Nil, List( - Field(None, Name("user"), List((Name("id"), IntValue(4))), Nil, + Field( + None, + Name("user"), + List((Name("id"), IntValue(4))), + Nil, List( Field(None, Name("id"), Nil, Nil, Nil), Field(None, Name("name"), Nil, Nil, Nil), - Field(None, Name("profilePic"), List((Name("size"), Variable(Name("devicePicSize")))), Nil, Nil) + Field( + None, + Name("profilePic"), + List((Name("size"), Variable(Name("devicePicSize")))), + Nil, + Nil) ) ) ) @@ -383,23 +504,43 @@ final class ParserSuite extends CatsEffectSuite { """ val expected = - Operation(Query,Some(Name("IntrospectionQuery")),Nil,Nil, + Operation( + Query, + Some(Name("IntrospectionQuery")), + Nil, + Nil, List( - Field(None,Name("__schema"),Nil,Nil, + Field( + None, + Name("__schema"), + Nil, + Nil, List( - Field(None,Name("queryType"),Nil,Nil, + Field( + None, + Name("queryType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ), - Field(None,Name("mutationType"),Nil,Nil, + Field( + None, + Name("mutationType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ), - Field(None,Name("subscriptionType"),Nil,Nil, + Field( + None, + Name("subscriptionType"), + Nil, + Nil, List( - Field(None,Name("name"),Nil,Nil,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ) ) @@ -416,7 +557,7 @@ final class ParserSuite extends CatsEffectSuite { test("invalid document") { parser.parseText("scalar Foo woozle").toOption match { case Some(_) => fail("should have failed") - case None => () + case None => () } } @@ -438,16 +579,24 @@ final class ParserSuite extends CatsEffectSuite { val expected = List( - Operation(Query, None, Nil, Nil, + Operation( + Query, + None, + Nil, + Nil, List( - Field(None, Name("character"), List((Name("id"), IntValue(1000))), Nil, + Field( + None, + Name("character"), + List((Name("id"), IntValue(1000))), + Nil, List( - FragmentSpread(Name("frag"),Nil), + FragmentSpread(Name("frag"), Nil), InlineFragment( Some(Named(Name("Character"))), Nil, List( - Field(None,Name("age"),Nil ,Nil ,Nil) + Field(None, Name("age"), Nil, Nil, Nil) ) ) ) @@ -459,7 +608,7 @@ final class ParserSuite extends CatsEffectSuite { Named(Name("Character")), Nil, List( - Field(None,Name("name"),Nil ,Nil ,Nil) + Field(None, Name("name"), Nil, Nil, Nil) ) ) ) @@ -487,16 +636,21 @@ final class ParserSuite extends CatsEffectSuite { Operation( Query, Some(Name("frag")), - List(VariableDefinition(Name("expanded"),Named(Name("Boolean")),None, Nil)), + List(VariableDefinition(Name("expanded"), Named(Name("Boolean")), None, Nil)), Nil, List( - Field(None, Name("character"), List((Name("id"), IntValue(1000))), Nil, + Field( + None, + Name("character"), + List((Name("id"), IntValue(1000))), + Nil, List( Field(None, Name("name"), Nil, Nil, Nil), InlineFragment( None, - List(Directive(Name("include"),List((Name("if"),Variable(Name("expanded")))))), - List(Field(None,Name("age"),List(),List(),List())) + List( + Directive(Name("include"), List((Name("if"), Variable(Name("expanded")))))), + List(Field(None, Name("age"), List(), List(), List())) ) ) ) @@ -528,13 +682,17 @@ final class ParserSuite extends CatsEffectSuite { Nil, Nil, List( - Field(None, Name("character"), List((Name("id"), IntValue(1000))), Nil, + Field( + None, + Name("character"), + List((Name("id"), IntValue(1000))), + Nil, List( Field(None, Name("name"), Nil, Nil, Nil), InlineFragment( None, List(Directive(Name("dir"), Nil)), - List(Field(None,Name("age"),List(),List(),List())) + List(Field(None, Name("age"), List(), List(), List())) ) ) ) @@ -551,7 +709,7 @@ final class ParserSuite extends CatsEffectSuite { def assertParse(input: String, expected: Value) = parser.parseText(s"query { foo(bar: $input) }").toOption match { - case Some(List(Operation(_, _, _, _,List(Field(_, _, List((_, v)), _, _))))) => + case Some(List(Operation(_, _, _, _, List(Field(_, _, List((_, v)), _, _))))) => assertEquals(v, expected) case _ => assert(false) } @@ -561,14 +719,14 @@ final class ParserSuite extends CatsEffectSuite { assertParse("\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"", StringValue("\" \\ / \b \f \n \r \t")) assertParse("123.2", FloatValue(123.2d)) - assertParse("123E2", FloatValue(123E2d)) - assertParse("123.2E2", FloatValue(123.2E2d)) + assertParse("123E2", FloatValue(123e2d)) + assertParse("123.2E2", FloatValue(123.2e2d)) // Negative values whose integer part is zero must retain their sign. assertParse("-0.99", FloatValue(-0.99d)) assertParse("-0.5", FloatValue(-0.5d)) assertParse("-1.5", FloatValue(-1.5d)) - assertParse("-0.5E2", FloatValue(-0.5E2d)) + assertParse("-0.5E2", FloatValue(-0.5e2d)) assertParse("123", IntValue(123)) assertParse("-123", IntValue(-123)) @@ -582,10 +740,14 @@ final class ParserSuite extends CatsEffectSuite { assertParse("[1, \"foo\"]", ListValue(List(IntValue(1), StringValue("foo")))) - assertParse("{foo: 1, bar: \"baz\"}", ObjectValue(List(Name("foo") -> IntValue(1), Name("bar") -> StringValue("baz")))) + assertParse( + "{foo: 1, bar: \"baz\"}", + ObjectValue(List(Name("foo") -> IntValue(1), Name("bar") -> StringValue("baz")))) assertParse("\"\"\"one\"\"\"", StringValue("one")) - assertParse("\"\"\" \n\n first\n \tλ\n 123\n\n\n \t\n\n\"\"\"", StringValue(" first\n \tλ\n123")) + assertParse( + "\"\"\" \n\n first\n \tλ\n 123\n\n\n \t\n\n\"\"\"", + StringValue(" first\n \tλ\n123")) } test("outsized int") { @@ -623,7 +785,11 @@ final class ParserSuite extends CatsEffectSuite { val expected = List( - ObjectTypeExtension(Named(Name("Foo")), List(FieldDefinition(Name("bar"),None,Nil,Named(Name("Int")),Nil)), Nil, Nil) + ObjectTypeExtension( + Named(Name("Foo")), + List(FieldDefinition(Name("bar"), None, Nil, Named(Name("Int")), Nil)), + Nil, + Nil) ) val res = parser.parseText(schema).toOption @@ -639,7 +805,9 @@ final class ParserSuite extends CatsEffectSuite { val expected = List( - SchemaExtension(List(RootOperationTypeDefinition(OperationType.Query, Named(Name("Query")), Nil)), Nil) + SchemaExtension( + List(RootOperationTypeDefinition(OperationType.Query, Named(Name("Query")), Nil)), + Nil) ) val res = parser.parseText(schema).toOption @@ -689,7 +857,7 @@ final class ParserSuite extends CatsEffectSuite { test("deep query") { def mkQuery(depth: Int): String = { val depth0 = depth - 1 - "query{" + ("f{" *depth0) + "f" + ("}" * depth0) + "}" + "query{" + ("f{" * depth0) + "f" + ("}" * depth0) + "}" } val limit = 5 @@ -736,7 +904,7 @@ final class ParserSuite extends CatsEffectSuite { test("deep list value") { def mkQuery(depth: Int): String = - "query{f(l: " + ("[" *depth) + "0" + ("]" * depth) + "){f}}" + "query{f(l: " + ("[" * depth) + "0" + ("]" * depth) + "){f}}" val limit = 5 val limitedParser = mkParser(maxInputValueDepth = limit) @@ -759,7 +927,7 @@ final class ParserSuite extends CatsEffectSuite { test("deep input object value") { def mkQuery(depth: Int): String = - "query{f(l: " + ("{m:" *depth) + "0" + ("}" * depth) + "){f}}" + "query{f(l: " + ("{m:" * depth) + "0" + ("}" * depth) + "){f}}" val limit = 5 val limitedParser = mkParser(maxInputValueDepth = limit) @@ -782,7 +950,7 @@ final class ParserSuite extends CatsEffectSuite { test("deep variable type") { def mkQuery(depth: Int): String = - "query($l: " + ("[" *depth) + "Int" + ("]" * depth) + "){f(a:$l)}" + "query($l: " + ("[" * depth) + "Int" + ("]" * depth) + "){f(a:$l)}" val limit = 5 val limitedParser = mkParser(maxListTypeDepth = limit) @@ -804,10 +972,10 @@ final class ParserSuite extends CatsEffectSuite { } def mkParser( - maxSelectionDepth: Int = GraphQLParser.defaultConfig.maxSelectionDepth, - maxSelectionWidth: Int = GraphQLParser.defaultConfig.maxSelectionWidth, - maxInputValueDepth: Int = GraphQLParser.defaultConfig.maxInputValueDepth, - maxListTypeDepth: Int = GraphQLParser.defaultConfig.maxListTypeDepth, + maxSelectionDepth: Int = GraphQLParser.defaultConfig.maxSelectionDepth, + maxSelectionWidth: Int = GraphQLParser.defaultConfig.maxSelectionWidth, + maxInputValueDepth: Int = GraphQLParser.defaultConfig.maxInputValueDepth, + maxListTypeDepth: Int = GraphQLParser.defaultConfig.maxListTypeDepth ): GraphQLParser = GraphQLParser( GraphQLParser.Config( diff --git a/modules/core/src/test/scala/schema/SchemaSuite.scala b/modules/core/src/test/scala/schema/SchemaSuite.scala index 389c459d..dfdf5559 100644 --- a/modules/core/src/test/scala/schema/SchemaSuite.scala +++ b/modules/core/src/test/scala/schema/SchemaSuite.scala @@ -18,7 +18,7 @@ package schema import cats.data.NonEmptyChain import munit.CatsEffectSuite -import grackle.{Result, Schema, ScalarType} +import grackle.{Result, ScalarType, Schema} import grackle.syntax._ final class SchemaSuite extends CatsEffectSuite { @@ -37,7 +37,8 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Reference to undefined type 'Episod'")) + case Result.Failure(ps) => + assertEquals(ps.map(_.message), NonEmptyChain("Reference to undefined type 'Episod'")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -58,7 +59,7 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => + case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Reference to undefined type 'CCid'")) case unexpected => fail(s"This was unexpected: $unexpected") } @@ -82,7 +83,10 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Duplicate definition of type 'Episode' found")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain("Duplicate definition of type 'Episode' found")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -97,12 +101,14 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Directive 'deprecated' may not occur more than once")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain("Directive 'deprecated' may not occur more than once")) case unexpected => fail(s"This was unexpected: $unexpected") } } - test("schema validation: deprecated annotation with unsupported argument") { val schema = Schema( """ @@ -113,7 +119,10 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Unknown argument(s) 'notareason' in directive deprecated")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain("Unknown argument(s) 'notareason' in directive deprecated")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -129,7 +138,10 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => assertEquals(ps.map(_.message), NonEmptyChain("Duplicate definition of enum value 'NORTH' for type 'Direction'")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain("Duplicate definition of enum value 'NORTH' for type 'Direction'")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -202,7 +214,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'id' from interface 'Character' is not defined by implementing type 'Human'", "Field 'email' from interface 'Character' is not defined by implementing type 'Human'" @@ -229,7 +242,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'id' from interface 'Character' is not defined by implementing type 'Named'", "Field 'email' from interface 'Character' is not defined by implementing type 'Named'" @@ -254,7 +268,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'name' of type 'Human' has type 'Int!', however implemented interface 'Character' requires it to be a subtype of 'String!'" ) @@ -278,7 +293,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'name' of type 'Human' has has an argument list that does not conform to that specified by implemented interface 'Character'" ) @@ -307,7 +323,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'id' from interface 'Character' is not defined by implementing type 'Human'", "Field 'id' from interface 'Character' is not defined by implementing type 'Dog'" @@ -337,7 +354,8 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), + assertEquals( + ps.map(_.message), NonEmptyChain( "Field 'id' from interface 'Character' is not defined by implementing type 'Human'", "Field 'email' from interface 'Contactable' is not defined by implementing type 'Human'" @@ -367,7 +385,8 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Success(a) => assertEquals(a.types.map(_.name), List("Node", "Resource", "Human")) + case Result.Success(a) => + assertEquals(a.types.map(_.name), List("Node", "Resource", "Human")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -393,7 +412,10 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), NonEmptyChain("Type 'Human' does not directly implement transitively implemented interface 'Node'")) + assertEquals( + ps.map(_.message), + NonEmptyChain( + "Type 'Human' does not directly implement transitively implemented interface 'Node'")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -418,7 +440,9 @@ final class SchemaSuite extends CatsEffectSuite { schema match { case Result.Failure(ps) => - assertEquals(ps.map(_.message), NonEmptyChain("Non-interface type 'Foo' declared as implemented by type 'Bar'")) + assertEquals( + ps.map(_.message), + NonEmptyChain("Non-interface type 'Foo' declared as implemented by type 'Bar'")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -506,8 +530,8 @@ final class SchemaSuite extends CatsEffectSuite { } """ - assert(schema.queryType =:= schema.ref("MyQuery")) - assert(schema.mutationType.exists(_ =:= schema.ref("MyMutation"))) + assert(schema.queryType =:= schema.ref("MyQuery")) + assert(schema.mutationType.exists(_ =:= schema.ref("MyMutation"))) assert(schema.subscriptionType.exists(_ =:= schema.ref("MySubscription"))) } @@ -530,7 +554,7 @@ final class SchemaSuite extends CatsEffectSuite { } """ - assert(schema.queryType =:= schema.ref("MyQuery")) + assert(schema.queryType =:= schema.ref("MyQuery")) assert(schema.mutationType.exists(_ =:= schema.ref("MyMutation"))) assertEquals(schema.subscriptionType, None) @@ -553,8 +577,8 @@ final class SchemaSuite extends CatsEffectSuite { } """ - assert(schema.queryType =:= schema.ref("Query")) - assert(schema.mutationType.exists(_ =:= schema.ref("Mutation"))) + assert(schema.queryType =:= schema.ref("Query")) + assert(schema.mutationType.exists(_ =:= schema.ref("Mutation"))) assert(schema.subscriptionType.exists(_ =:= schema.ref("Subscription"))) } @@ -584,7 +608,10 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Success(s) => assertEquals(s.types.map(_.name), List("Query", "Edge", "Connection", "MyEdge", "MyConnection")) + case Result.Success(s) => + assertEquals( + s.types.map(_.name), + List("Query", "Edge", "Connection", "MyEdge", "MyConnection")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -608,8 +635,12 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => - assertEquals(ps.map(_.message), NonEmptyChain("Reference to undefined type 'Long'", "Reference to undefined type 'Long'")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain( + "Reference to undefined type 'Long'", + "Reference to undefined type 'Long'")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -662,8 +693,10 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => - assertEquals(ps.map(_.message), NonEmptyChain("Value of type String required for 'url' in directive specifiedBy")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain("Value of type String required for 'url' in directive specifiedBy")) case unexpected => fail(s"This was unexpected: $unexpected") } } @@ -697,8 +730,11 @@ final class SchemaSuite extends CatsEffectSuite { ) schema match { - case Result.Failure(ps) => - assertEquals(ps.map(_.message), NonEmptyChain("oneOf input object type ExampleInput may not have non-nullable field(s): 'foo', 'baz'")) + case Result.Failure(ps) => + assertEquals( + ps.map(_.message), + NonEmptyChain( + "oneOf input object type ExampleInput may not have non-nullable field(s): 'foo', 'baz'")) case unexpected => fail(s"This was unexpected: $unexpected") } } diff --git a/modules/core/src/test/scala/sdl/SDLSuite.scala b/modules/core/src/test/scala/sdl/SDLSuite.scala index 6198b37b..d69cb75e 100644 --- a/modules/core/src/test/scala/sdl/SDLSuite.scala +++ b/modules/core/src/test/scala/sdl/SDLSuite.scala @@ -17,9 +17,11 @@ package sdl import munit.CatsEffectSuite -import grackle.{ Ast, GraphQLParser, SchemaParser } +import grackle.{GraphQLParser, SchemaParser} +import grackle.Ast._ +import grackle.Ast.OperationType._ +import grackle.Ast.Type.{List => _, _} import grackle.syntax._ -import Ast._, OperationType._, Type.{ List => _, _ } final class SDLSuite extends CatsEffectSuite { val parser = GraphQLParser(GraphQLParser.defaultConfig) @@ -63,7 +65,10 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( ScalarTypeDefinition(Name("Url"), Some("A scalar type"), Nil), - ScalarTypeDefinition(Name("Time"), Some("A scalar type"), List(Directive(Name("deprecated"), Nil))) + ScalarTypeDefinition( + Name("Time"), + Some("A scalar type"), + List(Directive(Name("deprecated"), Nil))) ) val res = parser.parseText(schema) @@ -82,13 +87,21 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( - ObjectTypeDefinition(Name("Query"), Some("An object type"), + ObjectTypeDefinition( + Name("Query"), + Some("An object type"), List( FieldDefinition(Name("posts"), None, Nil, Type.List(Named(Name("Post"))), Nil), FieldDefinition( Name("author"), None, - List(InputValueDefinition(Name("id"), None, NonNull(Left(Named(Name("Int")))), None, Nil)), + List( + InputValueDefinition( + Name("id"), + None, + NonNull(Left(Named(Name("Int")))), + None, + Nil)), Named(Name("Author")), Nil ) @@ -118,11 +131,23 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( - InterfaceTypeDefinition(Name("Post"), Some("An interface type"), + InterfaceTypeDefinition( + Name("Post"), + Some("An interface type"), List( - FieldDefinition(Name("id"), Some("A field"), Nil, NonNull(Left(Named(Name("Int")))), Nil), + FieldDefinition( + Name("id"), + Some("A field"), + Nil, + NonNull(Left(Named(Name("Int")))), + Nil), FieldDefinition(Name("title"), None, Nil, Named(Name("String")), Nil), - FieldDefinition(Name("author"), Some("A deprecated field"), Nil, Named(Name("Author")), List(Directive(Name("deprecated"), Nil))), + FieldDefinition( + Name("author"), + Some("A deprecated field"), + Nil, + Named(Name("Author")), + List(Directive(Name("deprecated"), Nil))), FieldDefinition(Name("votes"), None, Nil, Named(Name("Int")), Nil) ), Nil, @@ -143,7 +168,10 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( - UnionTypeDefinition(Name("ThisOrThat"), Some("A union type"), Nil, + UnionTypeDefinition( + Name("ThisOrThat"), + Some("A union type"), + Nil, List( Named(Name("This")), Named(Name("That")) @@ -169,7 +197,10 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( - EnumTypeDefinition(Name("Direction"), Some("An enum type"), Nil, + EnumTypeDefinition( + Name("Direction"), + Some("An enum type"), + Nil, List( EnumValueDefinition(Name("NORTH"), None, Nil), EnumValueDefinition(Name("EAST"), None, Nil), @@ -195,7 +226,9 @@ final class SDLSuite extends CatsEffectSuite { val expected = List( - InputObjectTypeDefinition(Name("Point2D"), Some("An input object type"), + InputObjectTypeDefinition( + Name("Point2D"), + Some("An input object type"), List( InputValueDefinition(Name("x"), None, Named(Name("Float")), None, Nil), InputValueDefinition(Name("y"), None, Named(Name("Float")), None, Nil) @@ -226,22 +259,22 @@ final class SDLSuite extends CatsEffectSuite { test("deserialize schema (1)") { val schema = - """|type Author { - | id: Int! - | firstName: String - | lastName: String - | posts: [Post] - |} - |type Post { - | id: Int! - | title: String - | author: Author - | votes: Int - |} - |type Query { - | posts: [Post] - | author(id: Int! = 23): Author - |}""".stripMargin + """|type Author { + | id: Int! + | firstName: String + | lastName: String + | posts: [Post] + |} + |type Post { + | id: Int! + | title: String + | author: Author + | votes: Int + |} + |type Query { + | posts: [Post] + | author(id: Int! = 23): Author + |}""".stripMargin val res = schemaParser.parseText(schema) val ser = res.map(_.toString) @@ -251,41 +284,41 @@ final class SDLSuite extends CatsEffectSuite { test("deserialize schema (2)") { val schema = - """|type Query { - | hero(episode: Episode!): Character! - | character(id: ID!): Character - | human(id: ID!): Human - | droid(id: ID!): Droid - |} - |enum Episode { - | ROGUEONE @deprecated(reason: "use NEWHOPE instead") - | NEWHOPE - | EMPIRE - | JEDI - |} - |interface Character { - | id: String! - | name: String - | fullname: String @deprecated(reason: "use 'name' instead") - | friends: [Character!] - | appearsIn: [Episode!] - |} - |type Human implements Character { - | id: String! - | name: String - | fullname: String @deprecated(reason: "use 'name' instead") - | friends: [Character!] - | appearsIn: [Episode!] - | homePlanet: String - |} - |type Droid implements Character { - | id: String! - | name: String - | fullname: String @deprecated(reason: "use 'name' instead") - | friends: [Character!] - | appearsIn: [Episode!] - | primaryFunction: String - |}""".stripMargin + """|type Query { + | hero(episode: Episode!): Character! + | character(id: ID!): Character + | human(id: ID!): Human + | droid(id: ID!): Droid + |} + |enum Episode { + | ROGUEONE @deprecated(reason: "use NEWHOPE instead") + | NEWHOPE + | EMPIRE + | JEDI + |} + |interface Character { + | id: String! + | name: String + | fullname: String @deprecated(reason: "use 'name' instead") + | friends: [Character!] + | appearsIn: [Episode!] + |} + |type Human implements Character { + | id: String! + | name: String + | fullname: String @deprecated(reason: "use 'name' instead") + | friends: [Character!] + | appearsIn: [Episode!] + | homePlanet: String + |} + |type Droid implements Character { + | id: String! + | name: String + | fullname: String @deprecated(reason: "use 'name' instead") + | friends: [Character!] + | appearsIn: [Episode!] + | primaryFunction: String + |}""".stripMargin val res = schemaParser.parseText(schema) val ser = res.map(_.toString) @@ -295,20 +328,20 @@ final class SDLSuite extends CatsEffectSuite { test("deserialize schema (3)") { val schema = - """|type Query { - | whatsat(p: Point!): ThisOrThat! - |} - |union ThisOrThat = This | That - |type This { - | id: ID! - |} - |type That { - | id: ID! - |} - |input Point { - | x: Int - | y: Int - |}""".stripMargin + """|type Query { + | whatsat(p: Point!): ThisOrThat! + |} + |union ThisOrThat = This | That + |type This { + | id: ID! + |} + |type That { + | id: ID! + |} + |input Point { + | x: Int + | y: Int + |}""".stripMargin val res = schemaParser.parseText(schema) val ser = res.map(_.toString) diff --git a/modules/core/src/test/scala/starwars/StarWarsData.scala b/modules/core/src/test/scala/starwars/StarWarsData.scala index ca94561c..98a993f9 100644 --- a/modules/core/src/test/scala/starwars/StarWarsData.scala +++ b/modules/core/src/test/scala/starwars/StarWarsData.scala @@ -20,12 +20,12 @@ import cats.implicits._ import io.circe.Encoder import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ - object StarWarsData { object Episode extends Enumeration { val NEWHOPE, EMPIRE, JEDI = Value @@ -40,19 +40,19 @@ object StarWarsData { } case class Human( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - homePlanet: Option[String] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + homePlanet: Option[String] ) extends Character case class Droid( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - primaryFunction: Option[String] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + primaryFunction: Option[String] ) extends Character def resolveFriends(c: Character): Option[List[Character]] = @@ -116,8 +116,8 @@ object StarWarsData { import Episode._ - val Some(lukeSkywalker) = characters.find(_.id == "1000") : @unchecked - val Some(r2d2) = characters.find(_.id == "2001") : @unchecked + val Some(lukeSkywalker) = characters.find(_.id == "1000"): @unchecked + val Some(r2d2) = characters.find(_.id == "2001"): @unchecked val hero: Map[Value, Character] = Map( NEWHOPE -> r2d2, @@ -184,43 +184,39 @@ object StarWarsMapping extends ValueMapping[IO] { List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - ValueField("hero", _ => characters), - ValueField("character", _ => characters), - ValueField("characters", _ => characters), - ValueField("human", _ => characters.collect { case h: Human => h }), - ValueField("droid", _ => characters.collect { case d: Droid => d }) - ) + fieldMappings = List( + ValueField("hero", _ => characters), + ValueField("character", _ => characters), + ValueField("characters", _ => characters), + ValueField("human", _ => characters.collect { case h: Human => h }), + ValueField("droid", _ => characters.collect { case d: Droid => d }) + ) ), ValueObjectMapping[Character]( tpe = CharacterType, - fieldMappings = - List( - ValueField("id", _.id), - ValueField("taggedId1", c => s"character-${c.id}"), - ValueField("name", _.name), - ValueField("appearsIn", _.appearsIn), - ValueField("numberOfFriends", _ => 0), - ValueField("friends", resolveFriends _) - ) + fieldMappings = List( + ValueField("id", _.id), + ValueField("taggedId1", c => s"character-${c.id}"), + ValueField("name", _.name), + ValueField("appearsIn", _.appearsIn), + ValueField("numberOfFriends", _ => 0), + ValueField("friends", resolveFriends _) + ) ), ValueObjectMapping[Human]( tpe = HumanType, - fieldMappings = - List( - ValueField("taggedId1", h => s"human-${h.id}"), - ValueField("taggedId2", h => s"human2-${h.id}"), - ValueField("homePlanet", _.homePlanet) - ) + fieldMappings = List( + ValueField("taggedId1", h => s"human-${h.id}"), + ValueField("taggedId2", h => s"human2-${h.id}"), + ValueField("homePlanet", _.homePlanet) + ) ), ValueObjectMapping[Droid]( tpe = DroidType, - fieldMappings = - List( - ValueField("taggedId2", h => s"droid2-${h.id}"), - ValueField("primaryFunction", _.primaryFunction) - ) + fieldMappings = List( + ValueField("taggedId2", h => s"droid2-${h.id}"), + ValueField("primaryFunction", _.primaryFunction) + ) ), LeafMapping[Episode.Value](EpisodeType) ) @@ -228,10 +224,14 @@ object StarWarsMapping extends ValueMapping[IO] { override val selectElaborator = SelectElaborator { case (QueryType, "hero", List(Binding("episode", EnumValue(e)))) => val episode = Episode.values.find(_.toString == e).get - Elab.transformChild(child => Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))) case (QueryType, "character" | "human" | "droid", List(Binding("id", IDValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(CharacterType / "id", Const(id)), child))) - case (QueryType, "characters", List(Binding("offset", IntValue(offset)), Binding("limit", IntValue(limit)))) => + case ( + QueryType, + "characters", + List(Binding("offset", IntValue(offset)), Binding("limit", IntValue(limit)))) => Elab.transformChild(child => Limit(limit, Offset(offset, child))) case (CharacterType | HumanType | DroidType, "numberOfFriends", _) => Elab.transformChild(_ => Count(Select("friends"))) diff --git a/modules/core/src/test/scala/subscription/SubscriptionSuite.scala b/modules/core/src/test/scala/subscription/SubscriptionSuite.scala index d1a38ecd..d94ecbe4 100644 --- a/modules/core/src/test/scala/subscription/SubscriptionSuite.scala +++ b/modules/core/src/test/scala/subscription/SubscriptionSuite.scala @@ -25,8 +25,8 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ +import grackle.QueryCompiler._ import grackle.syntax._ -import QueryCompiler._ final class SubscriptionSuite extends CatsEffectSuite { @@ -46,27 +46,35 @@ final class SubscriptionSuite extends CatsEffectSuite { } """ - val QueryType = schema.ref("Query") - val MutationType = schema.ref("Mutation") + val QueryType = schema.ref("Query") + val MutationType = schema.ref("Mutation") val SubscriptionType = schema.ref("Subscription") val typeMappings = List( - ObjectMapping(QueryType, List( - RootEffect.computeCursor("get")((path, env) => ref.get.map(n => Result(valueCursor(path, env, n)))) - )), - ObjectMapping(MutationType, List( - RootEffect.computeCursor("put")( (path, env) => - env.get[Int]("n") match { - case None => Result.failure(s"Implementation error: `n: Int` not found in $env").pure[IO] - case Some(n) => ref.set(n).map(_ => Result(valueCursor(path, env, n))) - } + ObjectMapping( + QueryType, + List( + RootEffect.computeCursor("get")((path, env) => + ref.get.map(n => Result(valueCursor(path, env, n)))) + )), + ObjectMapping( + MutationType, + List( + RootEffect.computeCursor("put")((path, env) => + env.get[Int]("n") match { + case None => + Result.failure(s"Implementation error: `n: Int` not found in $env").pure[IO] + case Some(n) => ref.set(n).map(_ => Result(valueCursor(path, env, n))) + }) ) - )), - ObjectMapping(SubscriptionType, List( - RootStream.computeCursor("watch")((path, env) => - ref.discrete.map(n => Result(valueCursor(path, env, n)))) - )) + ), + ObjectMapping( + SubscriptionType, + List( + RootStream.computeCursor("watch")((path, env) => + ref.discrete.map(n => Result(valueCursor(path, env, n)))) + )) ) override val selectElaborator: SelectElaborator = @@ -80,11 +88,12 @@ final class SubscriptionSuite extends CatsEffectSuite { val prog: IO[Json] = for { ref <- SignallingRef[IO, Int](0) - map = mapping(ref) - r1 <- map.compileAndRun("query { get }") + map = mapping(ref) + r1 <- map.compileAndRun("query { get }") } yield r1 - assertIO(prog, + assertIO( + prog, json""" { "data" : { @@ -99,11 +108,12 @@ final class SubscriptionSuite extends CatsEffectSuite { val prog: IO[Json] = for { ref <- SignallingRef[IO, Int](0) - map = mapping(ref) - r1 <- map.compileAndRun("mutation { put(n: 42) }") + map = mapping(ref) + r1 <- map.compileAndRun("mutation { put(n: 42) }") } yield r1 - assertIO(prog, + assertIO( + prog, json""" { "data" : { @@ -119,35 +129,38 @@ final class SubscriptionSuite extends CatsEffectSuite { val prog: IO[(Json, Json, Json)] = for { ref <- SignallingRef[IO, Int](0) - map = mapping(ref) - r0 <- map.compileAndRun("query { get }") - r1 <- map.compileAndRun("mutation { put(n: 42) }") - r2 <- map.compileAndRun("query { get }") + map = mapping(ref) + r0 <- map.compileAndRun("query { get }") + r1 <- map.compileAndRun("mutation { put(n: 42) }") + r2 <- map.compileAndRun("query { get }") } yield (r0, r1, r2) - assertIO(prog, (( - json""" + assertIO( + prog, + ( + json""" { "data" : { "get" : 0 } } """, - json""" + json""" { "data" : { "put" : 42 } } """, - json""" + json""" { "data" : { "get" : 42 } } """ - ))) + ) + ) } @@ -164,11 +177,12 @@ final class SubscriptionSuite extends CatsEffectSuite { val prog: IO[Json] = for { ref <- SignallingRef[IO, Int](0) - map = mapping(ref) - r <- map.compileAndRun(mutation) + map = mapping(ref) + r <- map.compileAndRun(mutation) } yield r - assertIO(prog, + assertIO( + prog, json""" { "data" : { @@ -186,49 +200,59 @@ final class SubscriptionSuite extends CatsEffectSuite { val prog: IO[List[Json]] = for { ref <- SignallingRef[IO, Int](0) - map = mapping(ref) - fib <- map.compileAndRunSubscription("subscription { watch }").take(4).compile.toList.start - _ <- IO.sleep(100.milli) // this is the best we can do for now; I will try to improve in a followup - _ <- map.compileAndRun("mutation { put(n: 123) }") - _ <- IO.sleep(100.milli) - _ <- map.compileAndRun("mutation { put(n: 42) }") - _ <- IO.sleep(100.milli) - _ <- map.compileAndRun("mutation { put(n: 77) }") - _ <- IO.sleep(100.milli) + map = mapping(ref) + fib <- map + .compileAndRunSubscription("subscription { watch }") + .take(4) + .compile + .toList + .start + _ <- IO.sleep( + 100.milli + ) // this is the best we can do for now; I will try to improve in a followup + _ <- map.compileAndRun("mutation { put(n: 123) }") + _ <- IO.sleep(100.milli) + _ <- map.compileAndRun("mutation { put(n: 42) }") + _ <- IO.sleep(100.milli) + _ <- map.compileAndRun("mutation { put(n: 77) }") + _ <- IO.sleep(100.milli) out <- fib.join res <- out.embedNever } yield res - assertIO(prog, List( - json""" + assertIO( + prog, + List( + json""" { "data" : { "watch" : 0 } } """, - json""" + json""" { "data" : { "watch" : 123 } } """, - json""" + json""" { "data" : { "watch" : 42 } } """, - json""" + json""" { "data" : { "watch" : 77 } } - """, - )) + """ + ) + ) } } diff --git a/modules/docs/src/main/scala/grackle/Output.scala b/modules/docs/src/main/scala/grackle/Output.scala index b75350c6..108926f2 100644 --- a/modules/docs/src/main/scala/grackle/Output.scala +++ b/modules/docs/src/main/scala/grackle/Output.scala @@ -30,7 +30,12 @@ object Output { for { txt <- IO.blocking(Files.readString(Path.of(path), StandardCharsets.UTF_8)) } yield { - val tagged = txt.split("\n").dropWhile(!_.contains(tag)).drop(1).takeWhile(!_.contains(tag)).mkString("\n") + val tagged = txt + .split("\n") + .dropWhile(!_.contains(tag)) + .drop(1) + .takeWhile(!_.contains(tag)) + .mkString("\n") s"$header\n$tagged\n$footer" } diff --git a/modules/doobie-core/src/main/scala/DoobieMapping.scala b/modules/doobie-core/src/main/scala/DoobieMapping.scala index cf78a17b..a8ba350a 100644 --- a/modules/doobie-core/src/main/scala/DoobieMapping.scala +++ b/modules/doobie-core/src/main/scala/DoobieMapping.scala @@ -13,47 +13,51 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package doobie +package grackle.doobie import cats.Reducible import cats.effect.Sync -import _root_.doobie.{ Meta, Put, Read, Transactor, Fragment => DoobieFragment } -import _root_.doobie.enumerated.JdbcType._ -import _root_.doobie.implicits._ -import _root_.doobie.util.fragments +import doobie.{Fragment => DoobieFragment, Meta, Put, Read, Transactor} +import doobie.enumerated.JdbcType._ +import doobie.implicits._ +import doobie.util.fragments import org.tpolecat.sourcepos.SourcePos import org.tpolecat.typename.TypeName +import grackle.Mapping import grackle.sql._ abstract class DoobieMapping[F[_]]( - val transactor: Transactor[F], - val monitor: DoobieMonitor[F], + val transactor: Transactor[F], + val monitor: DoobieMonitor[F] )( - implicit val M: Sync[F] -) extends Mapping[F] with DoobieMappingLike[F] + implicit val M: Sync[F] +) extends Mapping[F] + with DoobieMappingLike[F] trait DoobieMappingLike[F[_]] extends Mapping[F] with SqlMappingLike[F] { implicit val M: Sync[F] def transactor: Transactor[F] - def monitor: DoobieMonitor[F] + def monitor: DoobieMonitor[F] - type Codec = (Meta[_], Boolean) + type Codec = (Meta[_], Boolean) type Encoder = (Put[_], Boolean) - type Fragment = DoobieFragment + type Fragment = DoobieFragment def toEncoder(c: Codec): Encoder = (c._1.put, c._2) def isNullable(c: Codec): Boolean = c._2 - def intCodec = (Meta[Int], false) - def intEncoder = (Put[Int], false) - def stringEncoder = (Put[String], false) + def intCodec = (Meta[Int], false) + def intEncoder = (Put[Int], false) + def stringEncoder = (Put[String], false) def booleanEncoder = (Put[Boolean], false) - def doubleEncoder = (Put[Double], false) + def doubleEncoder = (Put[Double], false) - def col[T](colName: String, codec: Meta[T], nullable: Boolean = false)(implicit tableName: TableName, typeName: TypeName[T], pos: SourcePos): ColumnRef = + def col[T](colName: String, codec: Meta[T], nullable: Boolean = false)( + implicit tableName: TableName, + typeName: TypeName[T], + pos: SourcePos): ColumnRef = ColumnRef(tableName.name, colName, (codec, nullable), typeName.value, pos) implicit def Fragments: SqlFragment[Fragment] = @@ -66,14 +70,15 @@ trait DoobieMappingLike[F[_]] extends Mapping[F] with SqlMappingLike[F] { if (!nullable) sql"$value" else value match { - case None => sql"NULL" + case None => sql"NULL" case Some(v) => sql"${v.asInstanceOf[A]}" - case v => sql"${v.asInstanceOf[A]}" + case v => sql"${v.asInstanceOf[A]}" } } def const(s: String): Fragment = DoobieFragment.const(s) def and(fs: Fragment*): Fragment = fragments.andOpt(fs).getOrElse(empty) - def andOpt(fs: Option[Fragment]*): Fragment = fragments.andOpt(fs.flatten).getOrElse(empty) + def andOpt(fs: Option[Fragment]*): Fragment = + fragments.andOpt(fs.flatten).getOrElse(empty) def orOpt(fs: Option[Fragment]*): Fragment = fragments.orOpt(fs.flatten).getOrElse(empty) def whereAnd(fs: Fragment*): Fragment = fragments.whereAndOpt(fs) def whereAndOpt(fs: Option[Fragment]*): Fragment = fragments.whereAndOpt(fs.flatten) @@ -86,80 +91,80 @@ trait DoobieMappingLike[F[_]] extends Mapping[F] with SqlMappingLike[F] { def needsCollation(codec: Codec): Boolean = codec._1.put.jdbcTargets.head match { - case Char => true - case Clob => true - case LongnVarChar => true - case LongVarChar => true - case NChar => true - case NClob => true - case NVarChar => true - case VarChar => true - case _ => false + case Char => true + case Clob => true + case LongnVarChar => true + case LongVarChar => true + case NChar => true + case NClob => true + case NVarChar => true + case VarChar => true + case _ => false } def sqlTypeName(codec: Codec): Option[String] = { codec._1.put.jdbcTargets.head match { - case BigInt => Some("BIGINT") - case Binary => Some("BINARY") - case Bit => Some("BOOLEAN") - case Blob => Some("BLOB") - case Boolean => Some("BOOLEAN") - case Char => Some("CHAR") - case Clob => Some("CLOB") - case DataLink => Some("DATALINK") - case Date => Some("DATE") - case Decimal => Some("DECIMAL") - case Distinct => Some("DISTINCT") - case Double => Some("DOUBLE") - case Float => Some("FLOAT") - case Integer => Some("INTEGER") - case JavaObject => Some("JAVA_OBJECT") - case LongnVarChar => Some("LONGNVARCHAR") - case LongVarBinary => Some("LONGVARBINARY") - case LongVarChar => Some("LONGVARCHAR") - case NChar => Some("NCHAR") - case NClob => Some("NCLOB") - case Null => Some("NULL") - case Numeric => Some("NUMERIC") - case NVarChar => Some("NVARCHAR") - case Real => Some("REAL") - case Ref => Some("REF") - case RefCursor => Some("REF CURSOR") - case RowId => Some("ROWID") - case SmallInt => Some("SMALLINT") - case SqlXml => Some("XML") - case Struct => Some("STRUCT") - case Time => Some("TIME") - case TimeWithTimezone => Some("TIME WITH TIME ZONE") - case Timestamp => Some("TIMESTAMP") + case BigInt => Some("BIGINT") + case Binary => Some("BINARY") + case Bit => Some("BOOLEAN") + case Blob => Some("BLOB") + case Boolean => Some("BOOLEAN") + case Char => Some("CHAR") + case Clob => Some("CLOB") + case DataLink => Some("DATALINK") + case Date => Some("DATE") + case Decimal => Some("DECIMAL") + case Distinct => Some("DISTINCT") + case Double => Some("DOUBLE") + case Float => Some("FLOAT") + case Integer => Some("INTEGER") + case JavaObject => Some("JAVA_OBJECT") + case LongnVarChar => Some("LONGNVARCHAR") + case LongVarBinary => Some("LONGVARBINARY") + case LongVarChar => Some("LONGVARCHAR") + case NChar => Some("NCHAR") + case NClob => Some("NCLOB") + case Null => Some("NULL") + case Numeric => Some("NUMERIC") + case NVarChar => Some("NVARCHAR") + case Real => Some("REAL") + case Ref => Some("REF") + case RefCursor => Some("REF CURSOR") + case RowId => Some("ROWID") + case SmallInt => Some("SMALLINT") + case SqlXml => Some("XML") + case Struct => Some("STRUCT") + case Time => Some("TIME") + case TimeWithTimezone => Some("TIME WITH TIME ZONE") + case Timestamp => Some("TIMESTAMP") case TimestampWithTimezone => Some("TIMESTAMP WITH TIME ZONE") - case TinyInt => Some("TINYINT") - case VarBinary => Some("VARBINARY") - case VarChar => Some("VARCHAR") - case Array | Other => + case TinyInt => Some("TINYINT") + case VarBinary => Some("VARBINARY") + case VarChar => Some("VARCHAR") + case Array | Other => codec._1.put.vendorTypeNames.headOption match { case Some("json") => Some("JSONB") case other => other } - case _ => None + case _ => None } } } def fetch(fragment: Fragment, codecs: List[(Boolean, Codec)]): F[Vector[Array[Any]]] = { import cats.syntax.all._ - + val reads: Array[Read[Any]] = codecs.toArray.map { case (isJoin, (m, false)) => - if (isJoin) + if (isJoin) new Read.SingleOpt(m.get.widen[Any]).map(_.getOrElse(FailedJoin)) - else + else new Read.Single(m.get.widen[Any]) - case (_, (m, true)) => + case (_, (m, true)) => (new Read.SingleOpt(m.get.widen[Any]): Read[Option[Any]]).widen[Any] } - + implicit val read: Read[Array[Any]] = new Read.CompositeOfInstances[Any](reads) fragment.query[Array[Any]].to[Vector].transact(transactor) } diff --git a/modules/doobie-core/src/main/scala/DoobieMappingCompanion.scala b/modules/doobie-core/src/main/scala/DoobieMappingCompanion.scala index 448a0400..c44c24e6 100644 --- a/modules/doobie-core/src/main/scala/DoobieMappingCompanion.scala +++ b/modules/doobie-core/src/main/scala/DoobieMappingCompanion.scala @@ -13,37 +13,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package doobie +package grackle.doobie -import _root_.doobie.util.transactor.Transactor import cats.effect.Sync +import doobie.util.transactor.Transactor import org.typelevel.log4cats.Logger +import grackle.Mapping + trait DoobieMappingCompanion { def mkMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F]): Mapping[F] - def mkMapping[F[_] : Sync](transactor: Transactor[F]): Mapping[F] = { + def mkMapping[F[_]: Sync](transactor: Transactor[F]): Mapping[F] = { val monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[F] mkMapping(transactor, monitor) } @deprecated("Use mkMapping instead", "0.2.0") - def fromTransactor[F[_] : Sync](transactor: Transactor[F]): Mapping[F] = + def fromTransactor[F[_]: Sync](transactor: Transactor[F]): Mapping[F] = mkMapping(transactor) } trait LoggedDoobieMappingCompanion { def mkMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F]): Mapping[F] - def mkMapping[F[_] : Sync : Logger](transactor: Transactor[F]): Mapping[F] = { + def mkMapping[F[_]: Sync: Logger](transactor: Transactor[F]): Mapping[F] = { val monitor: DoobieMonitor[F] = DoobieMonitor.loggerMonitor[F](Logger[F]) mkMapping(transactor, monitor) } @deprecated("Use mkMapping instead", "0.2.0") - def fromTransactor[F[_] : Sync : Logger](transactor: Transactor[F]): Mapping[F] = + def fromTransactor[F[_]: Sync: Logger](transactor: Transactor[F]): Mapping[F] = mkMapping(transactor) } diff --git a/modules/doobie-core/src/main/scala/DoobieMonitor.scala b/modules/doobie-core/src/main/scala/DoobieMonitor.scala index 23c88aff..a6e7fb2d 100644 --- a/modules/doobie-core/src/main/scala/DoobieMonitor.scala +++ b/modules/doobie-core/src/main/scala/DoobieMonitor.scala @@ -13,31 +13,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package doobie +package grackle.doobie -import _root_.doobie.Fragment import cats.Applicative +import cats.effect.{Ref, Sync} import cats.implicits._ -import grackle.QueryInterpreter.ProtoJson +import doobie.Fragment import org.typelevel.log4cats.Logger + +import grackle.{Query, Result} +import grackle.QueryInterpreter.ProtoJson import grackle.sql.SqlStatsMonitor -import cats.effect.Ref -import cats.effect.Sync case class DoobieStats( - query: Query, - sql: String, - args: List[Any], - rows: Int, - cols: Int + query: Query, + sql: String, + args: List[Any], + rows: Int, + cols: Int ) object DoobieMonitor { def noopMonitor[F[_]: Applicative]: DoobieMonitor[F] = new DoobieMonitor[F] { - def queryMapped(query: Query, fragment: Fragment, rows: Int, cols: Int): F[Unit] = ().pure[F] + def queryMapped(query: Query, fragment: Fragment, rows: Int, cols: Int): F[Unit] = + ().pure[F] def resultComputed(result: Result[ProtoJson]): F[Unit] = ().pure[F] } @@ -45,11 +46,10 @@ object DoobieMonitor { new DoobieMonitor[F] { def queryMapped(query: Query, fragment: Fragment, rows: Int, cols: Int): F[Unit] = - logger.info( - s"""query: $query - |sql: ${fragment.internals.sql} - |args: ${fragment.internals.elements.mkString(", ")} - |fetched $rows row(s) of $cols column(s) + logger.info(s"""query: $query + |sql: ${fragment.internals.sql} + |args: ${fragment.internals.elements.mkString(", ")} + |fetched $rows row(s) of $cols column(s) """.stripMargin) def resultComputed(result: Result[ProtoJson]): F[Unit] = diff --git a/modules/doobie-core/src/main/scala/package.scala b/modules/doobie-core/src/main/scala/package.scala index 3084851c..6505fe82 100644 --- a/modules/doobie-core/src/main/scala/package.scala +++ b/modules/doobie-core/src/main/scala/package.scala @@ -15,9 +15,10 @@ package grackle -import grackle.sql.SqlMonitor import _root_.doobie.Fragment +import grackle.sql.SqlMonitor + package object doobie { type DoobieMonitor[F[_]] = SqlMonitor[F, Fragment] } diff --git a/modules/doobie-core/src/test/scala/DoobieDatabaseSuite.scala b/modules/doobie-core/src/test/scala/DoobieDatabaseSuite.scala index 04d6745d..09e46010 100644 --- a/modules/doobie-core/src/test/scala/DoobieDatabaseSuite.scala +++ b/modules/doobie-core/src/test/scala/DoobieDatabaseSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie -package test +package grackle.doobie.test import java.time.Duration @@ -22,6 +21,7 @@ import doobie.Meta import io.circe.{Decoder => CDecoder, Encoder => CEncoder} import munit.CatsEffectSuite +import grackle.doobie.DoobieMappingLike import grackle.sql.test._ trait DoobieDatabaseSuite extends CatsEffectSuite { @@ -44,11 +44,14 @@ trait DoobieDatabaseSuite extends CatsEffectSuite { def nullable[T](c: TestCodec[T]): TestCodec[T] = (c._1, true) - def list[T: CDecoder : CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { + def list[T: CDecoder: CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { val cm = c._1 val decode = cm.get.get.k.asInstanceOf[String => T] val encode = cm.put.put.k.asInstanceOf[T => String] - val cl = Meta.Advanced.array[String]("VARCHAR", "_VARCHAR").imap(_.toList.map(decode))(_.map(encode).toArray) + val cl = Meta + .Advanced + .array[String]("VARCHAR", "_VARCHAR") + .imap(_.toList.map(decode))(_.map(encode).toArray) (cl, false) } } diff --git a/modules/doobie-mssql/src/main/scala/DoobieMSSqlMapping.scala b/modules/doobie-mssql/src/main/scala/DoobieMSSqlMapping.scala index e7577530..691e1c0a 100644 --- a/modules/doobie-mssql/src/main/scala/DoobieMSSqlMapping.scala +++ b/modules/doobie-mssql/src/main/scala/DoobieMSSqlMapping.scala @@ -17,7 +17,7 @@ package grackle.doobie.mssql import cats.effect.Sync import cats.syntax.all._ -import _root_.doobie.Transactor +import doobie.Transactor import grackle.Mapping import grackle.Query.OrderSelection @@ -25,11 +25,12 @@ import grackle.doobie._ import grackle.sql._ abstract class DoobieMSSqlMapping[F[_]]( - val transactor: Transactor[F], - val monitor: DoobieMonitor[F], + val transactor: Transactor[F], + val monitor: DoobieMonitor[F] )( - implicit val M: Sync[F] -) extends Mapping[F] with DoobieMSSqlMappingLike[F] + implicit val M: Sync[F] +) extends Mapping[F] + with DoobieMSSqlMappingLike[F] trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingLike[F] { import SqlQuery.SqlSelect @@ -48,8 +49,10 @@ trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingL Fragments.const(" FETCH FIRST ") |+| limit |+| Fragments.const(" ROWS ONLY") def likeToFragment(expr: Fragment, pattern: String, caseInsensitive: Boolean): Fragment = { - val casedExpr = if(caseInsensitive) Fragments.const("UPPER(") |+| expr |+| Fragments.const(s")") else expr - val casedPattern = if(caseInsensitive) pattern.toUpperCase else pattern + val casedExpr = + if (caseInsensitive) Fragments.const("UPPER(") |+| expr |+| Fragments.const(s")") + else expr + val casedPattern = if (caseInsensitive) pattern.toUpperCase else pattern casedExpr |+| Fragments.const(s" LIKE ") |+| Fragments.bind(stringEncoder, casedPattern) } @@ -63,7 +66,8 @@ trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingL case "INTEGER" => "INTEGER" case "BIGINT" => "BIGINT" case "BOOLEAN" => "BIT" - case "TIMESTAMP" => "DATETIMEOFFSET" // TODO: Probably shouldn't be TIMESTAMP on the LHS + case "TIMESTAMP" => + "DATETIMEOFFSET" // TODO: Probably shouldn't be TIMESTAMP on the LHS case other => other } Fragments.const(s"CAST(NULL AS $convName)") @@ -75,12 +79,16 @@ trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingL def distinctOnToFragment(dcols: List[Fragment]): Fragment = Fragments.const("DISTINCT ") - def distinctOrderColumn(owner: ColumnOwner, col: SqlColumn, predCols: List[SqlColumn], orders: List[OrderSelection[_]]): SqlColumn = + def distinctOrderColumn( + owner: ColumnOwner, + col: SqlColumn, + predCols: List[SqlColumn], + orders: List[OrderSelection[_]]): SqlColumn = SqlColumn.FirstValueColumn(owner, col, predCols, orders) def encapsulateUnionBranch(s: SqlSelect): SqlSelect = - if(s.orders.isEmpty) s - else s.toSubquery(s.table.name+"_encaps", Laterality.NotLateral) + if (s.orders.isEmpty) s + else s.toSubquery(s.table.name + "_encaps", Laterality.NotLateral) def mkLateral(inner: Boolean): Laterality = Laterality.Apply(inner) @@ -95,12 +103,14 @@ trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingL limit.as(0) def orderToFragment(col: Fragment, ascending: Boolean, nullsLast: Boolean): Fragment = { - val dir = if(ascending) Fragments.empty else Fragments.const(" DESC") + val dir = if (ascending) Fragments.empty else Fragments.const(" DESC") val nulls = - if(nullsLast && ascending) - Fragments.const(" CASE WHEN ") |+| col |+| Fragments.const(" IS NULL THEN 1 ELSE 0 END ASC, ") - else if(!nullsLast && !ascending) - Fragments.const(" CASE WHEN ") |+| col |+| Fragments.const(" IS NULL THEN 0 ELSE 1 END DESC, ") + if (nullsLast && ascending) + Fragments.const(" CASE WHEN ") |+| col |+| Fragments.const( + " IS NULL THEN 1 ELSE 0 END ASC, ") + else if (!nullsLast && !ascending) + Fragments.const(" CASE WHEN ") |+| col |+| Fragments.const( + " IS NULL THEN 0 ELSE 1 END DESC, ") else Fragments.empty diff --git a/modules/doobie-mssql/src/test/scala/DoobieMSSqlDatabaseSuite.scala b/modules/doobie-mssql/src/test/scala/DoobieMSSqlDatabaseSuite.scala index 6519232e..8612f540 100644 --- a/modules/doobie-mssql/src/test/scala/DoobieMSSqlDatabaseSuite.scala +++ b/modules/doobie-mssql/src/test/scala/DoobieMSSqlDatabaseSuite.scala @@ -13,67 +13,77 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.mssql -package test +package grackle.doobie.mssql.test import java.sql.{Time, Timestamp} import java.time.{LocalDate, LocalTime, OffsetDateTime, ZoneId} import java.util.UUID + import scala.util.Try -import cats.effect.{Resource, Sync, IO} +import cats.effect.{IO, Resource, Sync} import cats.syntax.all._ import doobie.{Meta, Transactor} import doobie.enumerated.JdbcType import doobie.util.meta.MetaConstructors.Basic import io.circe.{Decoder => CDecoder, Encoder => CEncoder, Json} -import io.circe.syntax._ import io.circe.parser.parse +import io.circe.syntax._ import munit.catseffect._ import grackle.doobie.DoobieMonitor +import grackle.doobie.mssql.DoobieMSSqlMapping import grackle.doobie.test.DoobieDatabaseSuite - import grackle.sql.test._ trait DoobieMSSqlDatabaseSuite extends DoobieDatabaseSuite { - abstract class DoobieMSSqlTestMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) - extends DoobieMSSqlMapping[F](transactor, monitor) with DoobieTestMapping[F] with SqlTestMapping[F] { - def mkTestCodec[T](meta: Meta[T]): TestCodec[T] = (meta, false) - - val uuid: TestCodec[UUID] = - mkTestCodec(Meta[String].tiemap(s => Try(UUID.fromString(s)).toEither.leftMap(_.getMessage))(_.toString)) - - val localTime: TestCodec[LocalTime] = { - mkTestCodec(Meta[Time].timap(t => LocalTime.ofNanoOfDay(t.toLocalTime.toNanoOfDay))(lt => Time.valueOf(lt))) - } + abstract class DoobieMSSqlTestMapping[F[_]: Sync]( + transactor: Transactor[F], + monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) + extends DoobieMSSqlMapping[F](transactor, monitor) + with DoobieTestMapping[F] + with SqlTestMapping[F] { + def mkTestCodec[T](meta: Meta[T]): TestCodec[T] = (meta, false) + + val uuid: TestCodec[UUID] = + mkTestCodec(Meta[String].tiemap(s => + Try(UUID.fromString(s)).toEither.leftMap(_.getMessage))(_.toString)) + + val localTime: TestCodec[LocalTime] = { + mkTestCodec(Meta[Time].timap(t => LocalTime.ofNanoOfDay(t.toLocalTime.toNanoOfDay))(lt => + Time.valueOf(lt))) + } - val localDate: TestCodec[LocalDate] = - (Basic.oneObject(JdbcType.Date, None, classOf[LocalDate]), false) + val localDate: TestCodec[LocalDate] = + (Basic.oneObject(JdbcType.Date, None, classOf[LocalDate]), false) - // Forget precise time zone for compatibility with Postgres. Nb. this is specific to this test suite. - val offsetDateTime: TestCodec[OffsetDateTime] = - mkTestCodec(Meta[Timestamp].timap(t => OffsetDateTime.ofInstant(t.toInstant, ZoneId.of("UTC")))(o => Timestamp.from(o.toInstant))) + // Forget precise time zone for compatibility with Postgres. Nb. this is specific to this test suite. + val offsetDateTime: TestCodec[OffsetDateTime] = + mkTestCodec( + Meta[Timestamp].timap(t => OffsetDateTime.ofInstant(t.toInstant, ZoneId.of("UTC")))(o => + Timestamp.from(o.toInstant))) - val nvarchar: TestCodec[String] = mkTestCodec(Meta[String]) + val nvarchar: TestCodec[String] = mkTestCodec(Meta[String]) - val jsonb: TestCodec[Json] = - mkTestCodec(Meta[String].tiemap(s => parse(s).leftMap(_.getMessage))(_.noSpaces)) + val jsonb: TestCodec[Json] = + mkTestCodec(Meta[String].tiemap(s => parse(s).leftMap(_.getMessage))(_.noSpaces)) - override def list[T: CDecoder : CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { - def put(ts: List[T]): String = ts.asJson.noSpaces - def get(s: String): Either[String, List[T]] = parse(s).map(_.as[List[T]].toOption.get).leftMap(_.getMessage) + override def list[T: CDecoder: CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { + def put(ts: List[T]): String = ts.asJson.noSpaces + def get(s: String): Either[String, List[T]] = + parse(s).map(_.as[List[T]].toOption.get).leftMap(_.getMessage) - mkTestCodec(Meta[String].tiemap(get)(put)) - } + mkTestCodec(Meta[String].tiemap(get)(put)) } + } case class MSSqlConnectionInfo(host: String, port: Int) { val driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver" val databaseName = "test" val username = "sa" val password = "Test_123_Test" - val jdbcUrl = s"jdbc:sqlserver://$host:$port;databaseName=$databaseName;user=$username;password=$password;trustServerCertificate=true;sendTimeAsDatetime=false;" + val jdbcUrl = + s"jdbc:sqlserver://$host:$port;databaseName=$databaseName;user=$username;password=$password;trustServerCertificate=true;sendTimeAsDatetime=false;" } object MSSqlConnectionInfo { @@ -98,7 +108,8 @@ trait DoobieMSSqlDatabaseSuite extends DoobieDatabaseSuite { ) } - val transactorFixture: IOFixture[Transactor[IO]] = ResourceSuiteLocalFixture("mssqlpg", transactorResource) + val transactorFixture: IOFixture[Transactor[IO]] = + ResourceSuiteLocalFixture("mssqlpg", transactorResource) override def munitFixtures: Seq[IOFixture[_]] = Seq(transactorFixture) def transactor: Transactor[IO] = transactorFixture() diff --git a/modules/doobie-mssql/src/test/scala/DoobieMSSqlSuites.scala b/modules/doobie-mssql/src/test/scala/DoobieMSSqlSuites.scala index 52bd8b44..47c3f1e6 100644 --- a/modules/doobie-mssql/src/test/scala/DoobieMSSqlSuites.scala +++ b/modules/doobie-mssql/src/test/scala/DoobieMSSqlSuites.scala @@ -13,18 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.mssql -package test +package grackle.doobie.mssql.test import cats.effect.{IO, Resource} import doobie.{Meta, Transactor} import doobie.implicits._ import munit.catseffect.IOFixture +import grackle.Mapping import grackle.doobie.DoobieMonitor import grackle.sql.SqlStatsMonitor - -import grackle.Mapping import grackle.sql.test._ final class ArrayJoinSuite extends DoobieMSSqlDatabaseSuite with SqlArrayJoinSuite { @@ -33,15 +31,22 @@ final class ArrayJoinSuite extends DoobieMSSqlDatabaseSuite with SqlArrayJoinSui final class CoalesceSuite extends DoobieMSSqlDatabaseSuite with SqlCoalesceSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobieMSSqlTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => + (new DoobieMSSqlTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) } final class ComposedWorldSuite extends DoobieMSSqlDatabaseSuite with SqlComposedWorldSuite { def mapping: IO[(CurrencyMapping[IO], Mapping[IO])] = for { currencyMapping <- CurrencyMapping[IO] - } yield (currencyMapping, new SqlComposedMapping(new DoobieMSSqlTestMapping(transactor) with SqlWorldMapping[IO], currencyMapping)) + } yield ( + currencyMapping, + new SqlComposedMapping( + new DoobieMSSqlTestMapping(transactor) with SqlWorldMapping[IO], + currencyMapping)) } final class CompositeKeySuite extends DoobieMSSqlDatabaseSuite with SqlCompositeKeySuite { @@ -68,12 +73,18 @@ final class FilterJoinAliasSuite extends DoobieMSSqlDatabaseSuite with SqlFilter lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlFilterJoinAliasMapping[IO] } -final class FilterOrderOffsetLimitSuite extends DoobieMSSqlDatabaseSuite with SqlFilterOrderOffsetLimitSuite { - lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlFilterOrderOffsetLimitMapping[IO] +final class FilterOrderOffsetLimitSuite + extends DoobieMSSqlDatabaseSuite + with SqlFilterOrderOffsetLimitSuite { + lazy val mapping = new DoobieMSSqlTestMapping(transactor) + with SqlFilterOrderOffsetLimitMapping[IO] } -final class FilterOrderOffsetLimit2Suite extends DoobieMSSqlDatabaseSuite with SqlFilterOrderOffsetLimit2Suite { - lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlFilterOrderOffsetLimit2Mapping[IO] +final class FilterOrderOffsetLimit2Suite + extends DoobieMSSqlDatabaseSuite + with SqlFilterOrderOffsetLimit2Suite { + lazy val mapping = new DoobieMSSqlTestMapping(transactor) + with SqlFilterOrderOffsetLimit2Mapping[IO] } final class GraphSuite extends DoobieMSSqlDatabaseSuite with SqlGraphSuite { @@ -104,7 +115,9 @@ final class LikeSuite extends DoobieMSSqlDatabaseSuite with SqlLikeSuite { lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlLikeMapping[IO] } -final class MappingValidatorValidSuite extends DoobieMSSqlDatabaseSuite with SqlMappingValidatorValidSuite { +final class MappingValidatorValidSuite + extends DoobieMSSqlDatabaseSuite + with SqlMappingValidatorValidSuite { // no DB instance needed for this suite lazy val mapping = new DoobieMSSqlTestMapping(null) with SqlMappingValidatorValidMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) @@ -113,7 +126,9 @@ final class MappingValidatorValidSuite extends DoobieMSSqlDatabaseSuite with Sql override def munitFixtures: Seq[IOFixture[_]] = Nil } -final class MappingValidatorInvalidSuite extends DoobieMSSqlDatabaseSuite with SqlMappingValidatorInvalidSuite { +final class MappingValidatorInvalidSuite + extends DoobieMSSqlDatabaseSuite + with SqlMappingValidatorInvalidSuite { // no DB instance needed for this suite lazy val mapping = new DoobieMSSqlTestMapping(null) with SqlMappingValidatorInvalidMapping[IO] override def munitFixtures: Seq[IOFixture[_]] = Nil @@ -127,7 +142,8 @@ final class MovieSuite extends DoobieMSSqlDatabaseSuite with SqlMovieSuite { lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlMovieMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) - def feature: TestCodec[Feature] = (Meta[String].imap(Feature.fromString)(_.toString), false) + def feature: TestCodec[Feature] = + (Meta[String].imap(Feature.fromString)(_.toString), false) def tagList: TestCodec[List[String]] = (Meta[Int].imap(Tags.fromInt)(Tags.toInt), false) } } @@ -135,8 +151,9 @@ final class MovieSuite extends DoobieMSSqlDatabaseSuite with SqlMovieSuite { final class MutationSuite extends DoobieMSSqlDatabaseSuite with SqlMutationSuite { // A resource that copies and drops the table used in the tests. def withDuplicatedTables(transactor: Transactor[IO]): Resource[IO, Transactor[IO]] = { - val alloc = sql"SELECT * INTO city_copy FROM city".update.run.transact(transactor).as(transactor) - val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void + val alloc = + sql"SELECT * INTO city_copy FROM city".update.run.transact(transactor).as(transactor) + val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void Resource.make(alloc)(_ => free) } @@ -157,10 +174,7 @@ final class MutationSuite extends DoobieMSSqlDatabaseSuite with SqlMutationSuite INSERT INTO city_copy (id, name, countrycode, district, population) OUTPUT INSERTED.ID VALUES (NEXT VALUE FOR city_id, $name, $countryCode, 'ignored', $population) - """ - .query[Int] - .unique - .transact(transactor) + """.query[Int].unique.transact(transactor) } } @@ -193,7 +207,9 @@ final class ProjectionSuite extends DoobieMSSqlDatabaseSuite with SqlProjectionS lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlProjectionMapping[IO] } -final class RecursiveInterfacesSuite extends DoobieMSSqlDatabaseSuite with SqlRecursiveInterfacesSuite { +final class RecursiveInterfacesSuite + extends DoobieMSSqlDatabaseSuite + with SqlRecursiveInterfacesSuite { lazy val mapping = new DoobieMSSqlTestMapping(transactor) with SqlRecursiveInterfacesMapping[IO] { def itemType: TestCodec[ItemType] = @@ -220,8 +236,10 @@ final class WorldSuite extends DoobieMSSqlDatabaseSuite with SqlWorldSuite { final class WorldCompilerSuite extends DoobieMSSqlDatabaseSuite with SqlWorldCompilerSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobieMSSqlTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => (new DoobieMSSqlTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) def simpleRestrictedQuerySql: String = "SELECT country.code , country.name FROM country WHERE (( country.code = ?) )" diff --git a/modules/doobie-oracle/src/main/scala/DoobieOracleMapping.scala b/modules/doobie-oracle/src/main/scala/DoobieOracleMapping.scala index 9a407ea3..69931007 100644 --- a/modules/doobie-oracle/src/main/scala/DoobieOracleMapping.scala +++ b/modules/doobie-oracle/src/main/scala/DoobieOracleMapping.scala @@ -17,7 +17,7 @@ package grackle.doobie.oracle import cats.effect.Sync import cats.syntax.all._ -import _root_.doobie.Transactor +import doobie.Transactor import grackle.Mapping import grackle.Query.OrderSelection @@ -25,11 +25,12 @@ import grackle.doobie._ import grackle.sql._ abstract class DoobieOracleMapping[F[_]]( - val transactor: Transactor[F], - val monitor: DoobieMonitor[F], + val transactor: Transactor[F], + val monitor: DoobieMonitor[F] )( - implicit val M: Sync[F] -) extends Mapping[F] with DoobieOracleMappingLike[F] + implicit val M: Sync[F] +) extends Mapping[F] + with DoobieOracleMappingLike[F] trait DoobieOracleMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingLike[F] { import SqlQuery.SqlSelect @@ -48,8 +49,10 @@ trait DoobieOracleMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMapping Fragments.const(" FETCH FIRST ") |+| limit |+| Fragments.const(" ROWS ONLY") def likeToFragment(expr: Fragment, pattern: String, caseInsensitive: Boolean): Fragment = { - val casedExpr = if(caseInsensitive) Fragments.const("UPPER(") |+| expr |+| Fragments.const(s")") else expr - val casedPattern = if(caseInsensitive) pattern.toUpperCase else pattern + val casedExpr = + if (caseInsensitive) Fragments.const("UPPER(") |+| expr |+| Fragments.const(s")") + else expr + val casedPattern = if (caseInsensitive) pattern.toUpperCase else pattern casedExpr |+| Fragments.const(s" LIKE ") |+| Fragments.bind(stringEncoder, casedPattern) } @@ -73,8 +76,17 @@ trait DoobieOracleMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMapping def distinctOnToFragment(dcols: List[Fragment]): Fragment = Fragments.const("DISTINCT ") - def distinctOrderColumn(owner: ColumnOwner, col: SqlColumn, predCols: List[SqlColumn], orders: List[OrderSelection[_]]): SqlColumn = - SqlColumn.FirstValueColumn(owner, col, predCols, orders) // TODO: check that passing orders works with Oracle + def distinctOrderColumn( + owner: ColumnOwner, + col: SqlColumn, + predCols: List[SqlColumn], + orders: List[OrderSelection[_]]): SqlColumn = + SqlColumn.FirstValueColumn( + owner, + col, + predCols, + orders + ) // TODO: check that passing orders works with Oracle def encapsulateUnionBranch(s: SqlSelect): SqlSelect = s def mkLateral(inner: Boolean): Laterality = Laterality.Lateral @@ -82,11 +94,11 @@ trait DoobieOracleMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMapping def defaultOffsetForLimit(limit: Option[Int]): Option[Int] = None def orderToFragment(col: Fragment, ascending: Boolean, nullsLast: Boolean): Fragment = { - val dir = if(ascending) Fragments.empty else Fragments.const(" DESC") + val dir = if (ascending) Fragments.empty else Fragments.const(" DESC") val nulls = - if(!nullsLast && ascending) + if (!nullsLast && ascending) Fragments.const(" NULLS FIRST ") - else if(nullsLast && !ascending) + else if (nullsLast && !ascending) Fragments.const(" NULLS LAST ") else Fragments.empty diff --git a/modules/doobie-oracle/src/test/scala/DoobieOracleDatabaseSuite.scala b/modules/doobie-oracle/src/test/scala/DoobieOracleDatabaseSuite.scala index 8759c421..3dc7403d 100644 --- a/modules/doobie-oracle/src/test/scala/DoobieOracleDatabaseSuite.scala +++ b/modules/doobie-oracle/src/test/scala/DoobieOracleDatabaseSuite.scala @@ -13,17 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.oracle -package test +package grackle.doobie.oracle.test import java.sql.Timestamp import java.time.{LocalDate, LocalTime, OffsetDateTime, ZoneId} import java.time.format.DateTimeFormatter import java.util.UUID + import scala.util.Try import cats.data.NonEmptyList -import cats.effect.{Resource, Sync, IO} +import cats.effect.{IO, Resource, Sync} import cats.syntax.all._ import doobie.{Meta, Transactor} import doobie.enumerated.JdbcType @@ -34,49 +34,64 @@ import io.circe.parser.parse import munit.catseffect._ import grackle.doobie.DoobieMonitor +import grackle.doobie.oracle.DoobieOracleMapping import grackle.doobie.test.DoobieDatabaseSuite - import grackle.sql.test._ trait DoobieOracleDatabaseSuite extends DoobieDatabaseSuite { - abstract class DoobieOracleTestMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) - extends DoobieOracleMapping[F](transactor, monitor) with DoobieTestMapping[F] with SqlTestMapping[F] { - def mkTestCodec[T](meta: Meta[T]): TestCodec[T] = (meta, false) - - val uuid: TestCodec[UUID] = - mkTestCodec(Meta[String].tiemap(s => Try(UUID.fromString(s)).toEither.leftMap(_.getMessage))(_.toString)) - - val localTime: TestCodec[LocalTime] = { // TODO: Try Meta[java.sql.Time] - val localTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("0 H:m:s.S") - mkTestCodec(Meta[String].tiemap(s => Try(LocalTime.parse(s, localTimeFormat)).toEither.leftMap(_.getMessage))(_.format(localTimeFormat))) - } - - val localDate: TestCodec[LocalDate] = - (Basic.oneObject(JdbcType.Date, None, classOf[LocalDate]), false) - - // Forget precise time zone for compatibility with Postgres. Nb. this is specific to this test suite. - val offsetDateTime: TestCodec[OffsetDateTime] = - mkTestCodec(Meta[Timestamp].timap(t => OffsetDateTime.ofInstant(t.toInstant, ZoneId.of("UTC")))(o => Timestamp.from(o.toInstant))) - - val nvarchar: TestCodec[String] = { - val nvarcharMeta: Meta[String] = { - import JdbcType._ - val oldGet = Meta[String].get - val oldPut = Meta[String].put - val newTargets = NonEmptyList.of(NChar, NVarChar, LongnVarChar) - val newPut = - Put.Basic(oldPut.typeStack, newTargets, oldPut.put, oldPut.update, oldPut.vendorTypeNames.headOption) - - new Meta[String](oldGet, newPut) - } + abstract class DoobieOracleTestMapping[F[_]: Sync]( + transactor: Transactor[F], + monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) + extends DoobieOracleMapping[F](transactor, monitor) + with DoobieTestMapping[F] + with SqlTestMapping[F] { + def mkTestCodec[T](meta: Meta[T]): TestCodec[T] = (meta, false) + + val uuid: TestCodec[UUID] = + mkTestCodec(Meta[String].tiemap(s => + Try(UUID.fromString(s)).toEither.leftMap(_.getMessage))(_.toString)) + + val localTime: TestCodec[LocalTime] = { // TODO: Try Meta[java.sql.Time] + val localTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("0 H:m:s.S") + mkTestCodec( + Meta[String].tiemap(s => + Try(LocalTime.parse(s, localTimeFormat)).toEither.leftMap(_.getMessage))( + _.format(localTimeFormat))) + } - mkTestCodec(nvarcharMeta) + val localDate: TestCodec[LocalDate] = + (Basic.oneObject(JdbcType.Date, None, classOf[LocalDate]), false) + + // Forget precise time zone for compatibility with Postgres. Nb. this is specific to this test suite. + val offsetDateTime: TestCodec[OffsetDateTime] = + mkTestCodec( + Meta[Timestamp].timap(t => OffsetDateTime.ofInstant(t.toInstant, ZoneId.of("UTC")))(o => + Timestamp.from(o.toInstant))) + + val nvarchar: TestCodec[String] = { + val nvarcharMeta: Meta[String] = { + import JdbcType._ + val oldGet = Meta[String].get + val oldPut = Meta[String].put + val newTargets = NonEmptyList.of(NChar, NVarChar, LongnVarChar) + val newPut = + Put.Basic( + oldPut.typeStack, + newTargets, + oldPut.put, + oldPut.update, + oldPut.vendorTypeNames.headOption) + + new Meta[String](oldGet, newPut) } - val jsonb: TestCodec[Json] = - mkTestCodec(Meta[String].tiemap(s => parse(s).leftMap(_.getMessage))(_.noSpaces)) + mkTestCodec(nvarcharMeta) } + val jsonb: TestCodec[Json] = + mkTestCodec(Meta[String].tiemap(s => parse(s).leftMap(_.getMessage))(_.noSpaces)) + } + case class OracleConnectionInfo(host: String, port: Int) { val driverClassName = "oracle.jdbc.driver.OracleDriver" val databaseName = "FREEPDB1" @@ -110,7 +125,8 @@ trait DoobieOracleDatabaseSuite extends DoobieDatabaseSuite { ) } - val transactorFixture: IOFixture[Transactor[IO]] = ResourceSuiteLocalFixture("oraclepg", transactorResource) + val transactorFixture: IOFixture[Transactor[IO]] = + ResourceSuiteLocalFixture("oraclepg", transactorResource) override def munitFixtures: Seq[IOFixture[_]] = Seq(transactorFixture) def transactor: Transactor[IO] = transactorFixture() diff --git a/modules/doobie-oracle/src/test/scala/DoobieOracleSuites.scala b/modules/doobie-oracle/src/test/scala/DoobieOracleSuites.scala index ceeb5001..bfc25cd1 100644 --- a/modules/doobie-oracle/src/test/scala/DoobieOracleSuites.scala +++ b/modules/doobie-oracle/src/test/scala/DoobieOracleSuites.scala @@ -13,18 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.oracle -package test +package grackle.doobie.oracle.test import cats.effect.{IO, Resource} import doobie.{Meta, Transactor} import doobie.implicits._ import munit.catseffect.IOFixture +import grackle.Mapping import grackle.doobie.DoobieMonitor import grackle.sql.SqlStatsMonitor - -import grackle.Mapping import grackle.sql.test._ final class ArrayJoinSuite extends DoobieOracleDatabaseSuite with SqlArrayJoinSuite { @@ -33,15 +31,22 @@ final class ArrayJoinSuite extends DoobieOracleDatabaseSuite with SqlArrayJoinSu final class CoalesceSuite extends DoobieOracleDatabaseSuite with SqlCoalesceSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobieOracleTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => + (new DoobieOracleTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) } final class ComposedWorldSuite extends DoobieOracleDatabaseSuite with SqlComposedWorldSuite { def mapping: IO[(CurrencyMapping[IO], Mapping[IO])] = for { currencyMapping <- CurrencyMapping[IO] - } yield (currencyMapping, new SqlComposedMapping(new DoobieOracleTestMapping(transactor) with SqlWorldMapping[IO], currencyMapping)) + } yield ( + currencyMapping, + new SqlComposedMapping( + new DoobieOracleTestMapping(transactor) with SqlWorldMapping[IO], + currencyMapping)) } final class CompositeKeySuite extends DoobieOracleDatabaseSuite with SqlCompositeKeySuite { @@ -64,16 +69,24 @@ final class Embedding3Suite extends DoobieOracleDatabaseSuite with SqlEmbedding3 lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlEmbedding3Mapping[IO] } -final class FilterJoinAliasSuite extends DoobieOracleDatabaseSuite with SqlFilterJoinAliasSuite { +final class FilterJoinAliasSuite + extends DoobieOracleDatabaseSuite + with SqlFilterJoinAliasSuite { lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlFilterJoinAliasMapping[IO] } -final class FilterOrderOffsetLimitSuite extends DoobieOracleDatabaseSuite with SqlFilterOrderOffsetLimitSuite { - lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlFilterOrderOffsetLimitMapping[IO] +final class FilterOrderOffsetLimitSuite + extends DoobieOracleDatabaseSuite + with SqlFilterOrderOffsetLimitSuite { + lazy val mapping = new DoobieOracleTestMapping(transactor) + with SqlFilterOrderOffsetLimitMapping[IO] } -final class FilterOrderOffsetLimit2Suite extends DoobieOracleDatabaseSuite with SqlFilterOrderOffsetLimit2Suite { - lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlFilterOrderOffsetLimit2Mapping[IO] +final class FilterOrderOffsetLimit2Suite + extends DoobieOracleDatabaseSuite + with SqlFilterOrderOffsetLimit2Suite { + lazy val mapping = new DoobieOracleTestMapping(transactor) + with SqlFilterOrderOffsetLimit2Mapping[IO] } final class GraphSuite extends DoobieOracleDatabaseSuite with SqlGraphSuite { @@ -104,18 +117,24 @@ final class LikeSuite extends DoobieOracleDatabaseSuite with SqlLikeSuite { lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlLikeMapping[IO] } -final class MappingValidatorValidSuite extends DoobieOracleDatabaseSuite with SqlMappingValidatorValidSuite { +final class MappingValidatorValidSuite + extends DoobieOracleDatabaseSuite + with SqlMappingValidatorValidSuite { // no DB instance needed for this suite - lazy val mapping = new DoobieOracleTestMapping(null) with SqlMappingValidatorValidMapping[IO] { + lazy val mapping = new DoobieOracleTestMapping(null) + with SqlMappingValidatorValidMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) def feature: TestCodec[Feature] = (Meta[String].imap(Feature.fromString)(_.toString), false) } override def munitFixtures: Seq[IOFixture[_]] = Nil } -final class MappingValidatorInvalidSuite extends DoobieOracleDatabaseSuite with SqlMappingValidatorInvalidSuite { +final class MappingValidatorInvalidSuite + extends DoobieOracleDatabaseSuite + with SqlMappingValidatorInvalidSuite { // no DB instance needed for this suite - lazy val mapping = new DoobieOracleTestMapping(null) with SqlMappingValidatorInvalidMapping[IO] + lazy val mapping = new DoobieOracleTestMapping(null) + with SqlMappingValidatorInvalidMapping[IO] override def munitFixtures: Seq[IOFixture[_]] = Nil } @@ -127,7 +146,8 @@ final class MovieSuite extends DoobieOracleDatabaseSuite with SqlMovieSuite { lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlMovieMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) - def feature: TestCodec[Feature] = (Meta[String].imap(Feature.fromString)(_.toString), false) + def feature: TestCodec[Feature] = + (Meta[String].imap(Feature.fromString)(_.toString), false) def tagList: TestCodec[List[String]] = (Meta[Int].imap(Tags.fromInt)(Tags.toInt), false) } } @@ -135,8 +155,12 @@ final class MovieSuite extends DoobieOracleDatabaseSuite with SqlMovieSuite { final class MutationSuite extends DoobieOracleDatabaseSuite with SqlMutationSuite { // A resource that copies and drops the table used in the tests. def withDuplicatedTables(transactor: Transactor[IO]): Resource[IO, Transactor[IO]] = { - val alloc = sql"CREATE TABLE city_copy AS SELECT * FROM city".update.run.transact(transactor).as(transactor) - val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void + val alloc = sql"CREATE TABLE city_copy AS SELECT * FROM city" + .update + .run + .transact(transactor) + .as(transactor) + val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void Resource.make(alloc)(_ => free) } @@ -156,10 +180,7 @@ final class MutationSuite extends DoobieOracleDatabaseSuite with SqlMutationSuit sql""" INSERT INTO city_copy (id, name, countrycode, district, population) VALUES (city_id.nextval, $name, $countryCode, 'ignored', $population) - """ - .update - .withUniqueGeneratedKeys[Int]("id") - .transact(transactor) + """.update.withUniqueGeneratedKeys[Int]("id").transact(transactor) } } @@ -192,7 +213,9 @@ final class ProjectionSuite extends DoobieOracleDatabaseSuite with SqlProjection lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlProjectionMapping[IO] } -final class RecursiveInterfacesSuite extends DoobieOracleDatabaseSuite with SqlRecursiveInterfacesSuite { +final class RecursiveInterfacesSuite + extends DoobieOracleDatabaseSuite + with SqlRecursiveInterfacesSuite { lazy val mapping = new DoobieOracleTestMapping(transactor) with SqlRecursiveInterfacesMapping[IO] { def itemType: TestCodec[ItemType] = @@ -219,8 +242,10 @@ final class WorldSuite extends DoobieOracleDatabaseSuite with SqlWorldSuite { final class WorldCompilerSuite extends DoobieOracleDatabaseSuite with SqlWorldCompilerSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobieOracleTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => (new DoobieOracleTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) def simpleRestrictedQuerySql: String = "SELECT country.code , country.name FROM country WHERE (( country.code = ?) )" diff --git a/modules/doobie-pg/src/main/scala/DoobiePgMapping.scala b/modules/doobie-pg/src/main/scala/DoobiePgMapping.scala index 8eb0ac20..362cda74 100644 --- a/modules/doobie-pg/src/main/scala/DoobiePgMapping.scala +++ b/modules/doobie-pg/src/main/scala/DoobiePgMapping.scala @@ -16,17 +16,18 @@ package grackle.doobie.postgres import cats.effect.Sync -import _root_.doobie.Transactor +import doobie.Transactor import grackle.Mapping import grackle.doobie._ import grackle.sqlpg._ abstract class DoobiePgMapping[F[_]]( - val transactor: Transactor[F], - val monitor: DoobieMonitor[F], + val transactor: Transactor[F], + val monitor: DoobieMonitor[F] )( - implicit val M: Sync[F] -) extends Mapping[F] with DoobiePgMappingLike[F] + implicit val M: Sync[F] +) extends Mapping[F] + with DoobiePgMappingLike[F] trait DoobiePgMappingLike[F[_]] extends DoobieMappingLike[F] with SqlPgMappingLike[F] diff --git a/modules/doobie-pg/src/test/scala/DoobiePgDatabaseSuite.scala b/modules/doobie-pg/src/test/scala/DoobiePgDatabaseSuite.scala index 49e8f19b..6a832db5 100644 --- a/modules/doobie-pg/src/test/scala/DoobiePgDatabaseSuite.scala +++ b/modules/doobie-pg/src/test/scala/DoobiePgDatabaseSuite.scala @@ -13,27 +13,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.postgres -package test +package grackle.doobie.postgres.test import java.time.{LocalDate, LocalTime, OffsetDateTime} import java.util.UUID import cats.effect.{IO, Resource, Sync} import doobie.{Get, Meta, Put, Transactor} -import doobie.postgres.implicits._ import doobie.postgres.circe.jsonb.implicits._ +import doobie.postgres.implicits._ import io.circe.Json import munit.catseffect.IOFixture import grackle.doobie.DoobieMonitor +import grackle.doobie.postgres.DoobiePgMapping import grackle.doobie.test.DoobieDatabaseSuite import grackle.sql.test._ import grackle.sqlpg.test._ trait DoobiePgDatabaseSuite extends DoobieDatabaseSuite with SqlPgDatabaseSuite { - abstract class DoobiePgTestMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) - extends DoobiePgMapping[F](transactor, monitor) with DoobieTestMapping[F] with SqlTestMapping[F] { + abstract class DoobiePgTestMapping[F[_]: Sync]( + transactor: Transactor[F], + monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) + extends DoobiePgMapping[F](transactor, monitor) + with DoobieTestMapping[F] + with SqlTestMapping[F] { def uuid: TestCodec[UUID] = (Meta[UUID], false) def localDate: TestCodec[LocalDate] = (Meta[LocalDate], false) def localTime: TestCodec[LocalTime] = (Meta[LocalTime], false) @@ -57,7 +61,8 @@ trait DoobiePgDatabaseSuite extends DoobieDatabaseSuite with SqlPgDatabaseSuite ) } - val transactorFixture: IOFixture[Transactor[IO]] = ResourceSuiteLocalFixture("doobiepg", transactorResource) + val transactorFixture: IOFixture[Transactor[IO]] = + ResourceSuiteLocalFixture("doobiepg", transactorResource) override def munitFixtures: Seq[IOFixture[_]] = Seq(transactorFixture) def transactor: Transactor[IO] = transactorFixture() diff --git a/modules/doobie-pg/src/test/scala/DoobiePgSuites.scala b/modules/doobie-pg/src/test/scala/DoobiePgSuites.scala index 9ea5e3ee..f7dd5511 100644 --- a/modules/doobie-pg/src/test/scala/DoobiePgSuites.scala +++ b/modules/doobie-pg/src/test/scala/DoobiePgSuites.scala @@ -13,18 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.doobie.postgres -package test +package grackle.doobie.postgres.test import cats.effect.{IO, Resource} import doobie.{Meta, Transactor} import doobie.implicits._ import munit.catseffect.IOFixture +import grackle.Mapping import grackle.doobie.DoobieMonitor import grackle.sql.SqlStatsMonitor - -import grackle.Mapping import grackle.sql.test._ final class ArrayJoinSuite extends DoobiePgDatabaseSuite with SqlArrayJoinSuite { @@ -33,15 +31,21 @@ final class ArrayJoinSuite extends DoobiePgDatabaseSuite with SqlArrayJoinSuite final class CoalesceSuite extends DoobiePgDatabaseSuite with SqlCoalesceSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobiePgTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => (new DoobiePgTestMapping(transactor, mon) with SqlCoalesceMapping[IO], mon)) } final class ComposedWorldSuite extends DoobiePgDatabaseSuite with SqlComposedWorldSuite { def mapping: IO[(CurrencyMapping[IO], Mapping[IO])] = for { currencyMapping <- CurrencyMapping[IO] - } yield (currencyMapping, new SqlComposedMapping(new DoobiePgTestMapping(transactor) with SqlWorldMapping[IO], currencyMapping)) + } yield ( + currencyMapping, + new SqlComposedMapping( + new DoobiePgTestMapping(transactor) with SqlWorldMapping[IO], + currencyMapping)) } final class CompositeKeySuite extends DoobiePgDatabaseSuite with SqlCompositeKeySuite { @@ -68,12 +72,18 @@ final class FilterJoinAliasSuite extends DoobiePgDatabaseSuite with SqlFilterJoi lazy val mapping = new DoobiePgTestMapping(transactor) with SqlFilterJoinAliasMapping[IO] } -final class FilterOrderOffsetLimitSuite extends DoobiePgDatabaseSuite with SqlFilterOrderOffsetLimitSuite { - lazy val mapping = new DoobiePgTestMapping(transactor) with SqlFilterOrderOffsetLimitMapping[IO] +final class FilterOrderOffsetLimitSuite + extends DoobiePgDatabaseSuite + with SqlFilterOrderOffsetLimitSuite { + lazy val mapping = new DoobiePgTestMapping(transactor) + with SqlFilterOrderOffsetLimitMapping[IO] } -final class FilterOrderOffsetLimit2Suite extends DoobiePgDatabaseSuite with SqlFilterOrderOffsetLimit2Suite { - lazy val mapping = new DoobiePgTestMapping(transactor) with SqlFilterOrderOffsetLimit2Mapping[IO] +final class FilterOrderOffsetLimit2Suite + extends DoobiePgDatabaseSuite + with SqlFilterOrderOffsetLimit2Suite { + lazy val mapping = new DoobiePgTestMapping(transactor) + with SqlFilterOrderOffsetLimit2Mapping[IO] } final class GraphSuite extends DoobiePgDatabaseSuite with SqlGraphSuite { @@ -104,7 +114,9 @@ final class LikeSuite extends DoobiePgDatabaseSuite with SqlLikeSuite { lazy val mapping = new DoobiePgTestMapping(transactor) with SqlLikeMapping[IO] } -final class MappingValidatorValidSuite extends DoobiePgDatabaseSuite with SqlMappingValidatorValidSuite { +final class MappingValidatorValidSuite + extends DoobiePgDatabaseSuite + with SqlMappingValidatorValidSuite { // no DB instance needed for this suite lazy val mapping = new DoobiePgTestMapping(null) with SqlMappingValidatorValidMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) @@ -113,7 +125,9 @@ final class MappingValidatorValidSuite extends DoobiePgDatabaseSuite with SqlMap override def munitFixtures: Seq[IOFixture[_]] = Nil } -final class MappingValidatorInvalidSuite extends DoobiePgDatabaseSuite with SqlMappingValidatorInvalidSuite { +final class MappingValidatorInvalidSuite + extends DoobiePgDatabaseSuite + with SqlMappingValidatorInvalidSuite { // no DB instance needed for this suite lazy val mapping = new DoobiePgTestMapping(null) with SqlMappingValidatorInvalidMapping[IO] override def munitFixtures: Seq[IOFixture[_]] = Nil @@ -127,7 +141,8 @@ final class MovieSuite extends DoobiePgDatabaseSuite with SqlMovieSuite { lazy val mapping = new DoobiePgTestMapping(transactor) with SqlMovieMapping[IO] { def genre: TestCodec[Genre] = (Meta[Int].imap(Genre.fromInt)(Genre.toInt), false) - def feature: TestCodec[Feature] = (Meta[String].imap(Feature.fromString)(_.toString), false) + def feature: TestCodec[Feature] = + (Meta[String].imap(Feature.fromString)(_.toString), false) def tagList: TestCodec[List[String]] = (Meta[Int].imap(Tags.fromInt)(Tags.toInt), false) } } @@ -135,8 +150,12 @@ final class MovieSuite extends DoobiePgDatabaseSuite with SqlMovieSuite { final class MutationSuite extends DoobiePgDatabaseSuite with SqlMutationSuite { // A resource that copies and drops the table used in the tests. def withDuplicatedTables(transactor: Transactor[IO]): Resource[IO, Transactor[IO]] = { - val alloc = sql"CREATE TABLE city_copy AS SELECT * FROM city".update.run.transact(transactor).as(transactor) - val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void + val alloc = sql"CREATE TABLE city_copy AS SELECT * FROM city" + .update + .run + .transact(transactor) + .as(transactor) + val free = sql"DROP TABLE city_copy".update.run.transact(transactor).void Resource.make(alloc)(_ => free) } @@ -157,9 +176,7 @@ final class MutationSuite extends DoobiePgDatabaseSuite with SqlMutationSuite { INSERT INTO city_copy (id, name, countrycode, district, population) VALUES (nextval('city_id'), $name, $countryCode, 'ignored', $population) RETURNING id - """.query[Int] - .unique - .transact(transactor) + """.query[Int].unique.transact(transactor) } } @@ -192,7 +209,9 @@ final class ProjectionSuite extends DoobiePgDatabaseSuite with SqlProjectionSuit lazy val mapping = new DoobiePgTestMapping(transactor) with SqlProjectionMapping[IO] } -final class RecursiveInterfacesSuite extends DoobiePgDatabaseSuite with SqlRecursiveInterfacesSuite { +final class RecursiveInterfacesSuite + extends DoobiePgDatabaseSuite + with SqlRecursiveInterfacesSuite { lazy val mapping = new DoobiePgTestMapping(transactor) with SqlRecursiveInterfacesMapping[IO] { def itemType: TestCodec[ItemType] = @@ -219,8 +238,10 @@ final class WorldSuite extends DoobiePgDatabaseSuite with SqlWorldSuite { final class WorldCompilerSuite extends DoobiePgDatabaseSuite with SqlWorldCompilerSuite { type Fragment = doobie.Fragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - DoobieMonitor.statsMonitor[IO].map(mon => (new DoobiePgTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + DoobieMonitor + .statsMonitor[IO] + .map(mon => (new DoobiePgTestMapping(transactor, mon) with SqlWorldMapping[IO], mon)) def simpleRestrictedQuerySql: String = "SELECT country.code , country.name FROM country WHERE (( country.code = ?) )" diff --git a/modules/generic/src/main/scala-2/genericmapping2.scala b/modules/generic/src/main/scala-2/genericmapping2.scala index d5541a42..60974bad 100644 --- a/modules/generic/src/main/scala-2/genericmapping2.scala +++ b/modules/generic/src/main/scala-2/genericmapping2.scala @@ -13,22 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import scala.annotation.{nowarn, tailrec} import cats.implicits._ -import shapeless.{Coproduct, Generic, ::, HList, HNil, Inl, Inr, LabelledGeneric} -import shapeless.ops.hlist.{LiftAll => PLiftAll} +import shapeless.{::, Coproduct, Generic, HList, HNil, Inl, Inr, LabelledGeneric} import shapeless.ops.coproduct.{LiftAll => CLiftAll} +import shapeless.ops.hlist.{LiftAll => PLiftAll} import shapeless.ops.record.Keys -import syntax._ -import Cursor.AbstractCursor -import ShapelessUtils._ +import grackle._ +import grackle.Cursor.AbstractCursor +import grackle.generic.ShapelessUtils._ +import grackle.syntax._ -trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: GenericMappingLike[F] => +trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { + self: GenericMappingLike[F] => trait MkObjectCursorBuilder[T] { def apply(tpe: Type): ObjectCursorBuilder[T] } @@ -36,24 +37,26 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge object MkObjectCursorBuilder { type FieldMap[T] = Map[String, (Context, T, Option[Cursor], Env) => Result[Cursor]] - implicit def productCursorBuilder[T <: Product, R <: HList, L <: HList] - (implicit - @nowarn gen: Generic.Aux[T, R], + implicit def productCursorBuilder[T <: Product, R <: HList, L <: HList]( + implicit @nowarn gen: Generic.Aux[T, R], elems0: => PLiftAll[CursorBuilder, R], @nowarn lgen: LabelledGeneric.Aux[T, L], keys0: Keys[L] - ): MkObjectCursorBuilder[T] = + ): MkObjectCursorBuilder[T] = new MkObjectCursorBuilder[T] { def apply(tpe: Type): ObjectCursorBuilder[T] = { def fieldMap: Map[String, (Context, T, Option[Cursor], Env) => Result[Cursor]] = { val keys: List[String] = unsafeToList[Symbol](keys0()).map(_.name) val elems = unsafeToList[CursorBuilder[Any]](elems0.instances) - keys.zip(elems.zipWithIndex).map { - case (fieldName, (elem, idx)) => - def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = - elem.build(context, focus.productElement(idx), parent, env) - (fieldName, build _) - }.toMap + keys + .zip(elems.zipWithIndex) + .map { + case (fieldName, (elem, idx)) => + def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = + elem.build(context, focus.productElement(idx), parent, env) + (fieldName, build _) + } + .toMap } new Impl[T](tpe, fieldMap) } @@ -68,10 +71,11 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge CursorImpl(context.asType(tpe), focus, fieldMap, parent, env).success def renameField(from: String, to: String): ObjectCursorBuilder[T] = - transformFieldNames { case `from` => to ; case other => other } + transformFieldNames { case `from` => to; case other => other } def transformFieldNames(f: String => String): ObjectCursorBuilder[T] = new Impl(tpe, fieldMap0.map { case (k, v) => (f(k), v) }) - def transformField[U](fieldName: String)(f: T => Result[U])(implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] = { + def transformField[U](fieldName: String)(f: T => Result[U])( + implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] = { def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = f(focus).flatMap(f => cb.build(context, f, parent, env)) @@ -79,14 +83,24 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge } } - case class CursorImpl[T](context: Context, focus: T, fieldMap: FieldMap[T], parent: Option[Cursor], env: Env) - extends AbstractCursor { + case class CursorImpl[T]( + context: Context, + focus: T, + fieldMap: FieldMap[T], + parent: Option[Cursor], + env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = { val localField = - fieldMap.get(fieldName).toResult(s"No field '$fieldName' for type $tpe").flatMap { f => - f(context.forFieldOrAttribute(fieldName, resultName), focus, Some(this), Env.empty) + fieldMap.get(fieldName).toResult(s"No field '$fieldName' for type $tpe").flatMap { + f => + f( + context.forFieldOrAttribute(fieldName, resultName), + focus, + Some(this), + Env.empty) } localField orElse mkCursorForField(this, fieldName, resultName) @@ -99,11 +113,10 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge } object MkInterfaceCursorBuilder { - implicit def coproductCursorBuilder[T, R <: Coproduct] - (implicit - gen: Generic.Aux[T, R], + implicit def coproductCursorBuilder[T, R <: Coproduct]( + implicit gen: Generic.Aux[T, R], elems0: => CLiftAll[CursorBuilder, R] - ): MkInterfaceCursorBuilder[T] = + ): MkInterfaceCursorBuilder[T] = new MkInterfaceCursorBuilder[T] { def apply(tpe: Type): CursorBuilder[T] = { def elems = unsafeToList[CursorBuilder[T]](elems0.instances).toArray @@ -111,7 +124,7 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge @tailrec def loop(c: Coproduct, acc: Int): Int = c match { case Inl(_) => acc - case Inr(tl) => loop(tl, acc+1) + case Inr(tl) => loop(tl, acc + 1) } loop(gen.to(t), 0) } @@ -119,16 +132,28 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge } } - class Impl[T](tpe0: Type, sel: T => Int, elems: => Array[CursorBuilder[T]]) extends CursorBuilder[T] { + class Impl[T](tpe0: Type, sel: T => Int, elems: => Array[CursorBuilder[T]]) + extends CursorBuilder[T] { val tpe = tpe0 - def build(context: Context, focus: T, parent: Option[Cursor], env: Env): Result[Cursor] = { + def build( + context: Context, + focus: T, + parent: Option[Cursor], + env: Env): Result[Cursor] = { val builder = elems(sel(focus)) - builder.build(context.asType(tpe), focus, parent, env).map(cursor => CursorImpl(tpe, builder.tpe, cursor, parent, env)) + builder + .build(context.asType(tpe), focus, parent, env) + .map(cursor => CursorImpl(tpe, builder.tpe, cursor, parent, env)) } } - case class CursorImpl[T](tpe0: Type, rtpe: Type, cursor: Cursor, parent: Option[Cursor], env: Env) - extends AbstractCursor { + case class CursorImpl[T]( + tpe0: Type, + rtpe: Type, + cursor: Cursor, + parent: Option[Cursor], + env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) def focus: Any = cursor.focus @@ -143,7 +168,9 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge override def narrow(subtpe: TypeRef): Result[Cursor] = narrowsTo(subtpe).flatMap { n => if (n) copy(tpe0 = subtpe).success - else Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") + else + Result.internalError( + s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") } } } diff --git a/modules/generic/src/main/scala-3/genericmapping3.scala b/modules/generic/src/main/scala-3/genericmapping3.scala index 4e7cb6df..14c00db4 100644 --- a/modules/generic/src/main/scala-3/genericmapping3.scala +++ b/modules/generic/src/main/scala-3/genericmapping3.scala @@ -13,16 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import cats.implicits._ import shapeless3.deriving._ -import syntax._ -import Cursor.AbstractCursor +import grackle._ +import grackle.Cursor.AbstractCursor +import grackle.syntax._ -trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: GenericMappingLike[F] => +trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { + self: GenericMappingLike[F] => trait MkObjectCursorBuilder[T] { def apply(tpe: Type): ObjectCursorBuilder[T] } @@ -30,20 +31,25 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge object MkObjectCursorBuilder { type FieldMap[T] = Map[String, (Context, T, Option[Cursor], Env) => Result[Cursor]] - implicit def productCursorBuilder[T <: Product] - (implicit - inst: K0.ProductInstances[CursorBuilder, T], - labelling: Labelling[T], - ): MkObjectCursorBuilder[T] = + implicit def productCursorBuilder[T <: Product]( + implicit inst: K0.ProductInstances[CursorBuilder, T], + labelling: Labelling[T] + ): MkObjectCursorBuilder[T] = new MkObjectCursorBuilder[T] { def apply(tpe: Type): ObjectCursorBuilder[T] = { def fieldMap: FieldMap[T] = { - labelling.elemLabels.zipWithIndex.map { - case (fieldName, idx) => - def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = - inst.project(focus)(idx)([t] => (builder: CursorBuilder[t], pt: t) => builder.build(context, pt, parent, env)) - (fieldName, build _) - }.toMap + labelling + .elemLabels + .zipWithIndex + .map { + case (fieldName, idx) => + def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = + inst.project(focus)(idx)([t] => + (builder: CursorBuilder[t], pt: t) => + builder.build(context, pt, parent, env)) + (fieldName, build _) + } + .toMap } new Impl[T](tpe, fieldMap) } @@ -58,10 +64,11 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge CursorImpl(context.asType(tpe), focus, fieldMap, parent, env).success def renameField(from: String, to: String): ObjectCursorBuilder[T] = - transformFieldNames { case `from` => to ; case other => other } + transformFieldNames { case `from` => to; case other => other } def transformFieldNames(f: String => String): ObjectCursorBuilder[T] = new Impl(tpe, fieldMap0.map { case (k, v) => (f(k), v) }) - def transformField[U](fieldName: String)(f: T => Result[U])(implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] = { + def transformField[U](fieldName: String)(f: T => Result[U])( + implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] = { def build(context: Context, focus: T, parent: Option[Cursor], env: Env) = f(focus).flatMap(f => cb.build(context, f, parent, env)) @@ -69,14 +76,24 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge } } - case class CursorImpl[T](context: Context, focus: T, fieldMap: FieldMap[T], parent: Option[Cursor], env: Env) - extends AbstractCursor { + case class CursorImpl[T]( + context: Context, + focus: T, + fieldMap: FieldMap[T], + parent: Option[Cursor], + env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = { val localField = - fieldMap.get(fieldName).toResult(s"No field '$fieldName' for type $tpe").flatMap { f => - f(context.forFieldOrAttribute(fieldName, resultName), focus, Some(this), Env.empty) + fieldMap.get(fieldName).toResult(s"No field '$fieldName' for type $tpe").flatMap { + f => + f( + context.forFieldOrAttribute(fieldName, resultName), + focus, + Some(this), + Env.empty) } localField orElse mkCursorForField(this, fieldName, resultName) @@ -89,25 +106,36 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge } object MkInterfaceCursorBuilder { - implicit def coproductCursorBuilder[T] - (implicit - inst: => K0.CoproductInstances[CursorBuilder, T] - ): MkInterfaceCursorBuilder[T] = + implicit def coproductCursorBuilder[T]( + implicit inst: => K0.CoproductInstances[CursorBuilder, T] + ): MkInterfaceCursorBuilder[T] = new MkInterfaceCursorBuilder[T] { def apply(tpe: Type): CursorBuilder[T] = new Impl[T](tpe, inst) } - class Impl[T](tpe0: Type, inst: K0.CoproductInstances[CursorBuilder, T]) extends CursorBuilder[T] { + class Impl[T](tpe0: Type, inst: K0.CoproductInstances[CursorBuilder, T]) + extends CursorBuilder[T] { val tpe = tpe0 - def build(context: Context, focus: T, parent: Option[Cursor], env: Env): Result[Cursor] = { - inst.fold(focus)([t] => (builder: CursorBuilder[t], pt: t) => - builder.build(context, pt, parent, env).map(cursor => CursorImpl(tpe, builder.tpe, cursor, parent, env)) - ) + def build( + context: Context, + focus: T, + parent: Option[Cursor], + env: Env): Result[Cursor] = { + inst.fold(focus)([t] => + (builder: CursorBuilder[t], pt: t) => + builder + .build(context, pt, parent, env) + .map(cursor => CursorImpl(tpe, builder.tpe, cursor, parent, env))) } } - case class CursorImpl[T](tpe0: Type, rtpe: Type, cursor: Cursor, parent: Option[Cursor], env: Env) - extends AbstractCursor { + case class CursorImpl[T]( + tpe0: Type, + rtpe: Type, + cursor: Cursor, + parent: Option[Cursor], + env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) def focus: Any = cursor.focus @@ -122,7 +150,9 @@ trait ScalaVersionSpecificGenericMappingLike[F[_]] extends Mapping[F] { self: Ge override def narrow(subtpe: TypeRef): Result[Cursor] = narrowsTo(subtpe).flatMap { n => if (n) copy(tpe0 = subtpe).success - else Result.internalError(s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") + else + Result.internalError( + s"Focus ${focus} of static type $tpe cannot be narrowed to $subtpe") } } } diff --git a/modules/generic/src/main/scala/CursorBuilder.scala b/modules/generic/src/main/scala/CursorBuilder.scala index d8c32814..f0a0b0f0 100644 --- a/modules/generic/src/main/scala/CursorBuilder.scala +++ b/modules/generic/src/main/scala/CursorBuilder.scala @@ -13,23 +13,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import scala.collection.Factory import cats.implicits._ import io.circe.{Encoder, Json} -import syntax._ -import Cursor.AbstractCursor +import grackle._ +import grackle.Cursor.AbstractCursor +import grackle.syntax._ trait CursorBuilder[T] { def tpe: Type - def build(context: Context, focus: T, parent: Option[Cursor] = None, env: Env = Env.empty): Result[Cursor] + def build( + context: Context, + focus: T, + parent: Option[Cursor] = None, + env: Env = Env.empty): Result[Cursor] /** - * Apply a pre-processing function while fixing the `Type` of this `CursorBuilder`. + * Apply a pre-processing function while fixing the `Type` of this `CursorBuilder`. */ final def contramap[A](f: A => T): CursorBuilder[A] = CursorBuilder.contramap(f, this) } @@ -42,24 +46,33 @@ object CursorBuilder { implicit val stringCursorBuilder: CursorBuilder[String] = { case class StringCursor(context: Context, focus: String, parent: Option[Cursor], env: Env) extends PrimitiveCursor[String] { - def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) + def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def asLeaf: Result[Json] = Json.fromString(focus).success } new CursorBuilder[String] { val tpe = StringType - def build(context: Context, focus: String, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: String, + parent: Option[Cursor], + env: Env): Result[Cursor] = StringCursor(context.asType(tpe), focus, parent, env).success } } implicit val intCursorBuilder: CursorBuilder[Int] = { - case class IntCursor(context: Context, focus: Int, parent: Option[Cursor], env: Env) extends PrimitiveCursor[Int] { - def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) + case class IntCursor(context: Context, focus: Int, parent: Option[Cursor], env: Env) + extends PrimitiveCursor[Int] { + def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def asLeaf: Result[Json] = Json.fromInt(focus).success } new CursorBuilder[Int] { val tpe = IntType - def build(context: Context, focus: Int, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Int, + parent: Option[Cursor], + env: Env): Result[Cursor] = IntCursor(context.asType(tpe), focus, parent, env).success } } @@ -67,12 +80,16 @@ object CursorBuilder { implicit val longCursorBuilder: CursorBuilder[Long] = { case class LongCursor(context: Context, focus: Long, parent: Option[Cursor], env: Env) extends PrimitiveCursor[Long] { - def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) + def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def asLeaf: Result[Json] = Json.fromLong(focus).success } new CursorBuilder[Long] { val tpe = IntType - def build(context: Context, focus: Long, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Long, + parent: Option[Cursor], + env: Env): Result[Cursor] = LongCursor(context.asType(tpe), focus, parent, env).success } } @@ -86,7 +103,11 @@ object CursorBuilder { } new CursorBuilder[Float] { val tpe = FloatType - def build(context: Context, focus: Float, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Float, + parent: Option[Cursor], + env: Env): Result[Cursor] = FloatCursor(context.asType(tpe), focus, parent, env).success } } @@ -100,7 +121,11 @@ object CursorBuilder { } new CursorBuilder[Double] { val tpe = FloatType - def build(context: Context, focus: Double, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Double, + parent: Option[Cursor], + env: Env): Result[Cursor] = DoubleCursor(context.asType(tpe), focus, parent, env).success } } @@ -108,12 +133,16 @@ object CursorBuilder { implicit val booleanCursorBuilder: CursorBuilder[Boolean] = { case class BooleanCursor(context: Context, focus: Boolean, parent: Option[Cursor], env: Env) extends PrimitiveCursor[Boolean] { - def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) + def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def asLeaf: Result[Json] = Json.fromBoolean(focus).success } new CursorBuilder[Boolean] { val tpe = BooleanType - def build(context: Context, focus: Boolean, parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Boolean, + parent: Option[Cursor], + env: Env): Result[Cursor] = BooleanCursor(context.asType(tpe), focus, parent, env).success } } @@ -121,7 +150,7 @@ object CursorBuilder { def deriveEnumerationCursorBuilder[T <: Enumeration#Value](tpe0: Type): CursorBuilder[T] = { case class EnumerationCursor(context: Context, focus: T, parent: Option[Cursor], env: Env) extends PrimitiveCursor[T] { - def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) + def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def asLeaf: Result[Json] = Json.fromString(focus.toString).success } new CursorBuilder[T] { @@ -134,8 +163,13 @@ object CursorBuilder { implicit def enumerationCursorBuilder[T <: Enumeration#Value]: CursorBuilder[T] = deriveEnumerationCursorBuilder(StringType) - implicit def optionCursorBuiler[T](implicit elemBuilder: CursorBuilder[T]): CursorBuilder[Option[T]] = { - case class OptionCursor(context: Context, focus: Option[T], parent: Option[Cursor], env: Env) + implicit def optionCursorBuiler[T]( + implicit elemBuilder: CursorBuilder[T]): CursorBuilder[Option[T]] = { + case class OptionCursor( + context: Context, + focus: Option[T], + parent: Option[Cursor], + env: Env) extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) @@ -146,13 +180,19 @@ object CursorBuilder { new CursorBuilder[Option[T]] { outer => val tpe = NullableType(elemBuilder.tpe) - def build(context: Context, focus: Option[T], parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: Option[T], + parent: Option[Cursor], + env: Env): Result[Cursor] = OptionCursor(context.asType(tpe), focus, parent, env).success } } - implicit def listCursorBuiler[T](implicit elemBuilder: CursorBuilder[T]): CursorBuilder[List[T]] = { - case class ListCursor(context: Context, focus: List[T], parent: Option[Cursor], env: Env) extends AbstractCursor { + implicit def listCursorBuiler[T]( + implicit elemBuilder: CursorBuilder[T]): CursorBuilder[List[T]] = { + case class ListCursor(context: Context, focus: List[T], parent: Option[Cursor], env: Env) + extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) override def preunique: Result[Cursor] = { @@ -162,21 +202,32 @@ object CursorBuilder { override def isList: Boolean = true override def asList[C](factory: Factory[Cursor, C]): Result[C] = - focus.traverse(elem => elemBuilder.build(context, elem, Some(this), env)).map(_.to(factory)) + focus + .traverse(elem => elemBuilder.build(context, elem, Some(this), env)) + .map(_.to(factory)) } new CursorBuilder[List[T]] { outer => val tpe = ListType(elemBuilder.tpe) - def build(context: Context, focus: List[T], parent: Option[Cursor], env: Env): Result[Cursor] = + def build( + context: Context, + focus: List[T], + parent: Option[Cursor], + env: Env): Result[Cursor] = ListCursor(context.asType(tpe), focus, parent, env).success } } - case class LeafCursor[T](context: Context, focus: T, encoder: Encoder[T], parent: Option[Cursor], env: Env) + case class LeafCursor[T]( + context: Context, + focus: T, + encoder: Encoder[T], + parent: Option[Cursor], + env: Env) extends AbstractCursor { def withEnv(env0: Env): Cursor = copy(env = env.add(env0)) - override def isLeaf: Boolean = true + override def isLeaf: Boolean = true override def asLeaf: Result[Json] = encoder(focus).success } @@ -193,7 +244,11 @@ object CursorBuilder { def contramap[A, B](f: A => B, cb: CursorBuilder[B]): CursorBuilder[A] = new CursorBuilder[A] { def tpe: Type = cb.tpe - def build(context: Context, focus: A, parent: Option[Cursor] = None, env: Env = Env.empty): Result[Cursor] = + def build( + context: Context, + focus: A, + parent: Option[Cursor] = None, + env: Env = Env.empty): Result[Cursor] = cb.build(context, f(focus), parent, env) } } diff --git a/modules/generic/src/main/scala/genericmapping.scala b/modules/generic/src/main/scala/genericmapping.scala index 8b987993..1605d444 100644 --- a/modules/generic/src/main/scala/genericmapping.scala +++ b/modules/generic/src/main/scala/genericmapping.scala @@ -13,25 +13,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import cats.MonadThrow import org.tpolecat.sourcepos.SourcePos -import syntax._ -import Cursor.DeferredCursor +import grackle._ +import grackle.Cursor.DeferredCursor +import grackle.syntax._ -abstract class GenericMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with GenericMappingLike[F] +abstract class GenericMapping[F[_]](implicit val M: MonadThrow[F]) + extends Mapping[F] + with GenericMappingLike[F] trait GenericMappingLike[F[_]] extends ScalaVersionSpecificGenericMappingLike[F] { - def genericCursor[T](path: Path, env: Env, t: T)(implicit cb: => CursorBuilder[T]): Result[Cursor] = - if(path.isRoot) + def genericCursor[T](path: Path, env: Env, t: T)( + implicit cb: => CursorBuilder[T]): Result[Cursor] = + if (path.isRoot) cb.build(Context(path.rootTpe), t, None, env) else DeferredCursor(path, (context, parent) => cb.build(context, t, Some(parent), env)).success - override def mkCursorForMappedField(parent: Cursor, fieldContext: Context, fm: FieldMapping): Result[Cursor] = + override def mkCursorForMappedField( + parent: Cursor, + fieldContext: Context, + fm: FieldMapping): Result[Cursor] = fm match { case GenericField(_, t, cb, _) => cb().build(fieldContext, t, Some(parent), parent.env) @@ -39,25 +45,33 @@ trait GenericMappingLike[F[_]] extends ScalaVersionSpecificGenericMappingLike[F] super.mkCursorForMappedField(parent, fieldContext, fm) } - case class GenericField[T](val fieldName: String, t: T, cb: () => CursorBuilder[T], hidden: Boolean)( - implicit val pos: SourcePos + case class GenericField[T]( + val fieldName: String, + t: T, + cb: () => CursorBuilder[T], + hidden: Boolean)( + implicit val pos: SourcePos ) extends FieldMapping { def subtree: Boolean = true } - def GenericField[T](fieldName: String, t: T, hidden: Boolean = false)(implicit cb: => CursorBuilder[T], pos: SourcePos): GenericField[T] = + def GenericField[T](fieldName: String, t: T, hidden: Boolean = false)( + implicit cb: => CursorBuilder[T], + pos: SourcePos): GenericField[T] = new GenericField(fieldName, t, () => cb, hidden) object semiauto { - final def deriveObjectCursorBuilder[T](tpe: Type) - (implicit mkBuilder: => MkObjectCursorBuilder[T]): ObjectCursorBuilder[T] = mkBuilder(tpe) - final def deriveInterfaceCursorBuilder[T](tpe: Type) - (implicit mkBuilder: => MkInterfaceCursorBuilder[T]): CursorBuilder[T] = mkBuilder(tpe) + final def deriveObjectCursorBuilder[T](tpe: Type)( + implicit mkBuilder: => MkObjectCursorBuilder[T]): ObjectCursorBuilder[T] = mkBuilder( + tpe) + final def deriveInterfaceCursorBuilder[T](tpe: Type)( + implicit mkBuilder: => MkInterfaceCursorBuilder[T]): CursorBuilder[T] = mkBuilder(tpe) } trait ObjectCursorBuilder[T] extends CursorBuilder[T] { def renameField(from: String, to: String): ObjectCursorBuilder[T] def transformFieldNames(f: String => String): ObjectCursorBuilder[T] - def transformField[U](fieldName: String)(f: T => Result[U])(implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] + def transformField[U](fieldName: String)(f: T => Result[U])( + implicit cb: => CursorBuilder[U]): ObjectCursorBuilder[T] } } diff --git a/modules/generic/src/test/scala/DerivationSuite.scala b/modules/generic/src/test/scala/DerivationSuite.scala index 12b80f5f..73b0c4a5 100644 --- a/modules/generic/src/test/scala/DerivationSuite.scala +++ b/modules/generic/src/test/scala/DerivationSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import java.time._ @@ -24,14 +23,19 @@ import io.circe.Json import io.circe.literal._ import munit.CatsEffectSuite +import grackle.{Context, Result} +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.ScalarType._ +import grackle.Value._ +import grackle.generic.StarWarsData._ import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ -import ScalarType._ object StarWarsData { - import StarWarsMapping._ - import semiauto._ + import StarWarsMapping.semiauto._ + + import grackle.generic.StarWarsMapping._ object Episode extends Enumeration { val NEWHOPE, EMPIRE, JEDI = Value @@ -53,39 +57,39 @@ object StarWarsData { c.friends match { case None => None.success case Some(ids) => - ids.traverse(id => characters.find(_.id == id).toResultOrError(s"Bad id '$id'")).map(_.some) + ids + .traverse(id => characters.find(_.id == id).toResultOrError(s"Bad id '$id'")) + .map(_.some) } case class Planet(value: String) case class Human( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - homePlanet: Option[Planet] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + homePlanet: Option[Planet] ) extends Character object Human { implicit val planetCursorBuilder: CursorBuilder[Planet] = CursorBuilder[String].contramap(_.value) implicit val cursorBuilder: CursorBuilder[Human] = - deriveObjectCursorBuilder[Human](HumanType) - .transformField("friends")(resolveFriends) + deriveObjectCursorBuilder[Human](HumanType).transformField("friends")(resolveFriends) } case class Droid( - id: String, - name: Option[String], - appearsIn: Option[List[Episode.Value]], - friends: Option[List[String]], - primaryFunction: Option[String] + id: String, + name: Option[String], + appearsIn: Option[List[Episode.Value]], + friends: Option[List[String]], + primaryFunction: Option[String] ) extends Character object Droid { implicit val cursorBuilder: CursorBuilder[Droid] = - deriveObjectCursorBuilder[Droid](DroidType) - .transformField("friends")(resolveFriends) + deriveObjectCursorBuilder[Droid](DroidType).transformField("friends")(resolveFriends) } val characters: List[Character] = List( @@ -140,21 +144,19 @@ object StarWarsData { ) ) - import Episode._ - - val Some(lukeSkywalker: Human) = characters.find(_.id == "1000") : @unchecked - val Some(r2d2: Droid) = characters.find(_.id == "2001") : @unchecked + val Some(lukeSkywalker: Human) = characters.find(_.id == "1000"): @unchecked + val Some(r2d2: Droid) = characters.find(_.id == "2001"): @unchecked // Mapping from Episode to its hero Character - lazy val hero: Map[Value, Character] = Map( - NEWHOPE -> r2d2, - EMPIRE -> lukeSkywalker, - JEDI -> r2d2 + lazy val hero: Map[Episode.Value, Character] = Map( + Episode.NEWHOPE -> r2d2, + Episode.EMPIRE -> lukeSkywalker, + Episode.JEDI -> r2d2 ) } object StarWarsMapping extends GenericMapping[IO] { - import StarWarsData.{characters, hero, Droid, Human, Episode} + import grackle.generic.StarWarsData.{characters, hero, Droid, Episode, Human} val schema = schema""" @@ -204,21 +206,22 @@ object StarWarsMapping extends GenericMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - GenericField("hero", characters), - GenericField("character", characters), - GenericField("human", characters.collect { case h: Human => h }), - GenericField("droid", characters.collect { case d: Droid => d }) - ) + fieldMappings = List( + GenericField("hero", characters), + GenericField("character", characters), + GenericField("human", characters.collect { case h: Human => h }), + GenericField("droid", characters.collect { case d: Droid => d }) + ) ) ) override val selectElaborator = SelectElaborator { case (QueryType, "hero", List(Binding("episode", EnumValue(e)))) => for { - episode <- Elab.liftR(Episode.values.find(_.toString == e).toResult(s"Unknown episode '$e'")) - _ <- Elab.transformChild(child => Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))) + episode <- Elab.liftR( + Episode.values.find(_.toString == e).toResult(s"Unknown episode '$e'")) + _ <- Elab.transformChild(child => + Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))) } yield () case (QueryType, "character" | "human" | "droid", List(Binding("id", IDValue(id)))) => @@ -229,39 +232,37 @@ object StarWarsMapping extends GenericMapping[IO] { } } -import StarWarsData._, StarWarsMapping._ - final class DerivationSuite extends CatsEffectSuite { test("primitive types have leaf cursor builders") { val i = for { c <- CursorBuilder[Int].build(Context(IntType), 23) - _ = assert(c.isLeaf) - l <- c.asLeaf + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(i, Result.Success(Json.fromInt(23))) val s = for { c <- CursorBuilder[String].build(Context(StringType), "foo") - _ = assert(c.isLeaf) - l <- c.asLeaf + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(s, Result.Success(Json.fromString("foo"))) val b = for { c <- CursorBuilder[Boolean].build(Context(BooleanType), true) - _ = assert(c.isLeaf) - l <- c.asLeaf + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(b, Result.Success(Json.fromBoolean(true))) val f = for { c <- CursorBuilder[Double].build(Context(FloatType), 13.0) - _ = assert(c.isLeaf) - l <- c.asLeaf + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(f, Result.Success(Json.fromDouble(13.0).get)) } @@ -269,9 +270,11 @@ final class DerivationSuite extends CatsEffectSuite { test("types with a Circe Encoder instance have leaf cursor builders") { val z = for { - c <- CursorBuilder.leafCursorBuilder[ZonedDateTime].build(Context(AttributeType), ZonedDateTime.parse("2020-03-25T16:24:06.081Z")) - _ = assert(c.isLeaf) - l <- c.asLeaf + c <- CursorBuilder + .leafCursorBuilder[ZonedDateTime] + .build(Context(AttributeType), ZonedDateTime.parse("2020-03-25T16:24:06.081Z")) + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(z, Result.Success(Json.fromString("2020-03-25T16:24:06.081Z"))) } @@ -279,9 +282,11 @@ final class DerivationSuite extends CatsEffectSuite { test("Scala Enumeration types have leaf cursor builders") { val e = for { - c <- CursorBuilder.enumerationCursorBuilder[Episode.Value].build(Context(EpisodeType), Episode.JEDI) - _ = assert(c.isLeaf) - l <- c.asLeaf + c <- CursorBuilder + .enumerationCursorBuilder[Episode.Value] + .build(Context(StarWarsMapping.EpisodeType), Episode.JEDI) + _ = assert(c.isLeaf) + l <- c.asLeaf } yield l assertEquals(e, Result.Success(Json.fromString("JEDI"))) } @@ -289,7 +294,7 @@ final class DerivationSuite extends CatsEffectSuite { test("product types have cursor builders") { val name = for { - c <- CursorBuilder[Human].build(Context(HumanType), lukeSkywalker) + c <- CursorBuilder[Human].build(Context(StarWarsMapping.HumanType), lukeSkywalker) f <- c.field("name", None) n <- f.asNullable.flatMap(_.toResultOrError("missing")) l <- n.asLeaf @@ -300,19 +305,23 @@ final class DerivationSuite extends CatsEffectSuite { test("cursor builders can be resolved for nested types") { val appearsIn = for { - c <- CursorBuilder[Character].build(Context(CharacterType), lukeSkywalker) + c <- CursorBuilder[Character] + .build(Context(StarWarsMapping.CharacterType), lukeSkywalker) f <- c.field("appearsIn", None) n <- f.asNullable.flatMap(_.toResultOrError("missing")) l <- n.asList(List) s <- l.traverse(_.asLeaf) } yield s - assertEquals(appearsIn, Result.Success(List(Json.fromString("NEWHOPE"), Json.fromString("EMPIRE"), Json.fromString("JEDI")))) + assertEquals( + appearsIn, + Result.Success( + List(Json.fromString("NEWHOPE"), Json.fromString("EMPIRE"), Json.fromString("JEDI")))) } test("default cursor builders can be customised by mapping fields") { val friends = for { - c <- CursorBuilder[Human].build(Context(HumanType), lukeSkywalker) + c <- CursorBuilder[Human].build(Context(StarWarsMapping.HumanType), lukeSkywalker) f <- c.field("friends", None) n <- f.asNullable.flatMap(_.toResultOrError("missing")) l <- n.asList(List) @@ -320,14 +329,22 @@ final class DerivationSuite extends CatsEffectSuite { p <- m.traverse(_.asNullable.flatMap(_.toResultOrError("missing"))) q <- p.traverse(_.asLeaf) } yield q - assertEquals(friends, Result.Success(List(Json.fromString("Han Solo"), Json.fromString("Leia Organa"), Json.fromString("C-3PO"), Json.fromString("R2-D2")))) + assertEquals( + friends, + Result.Success( + List( + Json.fromString("Han Solo"), + Json.fromString("Leia Organa"), + Json.fromString("C-3PO"), + Json.fromString("R2-D2")))) } test("sealed ADTs have narrowable cursor builders") { val homePlanets = for { - c <- CursorBuilder[Character].build(Context(CharacterType), lukeSkywalker) - h <- c.narrow(HumanType) + c <- CursorBuilder[Character] + .build(Context(StarWarsMapping.CharacterType), lukeSkywalker) + h <- c.narrow(StarWarsMapping.HumanType) m <- h.field("homePlanet", None) n <- m.asNullable.flatMap(_.toResultOrError("missing")) l <- n.asLeaf diff --git a/modules/generic/src/test/scala/EffectsSuite.scala b/modules/generic/src/test/scala/EffectsSuite.scala index fe379e62..07ecc3b3 100644 --- a/modules/generic/src/test/scala/EffectsSuite.scala +++ b/modules/generic/src/test/scala/EffectsSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import cats.effect.{IO, Sync} import cats.implicits._ @@ -51,15 +50,15 @@ class GenericEffectMapping[F[_]: Sync](ref: SignallingRef[F, Int]) extends Gener val typeMappings = List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - // Compute a ValueCursor - RootEffect.computeCursor("foo")((p, e) => - ref.update(_+1).as( + fieldMappings = List( + // Compute a ValueCursor + RootEffect.computeCursor("foo")((p, e) => + ref + .update(_ + 1) + .as( genericCursor(p, e, Struct(42, "hi")) - ) - ) - ) + )) + ) ) ) } @@ -88,10 +87,10 @@ final class EffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new GenericEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new GenericEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) @@ -120,10 +119,10 @@ final class EffectSuite extends CatsEffectSuite { val prg: IO[(Json, Int)] = for { - ref <- SignallingRef[IO, Int](0) - map = new GenericEffectMapping(ref) - res <- map.compileAndRun(query) - eff <- ref.get + ref <- SignallingRef[IO, Int](0) + map = new GenericEffectMapping(ref) + res <- map.compileAndRun(query) + eff <- ref.get } yield (res, eff) assertIO(prg, (expected, 1)) diff --git a/modules/generic/src/test/scala/RecursionSuite.scala b/modules/generic/src/test/scala/RecursionSuite.scala index a6b55145..1eefd0da 100644 --- a/modules/generic/src/test/scala/RecursionSuite.scala +++ b/modules/generic/src/test/scala/RecursionSuite.scala @@ -13,17 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import cats.effect.IO import cats.implicits._ import io.circe.literal._ import munit.CatsEffectSuite +import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ object MutualRecursionData { import MutualRecursionMapping._ @@ -39,7 +41,9 @@ object MutualRecursionData { p.productions match { case None => None.success case Some(ids) => - ids.traverse(id => productions.find(_.id == id).toResultOrError(s"Bad id '$id'")).map(_.some) + ids + .traverse(id => productions.find(_.id == id).toResultOrError(s"Bad id '$id'")) + .map(_.some) } } @@ -86,11 +90,10 @@ object MutualRecursionMapping extends GenericMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - GenericField("programmeById", programmes), - GenericField("productionById", productions) - ) + fieldMappings = List( + GenericField("programmeById", programmes), + GenericField("productionById", productions) + ) ) ) diff --git a/modules/generic/src/test/scala/ScalarsSuite.scala b/modules/generic/src/test/scala/ScalarsSuite.scala index f1d56446..5a1cf6d0 100644 --- a/modules/generic/src/test/scala/ScalarsSuite.scala +++ b/modules/generic/src/test/scala/ScalarsSuite.scala @@ -13,11 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package generic +package grackle.generic import java.time.{Duration, LocalDate, LocalTime, OffsetDateTime} import java.util.UUID + import scala.util.Try import cats.{Eq, Order} @@ -27,11 +27,14 @@ import io.circe.Encoder import io.circe.literal._ import munit.CatsEffectSuite +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ object MovieData { + import Genre.{Action, Comedy, Drama} import MovieMapping._ import semiauto._ @@ -45,7 +48,7 @@ object MovieData { def fromString(s: String): Option[Genre] = s.trim.toUpperCase match { - case "DRAMA" => Some(Drama) + case "DRAMA" => Some(Drama) case "ACTION" => Some(Action) case "COMEDY" => Some(Comedy) case _ => None @@ -74,16 +77,14 @@ object MovieData { implicit val durationOrder: Order[Duration] = Order.fromComparable[Duration] - import Genre.{Action, Comedy, Drama} - case class Movie( - id: UUID, - title: String, - genre: Genre, - releaseDate: LocalDate, - showTime: LocalTime, - nextShowing: OffsetDateTime, - duration: Duration + id: UUID, + title: String, + genre: Genre, + releaseDate: LocalDate, + showTime: LocalTime, + nextShowing: OffsetDateTime, + duration: Duration ) object Movie { @@ -93,16 +94,96 @@ object MovieData { val movies = List( - Movie(UUID.fromString("6a7837fc-b463-4d32-b628-0f4b3065cb21"), "Celine et Julie Vont en Bateau", Drama, LocalDate.parse("1974-10-07"), LocalTime.parse("19:35:00"), OffsetDateTime.parse("2020-05-22T19:35:00Z"), Duration.ofMillis(12300000)), - Movie(UUID.fromString("11daf8c0-11c3-4453-bfe1-cb6e6e2f9115"), "Duelle", Drama, LocalDate.parse("1975-09-15"), LocalTime.parse("19:20:00"), OffsetDateTime.parse("2020-05-27T19:20:00Z"), Duration.ofMillis(7260000)), - Movie(UUID.fromString("aea9756f-621b-42d5-b130-71f3916c4ba3"), "L'Amour fou", Drama, LocalDate.parse("1969-01-15"), LocalTime.parse("21:00:00"), OffsetDateTime.parse("2020-05-27T21:00:00Z"), Duration.ofMillis(15120000)), - Movie(UUID.fromString("2ddb041f-86c2-4bd3-848c-990a3862634e"), "Last Year at Marienbad", Drama, LocalDate.parse("1961-06-25"), LocalTime.parse("20:30:00"), OffsetDateTime.parse("2020-05-26T20:30:00Z"), Duration.ofMillis(5640000)), - Movie(UUID.fromString("8ae5b13b-044c-4ff0-8b71-ccdb7d77cd88"), "Zazie dans le Métro", Comedy, LocalDate.parse("1960-10-28"), LocalTime.parse("20:15:00"), OffsetDateTime.parse("2020-05-25T20:15:00Z"), Duration.ofMillis(5340000)), - Movie(UUID.fromString("9dce9deb-9188-4cc2-9685-9842b8abdd34"), "Alphaville", Action, LocalDate.parse("1965-05-05"), LocalTime.parse("19:45:00"), OffsetDateTime.parse("2020-05-19T19:45:00Z"), Duration.ofMillis(5940000)), - Movie(UUID.fromString("1bf00ac6-91ab-4e51-b686-3fd5e2324077"), "Stalker", Drama, LocalDate.parse("1979-05-13"), LocalTime.parse("15:30:00"), OffsetDateTime.parse("2020-05-19T15:30:00Z"), Duration.ofMillis(9660000)), - Movie(UUID.fromString("6a878e06-6563-4a0c-acd9-d28dcfb2e91a"), "Weekend", Comedy, LocalDate.parse("1967-12-29"), LocalTime.parse("22:30:00"), OffsetDateTime.parse("2020-05-19T22:30:00Z"), Duration.ofMillis(6300000)), - Movie(UUID.fromString("2a40415c-ea6a-413f-bbef-a80ae280c4ff"), "Daisies", Comedy, LocalDate.parse("1966-12-30"), LocalTime.parse("21:30:00"), OffsetDateTime.parse("2020-05-15T21:30:00Z"), Duration.ofMillis(4560000)), - Movie(UUID.fromString("2f6dcb0a-4122-4a21-a1c6-534744dd6b85"), "Le Pont du Nord", Drama, LocalDate.parse("1982-01-13"), LocalTime.parse("20:45:00"), OffsetDateTime.parse("2020-05-11T20:45:00Z"), Duration.ofMillis(7620000)) + Movie( + UUID.fromString("6a7837fc-b463-4d32-b628-0f4b3065cb21"), + "Celine et Julie Vont en Bateau", + Drama, + LocalDate.parse("1974-10-07"), + LocalTime.parse("19:35:00"), + OffsetDateTime.parse("2020-05-22T19:35:00Z"), + Duration.ofMillis(12300000) + ), + Movie( + UUID.fromString("11daf8c0-11c3-4453-bfe1-cb6e6e2f9115"), + "Duelle", + Drama, + LocalDate.parse("1975-09-15"), + LocalTime.parse("19:20:00"), + OffsetDateTime.parse("2020-05-27T19:20:00Z"), + Duration.ofMillis(7260000) + ), + Movie( + UUID.fromString("aea9756f-621b-42d5-b130-71f3916c4ba3"), + "L'Amour fou", + Drama, + LocalDate.parse("1969-01-15"), + LocalTime.parse("21:00:00"), + OffsetDateTime.parse("2020-05-27T21:00:00Z"), + Duration.ofMillis(15120000) + ), + Movie( + UUID.fromString("2ddb041f-86c2-4bd3-848c-990a3862634e"), + "Last Year at Marienbad", + Drama, + LocalDate.parse("1961-06-25"), + LocalTime.parse("20:30:00"), + OffsetDateTime.parse("2020-05-26T20:30:00Z"), + Duration.ofMillis(5640000) + ), + Movie( + UUID.fromString("8ae5b13b-044c-4ff0-8b71-ccdb7d77cd88"), + "Zazie dans le Métro", + Comedy, + LocalDate.parse("1960-10-28"), + LocalTime.parse("20:15:00"), + OffsetDateTime.parse("2020-05-25T20:15:00Z"), + Duration.ofMillis(5340000) + ), + Movie( + UUID.fromString("9dce9deb-9188-4cc2-9685-9842b8abdd34"), + "Alphaville", + Action, + LocalDate.parse("1965-05-05"), + LocalTime.parse("19:45:00"), + OffsetDateTime.parse("2020-05-19T19:45:00Z"), + Duration.ofMillis(5940000) + ), + Movie( + UUID.fromString("1bf00ac6-91ab-4e51-b686-3fd5e2324077"), + "Stalker", + Drama, + LocalDate.parse("1979-05-13"), + LocalTime.parse("15:30:00"), + OffsetDateTime.parse("2020-05-19T15:30:00Z"), + Duration.ofMillis(9660000) + ), + Movie( + UUID.fromString("6a878e06-6563-4a0c-acd9-d28dcfb2e91a"), + "Weekend", + Comedy, + LocalDate.parse("1967-12-29"), + LocalTime.parse("22:30:00"), + OffsetDateTime.parse("2020-05-19T22:30:00Z"), + Duration.ofMillis(6300000) + ), + Movie( + UUID.fromString("2a40415c-ea6a-413f-bbef-a80ae280c4ff"), + "Daisies", + Comedy, + LocalDate.parse("1966-12-30"), + LocalTime.parse("21:30:00"), + OffsetDateTime.parse("2020-05-15T21:30:00Z"), + Duration.ofMillis(4560000) + ), + Movie( + UUID.fromString("2f6dcb0a-4122-4a21-a1c6-534744dd6b85"), + "Le Pont du Nord", + Drama, + LocalDate.parse("1982-01-13"), + LocalTime.parse("20:45:00"), + OffsetDateTime.parse("2020-05-11T20:45:00Z"), + Duration.ofMillis(7620000) + ) ).sortBy(_.id.toString) } @@ -148,15 +229,14 @@ object MovieMapping extends GenericMapping[IO] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - GenericField("movieById", movies), - GenericField("moviesByGenre", movies), - GenericField("moviesReleasedBetween", movies), - GenericField("moviesLongerThan", movies), - GenericField("moviesShownLaterThan", movies), - GenericField("moviesShownBetween", movies) - ) + fieldMappings = List( + GenericField("movieById", movies), + GenericField("moviesByGenre", movies), + GenericField("moviesReleasedBetween", movies), + GenericField("moviesLongerThan", movies), + GenericField("moviesShownLaterThan", movies), + GenericField("moviesShownBetween", movies) + ) ) ) @@ -195,7 +275,10 @@ object MovieMapping extends GenericMapping[IO] { Elab.transformChild(child => Unique(Filter(Eql(MovieType / "id", Const(id)), child))) case (QueryType, "moviesByGenre", List(Binding("genre", GenreValue(genre)))) => Elab.transformChild(child => Filter(Eql(MovieType / "genre", Const(genre)), child)) - case (QueryType, "moviesReleasedBetween", List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => + case ( + QueryType, + "moviesReleasedBetween", + List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => Elab.transformChild(child => Filter( And( @@ -203,23 +286,23 @@ object MovieMapping extends GenericMapping[IO] { Lt(MovieType / "releaseDate", Const(to)) ), child - ) - ) + )) case (QueryType, "moviesLongerThan", List(Binding("duration", IntervalValue(duration)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "duration", Const(duration))), child - ) - ) + )) case (QueryType, "moviesShownLaterThan", List(Binding("time", TimeValue(time)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "showTime", Const(time))), child - ) - ) - case (QueryType, "moviesShownBetween", List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => + )) + case ( + QueryType, + "moviesShownBetween", + List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => Elab.transformChild(child => Filter( And( @@ -227,8 +310,7 @@ object MovieMapping extends GenericMapping[IO] { Lt(MovieType / "nextShowing", Const(to)) ), child - ) - ) + )) } } diff --git a/modules/skunk/js-jvm/src/test/scala/SkunkDatabaseSuite.scala b/modules/skunk/js-jvm/src/test/scala/SkunkDatabaseSuite.scala index 565efff6..451bb247 100644 --- a/modules/skunk/js-jvm/src/test/scala/SkunkDatabaseSuite.scala +++ b/modules/skunk/js-jvm/src/test/scala/SkunkDatabaseSuite.scala @@ -25,12 +25,11 @@ import org.typelevel.otel4s.metrics.Meter import org.typelevel.otel4s.metrics.Meter.Implicits.noop as metricsNoop import org.typelevel.otel4s.trace.Tracer import org.typelevel.otel4s.trace.Tracer.Implicits.noop -import skunk.{ Codec => SCodec, Session } -import skunk.codec.{ all => codec } -import skunk.circe.codec.{ all => ccodec } - -import grackle._, skunk._ +import skunk.{Codec => SCodec, Session} +import skunk.circe.codec.{all => ccodec} +import skunk.codec.{all => codec} +import grackle.skunk._ import grackle.sql.test._ import grackle.sqlpg.test._ @@ -43,7 +42,8 @@ trait SkunkDatabaseSuite extends SqlPgDatabaseSuite { val connInfo = postgresConnectionInfo import connInfo._ - Session.Builder[IO] + Session + .Builder[IO] .withHost(host) .withPort(port) .withUserAndPassword(username, password) @@ -52,27 +52,33 @@ trait SkunkDatabaseSuite extends SqlPgDatabaseSuite { .pooled(max = 3) } - val poolFixture: IOFixture[Resource[IO, Session[IO]]] = ResourceSuiteLocalFixture("skunk", poolResource) + val poolFixture: IOFixture[Resource[IO, Session[IO]]] = + ResourceSuiteLocalFixture("skunk", poolResource) override def munitFixtures: Seq[IOFixture[_]] = Seq(poolFixture) def pool: Resource[IO, Session[IO]] = poolFixture() - abstract class SkunkTestMapping[F[_]: Sync](pool: Resource[F, Session[F]], monitor: SkunkMonitor[F] = SkunkMonitor.noopMonitor[IO]) - extends SkunkMapping[F](pool, monitor) with SqlTestMapping[F] { + abstract class SkunkTestMapping[F[_]: Sync]( + pool: Resource[F, Session[F]], + monitor: SkunkMonitor[F] = SkunkMonitor.noopMonitor[IO]) + extends SkunkMapping[F](pool, monitor) + with SqlTestMapping[F] { type TestCodec[T] = (SCodec[T], Boolean) def bool: TestCodec[Boolean] = (codec.bool, false) def text: TestCodec[String] = (codec.text, false) def varchar: TestCodec[String] = (codec.varchar, false) - def nvarchar: TestCodec[String] = (codec.text, false) // For compatbiltity with Oracle in these Suites. + def nvarchar: TestCodec[String] = + (codec.text, false) // For compatbiltity with Oracle in these Suites. def bpchar(len: Int): TestCodec[String] = (codec.bpchar(len), false) def int2: TestCodec[Int] = (codec.int2.imap(_.toInt)(_.toShort), false) def int4: TestCodec[Int] = (codec.int4, false) def int8: TestCodec[Long] = (codec.int8, false) def float4: TestCodec[Float] = (codec.float4, false) def float8: TestCodec[Double] = (codec.float8, false) - def numeric(precision: Int, scale: Int): TestCodec[BigDecimal] = (codec.numeric(precision, scale), false) + def numeric(precision: Int, scale: Int): TestCodec[BigDecimal] = + (codec.numeric(precision, scale), false) def uuid: TestCodec[UUID] = (codec.uuid, false) def localDate: TestCodec[LocalDate] = (codec.date, false) @@ -84,9 +90,9 @@ trait SkunkDatabaseSuite extends SqlPgDatabaseSuite { def nullable[T](c: TestCodec[T]): TestCodec[T] = (c._1.opt, true).asInstanceOf[TestCodec[T]] - def list[T: CDecoder : CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { + def list[T: CDecoder: CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { val cc = c._1 - val ty = _root_.skunk.data.Type(s"_${cc.types.head.name}", cc.types) + val ty = skunk.data.Type(s"_${cc.types.head.name}", cc.types) val encode = (elem: T) => cc.encode(elem).head.get.value val decode = (str: String) => cc.decode(0, List(Some(str))).left.map(_.message) (SCodec.array(encode, decode, ty), false).asInstanceOf[TestCodec[List[T]]] diff --git a/modules/skunk/js-jvm/src/test/scala/SkunkSuites.scala b/modules/skunk/js-jvm/src/test/scala/SkunkSuites.scala index d4ed3489..1152f8a6 100644 --- a/modules/skunk/js-jvm/src/test/scala/SkunkSuites.scala +++ b/modules/skunk/js-jvm/src/test/scala/SkunkSuites.scala @@ -17,18 +17,16 @@ package grackle.skunk.test import cats.effect.{IO, Resource} import munit.catseffect.IOFixture +import org.typelevel.twiddles._ import skunk.Session import skunk.codec.{all => codec} import skunk.data.Encoded import skunk.implicits._ +import grackle.Mapping import grackle.skunk.SkunkMonitor import grackle.sql.SqlStatsMonitor - import grackle.sql.test._ -import grackle.Mapping - -import org.typelevel.twiddles._ final class ArrayJoinSuite extends SkunkDatabaseSuite with SqlArrayJoinSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlArrayJoinMapping[IO] @@ -36,15 +34,21 @@ final class ArrayJoinSuite extends SkunkDatabaseSuite with SqlArrayJoinSuite { final class CoalesceSuite extends SkunkDatabaseSuite with SqlCoalesceSuite { type Fragment = skunk.AppliedFragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - SkunkMonitor.statsMonitor[IO].map(mon => (new SkunkTestMapping(pool, mon) with SqlCoalesceMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + SkunkMonitor + .statsMonitor[IO] + .map(mon => (new SkunkTestMapping(pool, mon) with SqlCoalesceMapping[IO], mon)) } final class ComposedWorldSuite extends SkunkDatabaseSuite with SqlComposedWorldSuite { def mapping: IO[(CurrencyMapping[IO], Mapping[IO])] = for { currencyMapping <- CurrencyMapping[IO] - } yield (currencyMapping, new SqlComposedMapping(new SkunkTestMapping(pool) with SqlWorldMapping[IO], currencyMapping)) + } yield ( + currencyMapping, + new SqlComposedMapping( + new SkunkTestMapping(pool) with SqlWorldMapping[IO], + currencyMapping)) } final class CompositeKeySuite extends SkunkDatabaseSuite with SqlCompositeKeySuite { @@ -71,11 +75,15 @@ final class FilterJoinAliasSuite extends SkunkDatabaseSuite with SqlFilterJoinAl lazy val mapping = new SkunkTestMapping(pool) with SqlFilterJoinAliasMapping[IO] } -final class FilterOrderOffsetLimitSuite extends SkunkDatabaseSuite with SqlFilterOrderOffsetLimitSuite { +final class FilterOrderOffsetLimitSuite + extends SkunkDatabaseSuite + with SqlFilterOrderOffsetLimitSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlFilterOrderOffsetLimitMapping[IO] } -final class FilterOrderOffsetLimit2Suite extends SkunkDatabaseSuite with SqlFilterOrderOffsetLimit2Suite { +final class FilterOrderOffsetLimit2Suite + extends SkunkDatabaseSuite + with SqlFilterOrderOffsetLimit2Suite { lazy val mapping = new SkunkTestMapping(pool) with SqlFilterOrderOffsetLimit2Mapping[IO] } @@ -107,17 +115,22 @@ final class LikeSuite extends SkunkDatabaseSuite with SqlLikeSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlLikeMapping[IO] } -final class MappingValidatorValidSuite extends SkunkDatabaseSuite with SqlMappingValidatorValidSuite { +final class MappingValidatorValidSuite + extends SkunkDatabaseSuite + with SqlMappingValidatorValidSuite { // no DB instance needed for this suite lazy val mapping = new SkunkTestMapping(null) with SqlMappingValidatorValidMapping[IO] { def genre: TestCodec[Genre] = (codec.int4.imap(Genre.fromInt)(Genre.toInt), false) - def feature: TestCodec[Feature] = (codec.varchar.imap(Feature.fromString)(_.toString), false) + def feature: TestCodec[Feature] = + (codec.varchar.imap(Feature.fromString)(_.toString), false) } override def munitFixtures: Seq[IOFixture[_]] = Nil } -final class MappingValidatorInvalidSuite extends SkunkDatabaseSuite with SqlMappingValidatorInvalidSuite { +final class MappingValidatorInvalidSuite + extends SkunkDatabaseSuite + with SqlMappingValidatorInvalidSuite { // no DB instance needed for this suite lazy val mapping = new SkunkTestMapping(null) with SqlMappingValidatorInvalidMapping[IO] override def munitFixtures: Seq[IOFixture[_]] = Nil @@ -131,16 +144,19 @@ final class MovieSuite extends SkunkDatabaseSuite with SqlMovieSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlMovieMapping[IO] { def genre: TestCodec[Genre] = (codec.int4.imap(Genre.fromInt)(Genre.toInt), false) - def feature: TestCodec[Feature] = (codec.varchar.imap(Feature.fromString)(_.toString), false) + def feature: TestCodec[Feature] = + (codec.varchar.imap(Feature.fromString)(_.toString), false) def tagList: TestCodec[List[String]] = (codec.int4.imap(Tags.fromInt)(Tags.toInt), false) } } final class MutationSuite extends SkunkDatabaseSuite with SqlMutationSuite { // A resource that copies and drops the table used in the tests. - def withDuplicatedTables(p: Resource[IO, Session[IO]]): Resource[IO, Resource[IO, Session[IO]]] = { - val alloc = p.use(_.execute(sql"CREATE TABLE city_copy AS SELECT * FROM city".command)).as(p) - val free = p.use(_.execute(sql"DROP TABLE city_copy".command)).void + def withDuplicatedTables( + p: Resource[IO, Session[IO]]): Resource[IO, Resource[IO, Session[IO]]] = { + val alloc = + p.use(_.execute(sql"CREATE TABLE city_copy AS SELECT * FROM city".command)).as(p) + val free = p.use(_.execute(sql"DROP TABLE city_copy".command)).void Resource.make(alloc)(_ => free) } @@ -150,14 +166,16 @@ final class MutationSuite extends SkunkDatabaseSuite with SqlMutationSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlMutationMapping[IO] { def updatePopulation(id: Int, population: Int): IO[Unit] = - pool.use(_.prepareR(sql"UPDATE city_copy SET population=${codec.int4} WHERE id=${codec.int4}".command).use { ps => - ps.execute(population *: id *: EmptyTuple).void - }) + pool.use( + _.prepareR( + sql"UPDATE city_copy SET population=${codec.int4} WHERE id=${codec.int4}".command) + .use { ps => ps.execute(population *: id *: EmptyTuple).void }) def createCity(name: String, countryCode: String, population: Int): IO[Int] = { val q = sql""" INSERT INTO city_copy (id, name, countrycode, district, population) - VALUES (nextval('city_id'), ${codec.varchar}, ${codec.bpchar(3)}, 'ignored', ${codec.int4}) + VALUES (nextval('city_id'), ${codec.varchar}, ${codec.bpchar( + 3)}, 'ignored', ${codec.int4}) RETURNING id """.query(codec.int4) pool.use(_.prepareR(q).use { ps => @@ -196,7 +214,9 @@ final class ProjectionSuite extends SkunkDatabaseSuite with SqlProjectionSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlProjectionMapping[IO] } -final class RecursiveInterfacesSuite extends SkunkDatabaseSuite with SqlRecursiveInterfacesSuite { +final class RecursiveInterfacesSuite + extends SkunkDatabaseSuite + with SqlRecursiveInterfacesSuite { lazy val mapping = new SkunkTestMapping(pool) with SqlRecursiveInterfacesMapping[IO] { def itemType: TestCodec[ItemType] = @@ -223,8 +243,10 @@ final class WorldSuite extends SkunkDatabaseSuite with SqlWorldSuite { final class WorldCompilerSuite extends SkunkDatabaseSuite with SqlWorldCompilerSuite { type Fragment = skunk.AppliedFragment - def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO,Fragment])] = - SkunkMonitor.statsMonitor[IO].map(mon => (new SkunkTestMapping(pool, mon) with SqlWorldMapping[IO], mon)) + def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] = + SkunkMonitor + .statsMonitor[IO] + .map(mon => (new SkunkTestMapping(pool, mon) with SqlWorldMapping[IO], mon)) def simpleRestrictedQuerySql: String = "SELECT country.code, country.name FROM country WHERE ((country.code = $1))" diff --git a/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionMapping.scala b/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionMapping.scala index d9360b47..c6de9a42 100644 --- a/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionMapping.scala +++ b/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionMapping.scala @@ -21,12 +21,12 @@ import skunk.codec.all._ import skunk.implicits._ import grackle._ -import syntax._ -import Predicate._ -import Query._ -import QueryCompiler._ -import skunk._ -import Value._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.skunk._ +import grackle.syntax._ trait SubscriptionMapping[F[_]] extends SkunkMapping[F] { @@ -55,32 +55,32 @@ trait SubscriptionMapping[F[_]] extends SkunkMapping[F] { } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", text) - val population = col("population", int4) + val name = col("name", text) + val population = col("population", int4) } - val QueryType = schema.ref("Query") + val QueryType = schema.ref("Query") val SubscriptionType = schema.ref("Subscription") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") val typeMappings = List( ObjectMapping( tpe = QueryType, fieldMappings = List( - SqlObject("city"), - ), + SqlObject("city") + ) ), ObjectMapping( tpe = CountryType, fieldMappings = List( SqlField("code", country.code, key = true, hidden = true), - SqlField("name", country.name), - SqlObject("cities", Join(country.code, city.countrycode)), - ), + SqlField("name", country.name), + SqlObject("cities", Join(country.code, city.countrycode)) + ) ), ObjectMapping( tpe = CityType, @@ -89,7 +89,7 @@ trait SubscriptionMapping[F[_]] extends SkunkMapping[F] { SqlField("countrycode", city.countrycode, hidden = true), SqlField("name", city.name), SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlObject("country", Join(city.countrycode, country.code)) ) ), ObjectMapping( @@ -97,10 +97,9 @@ trait SubscriptionMapping[F[_]] extends SkunkMapping[F] { fieldMappings = List( RootStream.computeChild("channel")((child, _, _) => for { - s <- fs2.Stream.resource(pool) + s <- fs2.Stream.resource(pool) id <- s.channel(id"city_channel").listen(256).map(_.value.toInt) - } yield Unique(Filter(Eql(CityType / "id", Const(id)), child)).success - ) + } yield Unique(Filter(Eql(CityType / "id", Const(id)), child)).success) ) ) ) @@ -112,6 +111,8 @@ trait SubscriptionMapping[F[_]] extends SkunkMapping[F] { } object SubscriptionMapping extends SkunkMappingCompanion { - def mkMapping[F[_]: Sync](pool: Resource[F, Session[F]], monitor: SkunkMonitor[F]): Mapping[F] = + def mkMapping[F[_]: Sync]( + pool: Resource[F, Session[F]], + monitor: SkunkMonitor[F]): Mapping[F] = new SkunkMapping[F](pool, monitor) with SubscriptionMapping[F] } diff --git a/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionSuite.scala b/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionSuite.scala index d30acaff..644fb7e3 100644 --- a/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionSuite.scala +++ b/modules/skunk/js-jvm/src/test/scala/subscription/SubscriptionSuite.scala @@ -66,7 +66,7 @@ class SubscriptionSuite extends SkunkDatabaseSuite { } } } - """, + """ ) val prog: IO[List[Json]] = @@ -76,17 +76,17 @@ class SubscriptionSuite extends SkunkDatabaseSuite { fi <- mapping.compileAndRunSubscription(query).take(2).compile.toList.start // We're racing now, so wait a sec before starting notifications - _ <- IO.sleep(1.second) + _ <- IO.sleep(1.second) // Send some notifications through Postgres, which will trigger queries on the subscription. - _ <- pool.use { s => - val ch = s.channel(id"city_channel").contramap[Int](_.toString) - List(101, 102, 103).traverse_(ch.notify) - } + _ <- pool.use { s => + val ch = s.channel(id"city_channel").contramap[Int](_.toString) + List(101, 102, 103).traverse_(ch.notify) + } // Now rejoin the fiber out <- fi.join - js <- out.embedNever + js <- out.embedNever } yield js diff --git a/modules/skunk/shared/src/main/scala/SkunkMapping.scala b/modules/skunk/shared/src/main/scala/SkunkMapping.scala index 62862707..7463f8fc 100644 --- a/modules/skunk/shared/src/main/scala/SkunkMapping.scala +++ b/modules/skunk/shared/src/main/scala/SkunkMapping.scala @@ -13,58 +13,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package skunk +package grackle.skunk import scala.util.control.NonFatal import cats.{Foldable, Reducible} -import cats.effect.{ Resource, Sync } +import cats.effect.{Resource, Sync} import cats.implicits._ -import _root_.skunk.{ AppliedFragment, Decoder, Session, Fragment => SFragment } -import _root_.skunk.data.Arr -import _root_.skunk.codec.all.{ bool, varchar, int4, float8 } -import _root_.skunk.implicits._ import org.tpolecat.sourcepos.SourcePos import org.tpolecat.typename.TypeName +import skunk.{AppliedFragment, Decoder, Fragment => SFragment, Session} +import skunk.codec.all.{bool, float8, int4, varchar} +import skunk.data.Arr +import skunk.implicits._ +import grackle.Mapping import grackle.sql._ import grackle.sqlpg._ abstract class SkunkMapping[F[_]]( - val pool: Resource[F, Session[F]], - val monitor: SkunkMonitor[F] + val pool: Resource[F, Session[F]], + val monitor: SkunkMonitor[F] )( - implicit val M: Sync[F] -) extends Mapping[F] with SkunkMappingLike[F] + implicit val M: Sync[F] +) extends Mapping[F] + with SkunkMappingLike[F] trait SkunkMappingLike[F[_]] extends Mapping[F] with SqlPgMappingLike[F] { outer => implicit val M: Sync[F] - val pool: Resource[F, Session[F]] + val pool: Resource[F, Session[F]] val monitor: SkunkMonitor[F] // Grackle needs to know about codecs, encoders, and fragments. - type Codec = (_root_.skunk.Codec[_], Boolean) - type Encoder = _root_.skunk.Encoder[_] - type Fragment = _root_.skunk.AppliedFragment + type Codec = (skunk.Codec[_], Boolean) + type Encoder = skunk.Encoder[_] + type Fragment = skunk.AppliedFragment def toEncoder(c: Codec): Encoder = c._1 def isNullable(c: Codec): Boolean = c._2 // Also we need to know how to encode the basic GraphQL types. def booleanEncoder = bool - def stringEncoder = varchar - def doubleEncoder = float8 - def intEncoder = int4 + def stringEncoder = varchar + def doubleEncoder = float8 + def intEncoder = int4 - def intCodec = (int4, false) + def intCodec = (int4, false) sealed trait IsNullable[T] { def isNullable: Boolean } object IsNullable extends IsNullable0 { - implicit def nullable[T]: IsNullable[Option[T]] = new IsNullable[Option[T]] { def isNullable = true } + implicit def nullable[T]: IsNullable[Option[T]] = new IsNullable[Option[T]] { + def isNullable = true + } } trait IsNullable0 { implicit def notNullable[T]: IsNullable[T] = new IsNullable[T] { def isNullable = false } @@ -74,13 +77,19 @@ trait SkunkMappingLike[F[_]] extends Mapping[F] with SqlPgMappingLike[F] { outer def value: String } object NullableTypeName extends NullableTypeName0 { - implicit def nullable[T](implicit typeName: TypeName[T]): NullableTypeName[Option[T]] = new NullableTypeName[Option[T]] { def value = typeName.value } + implicit def nullable[T](implicit typeName: TypeName[T]): NullableTypeName[Option[T]] = + new NullableTypeName[Option[T]] { def value = typeName.value } } trait NullableTypeName0 { - implicit def notNullable[T](implicit typeName: TypeName[T]): NullableTypeName[T] = new NullableTypeName[T] { def value = typeName.value } + implicit def notNullable[T](implicit typeName: TypeName[T]): NullableTypeName[T] = + new NullableTypeName[T] { def value = typeName.value } } - def col[T](colName: String, codec: _root_.skunk.Codec[T])(implicit tableName: TableName, typeName: NullableTypeName[T], isNullable: IsNullable[T], pos: SourcePos): ColumnRef = + def col[T](colName: String, codec: skunk.Codec[T])( + implicit tableName: TableName, + typeName: NullableTypeName[T], + isNullable: IsNullable[T], + pos: SourcePos): ColumnRef = ColumnRef(tableName.name, colName, (codec, isNullable.isNullable), typeName.value, pos) // We need to demonstrate that our `Fragment` type has certain compositional properties. @@ -91,16 +100,24 @@ trait SkunkMappingLike[F[_]] extends Mapping[F] with SqlPgMappingLike[F] { outer // N.B. the casts here are due to a bug in the Scala 3 sql interpreter, caused by something // that may or may not be a bug in dotty. https://github.com/lampepfl/dotty/issues/12343 - def bind[A](encoder: Encoder, value: A) = sql"$encoder".asInstanceOf[SFragment[A]].apply(value) - def in[G[_]: Reducible, A](f: AppliedFragment, fs: G[A], enc: Encoder): AppliedFragment = fs.toList.map(sql"$enc".asInstanceOf[SFragment[A]].apply).foldSmash(f |+| void" IN (", void", ", void")")(self, Foldable[List]) - - def const(s: String) = sql"#$s".apply(_root_.skunk.Void) - def and(fs: AppliedFragment*): AppliedFragment = fs.toList.map(parentheses).intercalate(void" AND ")(self) + def bind[A](encoder: Encoder, value: A) = + sql"$encoder".asInstanceOf[SFragment[A]].apply(value) + def in[G[_]: Reducible, A](f: AppliedFragment, fs: G[A], enc: Encoder): AppliedFragment = + fs.toList + .map(sql"$enc".asInstanceOf[SFragment[A]].apply) + .foldSmash(f |+| void" IN (", void", ", void")")(self, Foldable[List]) + + def const(s: String) = sql"#$s".apply(skunk.Void) + def and(fs: AppliedFragment*): AppliedFragment = + fs.toList.map(parentheses).intercalate(void" AND ")(self) def andOpt(fs: Option[AppliedFragment]*): AppliedFragment = and(fs.toList.unite: _*) - def or(fs: AppliedFragment*): AppliedFragment = fs.toList.map(parentheses).intercalate(void" OR ")(self) + def or(fs: AppliedFragment*): AppliedFragment = + fs.toList.map(parentheses).intercalate(void" OR ")(self) def orOpt(fs: Option[AppliedFragment]*): AppliedFragment = or(fs.toList.unite: _*) - def whereAnd(fs: AppliedFragment*): AppliedFragment = if (fs.isEmpty) AppliedFragment.empty else void"WHERE " |+| and(fs: _*) - def whereAndOpt(fs: Option[AppliedFragment]*): AppliedFragment = whereAnd(fs.toList.unite: _*) + def whereAnd(fs: AppliedFragment*): AppliedFragment = + if (fs.isEmpty) AppliedFragment.empty else void"WHERE " |+| and(fs: _*) + def whereAndOpt(fs: Option[AppliedFragment]*): AppliedFragment = whereAnd( + fs.toList.unite: _*) def parentheses(f: AppliedFragment): AppliedFragment = void"(" |+| f |+| void")" def needsCollation(codec: Codec): Boolean = @@ -124,21 +141,21 @@ trait SkunkMappingLike[F[_]] extends Mapping[F] with SqlPgMappingLike[F] { outer val types = codecs.flatMap { case (_, (d, _)) => d.types } def arrToList(arr: Arr[_]): List[Any] = - (arr.foldLeft(List.empty[Any]) { case (acc, elem) => elem :: acc }).reverse + arr.foldLeft(List.empty[Any]) { case (acc, elem) => elem :: acc }.reverse def decode(start: Int, ss: List[Option[String]]): Either[Decoder.Error, Array[Any]] = { - val ncols = ss.length-start + val ncols = ss.length - start val arr = scala.Array.ofDim[Any](ncols) var i = 0 var ss0 = ss.drop(start) var codecs0 = codecs - while(i < ncols) { + while (i < ncols) { val (isJoin, (decoder, isNullable)) = codecs0.head val len = decoder.length val (seg, tl) = ss0.splitAt(len) val elem: Either[Decoder.Error, Any] = - if(isJoin && !isNullable) + if (isJoin && !isNullable) decoder.opt.decode(0, seg).map(_.getOrElse(FailedJoin)) else decoder.decode(0, seg) @@ -162,12 +179,12 @@ trait SkunkMappingLike[F[_]] extends Mapping[F] with SqlPgMappingLike[F] { outer } } - pool.use { s => - Resource.eval(s.prepare(fragment.fragment.query(rowDecoder))).use { ps => - ps.stream(fragment.argument, 1024).compile.toVector + pool + .use { s => + Resource.eval(s.prepare(fragment.fragment.query(rowDecoder))).use { ps => + ps.stream(fragment.argument, 1024).compile.toVector + } } - }.onError { - case NonFatal(e) => Sync[F].delay(e.printStackTrace()) - } + .onError { case NonFatal(e) => Sync[F].delay(e.printStackTrace()) } } } diff --git a/modules/skunk/shared/src/main/scala/SkunkMappingCompanion.scala b/modules/skunk/shared/src/main/scala/SkunkMappingCompanion.scala index f6d093cf..9ec039a8 100644 --- a/modules/skunk/shared/src/main/scala/SkunkMappingCompanion.scala +++ b/modules/skunk/shared/src/main/scala/SkunkMappingCompanion.scala @@ -13,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package skunk +package grackle.skunk -import _root_.skunk.Session -import cats.effect.{ Resource, Sync } +import cats.effect.{Resource, Sync} +import skunk.Session + +import grackle.Mapping trait SkunkMappingCompanion { diff --git a/modules/skunk/shared/src/main/scala/SkunkMonitor.scala b/modules/skunk/shared/src/main/scala/SkunkMonitor.scala index 985321dd..e81a4976 100644 --- a/modules/skunk/shared/src/main/scala/SkunkMonitor.scala +++ b/modules/skunk/shared/src/main/scala/SkunkMonitor.scala @@ -13,31 +13,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package skunk +package grackle.skunk import cats.Applicative +import cats.effect.{Ref, Sync} import cats.implicits._ -import cats.effect.Sync -import cats.effect.Ref -import _root_.skunk.AppliedFragment +import skunk.AppliedFragment -import QueryInterpreter.ProtoJson -import sql._ +import grackle.{Query, Result} +import grackle.QueryInterpreter.ProtoJson +import grackle.sql._ case class SkunkStats( - query: Query, - sql: String, - args: Any, - rows: Int, - cols: Int + query: Query, + sql: String, + args: Any, + rows: Int, + cols: Int ) object SkunkMonitor { def noopMonitor[F[_]: Applicative]: SkunkMonitor[F] = new SkunkMonitor[F] { - def queryMapped(query: Query, fragment: AppliedFragment, rows: Int, cols: Int): F[Unit] = ().pure[F] + def queryMapped(query: Query, fragment: AppliedFragment, rows: Int, cols: Int): F[Unit] = + ().pure[F] def resultComputed(result: Result[ProtoJson]): F[Unit] = ().pure[F] } diff --git a/modules/skunk/shared/src/main/scala/package.scala b/modules/skunk/shared/src/main/scala/package.scala index 514a7d25..534fe0b5 100644 --- a/modules/skunk/shared/src/main/scala/package.scala +++ b/modules/skunk/shared/src/main/scala/package.scala @@ -15,9 +15,10 @@ package grackle -import grackle.sql.SqlMonitor import _root_.skunk.AppliedFragment +import grackle.sql.SqlMonitor + package object skunk { type SkunkMonitor[F[_]] = SqlMonitor[F, AppliedFragment] } diff --git a/modules/sql-core/src/main/scala-2/Like.scala b/modules/sql-core/src/main/scala-2/Like.scala index 401931f5..3c924b87 100644 --- a/modules/sql-core/src/main/scala-2/Like.scala +++ b/modules/sql-core/src/main/scala-2/Like.scala @@ -13,21 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql import scala.util.matching.Regex -import syntax._ +import grackle._ +import grackle.syntax._ -case class Like private[sql] (x: Term[_], pattern: String, caseInsensitive: Boolean) extends Predicate { +case class Like private[sql] (x: Term[_], pattern: String, caseInsensitive: Boolean) + extends Predicate { lazy val r = Like.likeToRegex(pattern, caseInsensitive) def apply(c: Cursor): Result[Boolean] = x(c).flatMap(_ match { case s: String => r.matches(s).success case Some(s: String) => r.matches(s).success case None => false.success - case other => Result.internalError(s"Expected value of type String or Option[String], found $other") + case other => + Result.internalError(s"Expected value of type String or Option[String], found $other") }) def children = List(x) } @@ -37,7 +39,7 @@ object Like extends Like0 { new Like(x, pattern, caseInsensitive) private def likeToRegex(pattern: String, caseInsensitive: Boolean): Regex = { - val csr = ("^"+pattern.replace("%", ".*").replace("_", ".")+"$") + val csr = "^" + pattern.replace("%", ".*").replace("_", ".") + "$" (if (caseInsensitive) s"(?i:$csr)" else csr).r } } @@ -48,9 +50,11 @@ trait Like0 { implicit val sInst: PossiblyOptionString[String] = new PossiblyOptionString[String] {} } trait PossiblyOptionString0 { - implicit val osInst: PossiblyOptionString[Option[String]] = new PossiblyOptionString[Option[String]] {} + implicit val osInst: PossiblyOptionString[Option[String]] = + new PossiblyOptionString[Option[String]] {} } - def apply[T](x: Term[T], pattern: String, caseInsensitive: Boolean)(implicit ev: PossiblyOptionString[T]): Predicate = + def apply[T](x: Term[T], pattern: String, caseInsensitive: Boolean)( + implicit ev: PossiblyOptionString[T]): Predicate = new Like(x, pattern, caseInsensitive) } diff --git a/modules/sql-core/src/main/scala-3/Like.scala b/modules/sql-core/src/main/scala-3/Like.scala index b1e324c0..e6d57835 100644 --- a/modules/sql-core/src/main/scala-3/Like.scala +++ b/modules/sql-core/src/main/scala-3/Like.scala @@ -13,12 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql import scala.util.matching.Regex -case class Like(x: Term[String]|Term[Option[String]], pattern: String, caseInsensitive: Boolean) extends Predicate { +import grackle._ + +case class Like( + x: Term[String] | Term[Option[String]], + pattern: String, + caseInsensitive: Boolean) + extends Predicate { lazy val r = Like.likeToRegex(pattern, caseInsensitive) def apply(c: Cursor): Result[Boolean] = x(c).map(_ match { @@ -31,7 +36,7 @@ case class Like(x: Term[String]|Term[Option[String]], pattern: String, caseInsen object Like { private def likeToRegex(pattern: String, caseInsensitive: Boolean): Regex = { - val csr = ("^"+pattern.replace("%", ".*").replace("_", ".")+"$") + val csr = "^" + pattern.replace("%", ".*").replace("_", ".") + "$" (if (caseInsensitive) s"(?i:$csr)" else csr).r } } diff --git a/modules/sql-core/src/main/scala/FailedJoin.scala b/modules/sql-core/src/main/scala/FailedJoin.scala index 373d79e2..c74137d0 100644 --- a/modules/sql-core/src/main/scala/FailedJoin.scala +++ b/modules/sql-core/src/main/scala/FailedJoin.scala @@ -15,5 +15,7 @@ package grackle.sql -/** A sentinal value representing the empty column values from a failed join. */ +/** + * A sentinal value representing the empty column values from a failed join. + */ case object FailedJoin diff --git a/modules/sql-core/src/main/scala/SqlMapping.scala b/modules/sql-core/src/main/scala/SqlMapping.scala index 571a1dd1..272866fc 100644 --- a/modules/sql-core/src/main/scala/SqlMapping.scala +++ b/modules/sql-core/src/main/scala/SqlMapping.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql import scala.annotation.tailrec import scala.collection.Factory @@ -27,15 +26,20 @@ import io.circe.Json import org.tpolecat.sourcepos.SourcePos import org.tpolecat.typename.typeName -import circe.CirceMappingLike -import syntax._ -import Predicate._ -import Query._ -import ValidationFailure.Severity +import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.ValidationFailure.Severity +import grackle.circe.CirceMappingLike +import grackle.syntax._ -abstract class SqlMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with SqlMappingLike[F] +abstract class SqlMapping[F[_]](implicit val M: MonadThrow[F]) + extends Mapping[F] + with SqlMappingLike[F] -/** An abstract mapping that is backed by a SQL database. */ +/** + * An abstract mapping that is backed by a SQL database. + */ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self => import SqlQuery.{EmptySqlQuery, SqlJoin, SqlSelect, SqlUnion} import TableExpr.{DerivedTableRef, Laterality, SubqueryRef, TableRef, WithRef} @@ -48,7 +52,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def ascribedNullToFragment(codec: Codec): Fragment def collateSelected: Boolean def distinctOnToFragment(dcols: List[Fragment]): Fragment - def distinctOrderColumn(owner: ColumnOwner, col: SqlColumn, predCols: List[SqlColumn], orders: List[OrderSelection[_]]): SqlColumn + def distinctOrderColumn( + owner: ColumnOwner, + col: SqlColumn, + predCols: List[SqlColumn], + orders: List[OrderSelection[_]]): SqlColumn def encapsulateUnionBranch(s: SqlSelect): SqlSelect def mkLateral(inner: Boolean): Laterality def defaultOffsetForSubquery(subquery: SqlQuery): SqlQuery @@ -70,15 +78,20 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } /** - * Name of a SQL schema column and its associated codec, Scala type an defining - * source position within an `SqlMapping`. + * Name of a SQL schema column and its associated codec, Scala type an defining source + * position within an `SqlMapping`. * * `Column`s are considered equal if their table and column names are equal. * - * Note that `ColumnRef` primarily play a role in mappings. During compilation - * they will be used to construct `SqlColumns`. + * Note that `ColumnRef` primarily play a role in mappings. During compilation they will be + * used to construct `SqlColumns`. */ - case class ColumnRef(table: String, column: String, codec: Codec, scalaTypeName: String, pos: SourcePos) { + case class ColumnRef( + table: String, + column: String, + codec: Codec, + scalaTypeName: String, + pos: SourcePos) { override def equals(other: Any) = other match { case cr: ColumnRef => table == cr.table && column == cr.column @@ -95,30 +108,37 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def liftR[T](rt: Result[T]): Aliased[T] = StateT.liftF(rt) def tableDef(table: TableExpr): Aliased[String] = StateT(_.tableDef(table).success) def tableRef(table: TableExpr): Aliased[String] = StateT(_.tableRef(table).success) - def columnDef(column: SqlColumn): Aliased[(Option[String], String)] = StateT(_.columnDef(column).success) - def columnRef(column: SqlColumn): Aliased[(Option[String], String)] = StateT(_.columnRef(column).success) + def columnDef(column: SqlColumn): Aliased[(Option[String], String)] = StateT( + _.columnDef(column).success) + def columnRef(column: SqlColumn): Aliased[(Option[String], String)] = StateT( + _.columnRef(column).success) def pushOwner(owner: ColumnOwner): Aliased[Unit] = StateT(_.pushOwner(owner).success) def popOwner: Aliased[ColumnOwner] = StateT(_.popOwner.success) - def internalError[T](msg: String): Aliased[T] = StateT(_ => Result.internalError[(AliasState, T)](msg)) - def internalError[T](err: Throwable): Aliased[T] = StateT(_ => Result.internalError[(AliasState, T)](err)) + def internalError[T](msg: String): Aliased[T] = + StateT(_ => Result.internalError[(AliasState, T)](msg)) + def internalError[T](err: Throwable): Aliased[T] = + StateT(_ => Result.internalError[(AliasState, T)](err)) } /** * State required to assign table and column aliases. * - * Used when rendering an `SqlQuery` as a `Fragment`. Table aliases are assigned - * as needed for recursive queries. Column aliases are assigned to disambiguate - * collections of columns generated by subqueries and unions. + * Used when rendering an `SqlQuery` as a `Fragment`. Table aliases are assigned as needed for + * recursive queries. Column aliases are assigned to disambiguate collections of columns + * generated by subqueries and unions. */ case class AliasState( - next: Int, - seenTables: Set[String], - tableAliases: Map[(List[String], String), String], - seenColumns: Set[String], - columnAliases: Map[(List[String], String), String], - ownerChain: List[ColumnOwner] + next: Int, + seenTables: Set[String], + tableAliases: Map[(List[String], String), String], + seenColumns: Set[String], + columnAliases: Map[(List[String], String), String], + ownerChain: List[ColumnOwner] ) { - /** Update state to reflect a defining occurence of a table */ + + /** + * Update state to reflect a defining occurence of a table + */ def tableDef(table: TableExpr): (AliasState, String) = tableAliases.get((table.context.resultPath, table.name)) match { case Some(alias) => (this, alias) @@ -127,7 +147,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val alias = s"${table.name}_alias_$next" val newState = copy( - next = next+1, + next = next + 1, tableAliases = tableAliases + ((table.context.resultPath, table.name) -> alias) ) (newState, alias) @@ -135,63 +155,90 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val newState = copy( seenTables = seenTables + table.name, - tableAliases = tableAliases + ((table.context.resultPath, table.name) -> table.name) + tableAliases = + tableAliases + ((table.context.resultPath, table.name) -> table.name) ) (newState, table.name) } } - /** Yields the possibly aliased name of the supplied table */ + /** + * Yields the possibly aliased name of the supplied table + */ def tableRef(table: TableExpr): (AliasState, String) = tableAliases.get((table.context.resultPath, table.name)) match { case Some(alias) => (this, alias) case None => (this, table.name) } - /** Update state to reflect a defining occurence of a column */ + /** + * Update state to reflect a defining occurence of a column + */ def columnDef(column: SqlColumn): (AliasState, (Option[String], String)) = { - val (newState0, table0) = column.namedOwner.map(named => tableDef(named).fmap(Option(_))).getOrElse((this, None)) - columnAliases.get((column.underlying.owner.context.resultPath, column.underlying.column)) match { + val (newState0, table0) = + column.namedOwner.map(named => tableDef(named).fmap(Option(_))).getOrElse((this, None)) + columnAliases.get( + (column.underlying.owner.context.resultPath, column.underlying.column)) match { case Some(name) => (newState0, (table0, name)) case None => val (next0, seenColumns0, column0) = if (newState0.seenColumns(column.column)) - (newState0.next+1, newState0.seenColumns, s"${column.column}_alias_${newState0.next}") + ( + newState0.next + 1, + newState0.seenColumns, + s"${column.column}_alias_${newState0.next}") else (newState0.next, newState0.seenColumns + column.column, column.column) val columnAliases0 = - newState0.columnAliases + ((column.underlying.owner.context.resultPath, column.underlying.column) -> column0) + newState0.columnAliases + (( + column.underlying.owner.context.resultPath, + column.underlying.column) -> column0) - val newState = newState0.copy(next = next0, seenColumns = seenColumns0, columnAliases = columnAliases0) + val newState = newState0.copy( + next = next0, + seenColumns = seenColumns0, + columnAliases = columnAliases0) (newState, (table0, column0)) } } - /** Yields the possibly aliased name of the supplied column */ + /** + * Yields the possibly aliased name of the supplied column + */ def columnRef(column: SqlColumn): (AliasState, (Option[String], String)) = { if (ownerChain.exists(_.directlyOwns(column))) { - column.namedOwner.map(named => tableRef(named). - fmap(Option(_))).getOrElse((this, None)). - fmap(table0 => (table0, column.column)) + column + .namedOwner + .map(named => tableRef(named).fmap(Option(_))) + .getOrElse((this, None)) + .fmap(table0 => (table0, column.column)) } else { - val name = columnAliases.get((column.underlying.owner.context.resultPath, column.underlying.column)).getOrElse(column.column) - column.namedOwner.map(named => tableRef(named). - fmap(Option(_))).getOrElse((this, None)). - fmap(table0 => (table0, name)) + val name = columnAliases + .get((column.underlying.owner.context.resultPath, column.underlying.column)) + .getOrElse(column.column) + column + .namedOwner + .map(named => tableRef(named).fmap(Option(_))) + .getOrElse((this, None)) + .fmap(table0 => (table0, name)) } } - /** Update state to reflect the current column owner while traversing - * the `SqlQuery` being rendered + /** + * Update state to reflect the current column owner while traversing the `SqlQuery` being + * rendered */ - def pushOwner(owner: ColumnOwner): (AliasState, Unit) = (copy(ownerChain = owner :: ownerChain), ()) + def pushOwner(owner: ColumnOwner): (AliasState, Unit) = + (copy(ownerChain = owner :: ownerChain), ()) - /** Update state to restore the current column owner while traversing - * the `SqlQuery` being rendered + /** + * Update state to restore the current column owner while traversing the `SqlQuery` being + * rendered */ - def popOwner: (AliasState, ColumnOwner) = (copy(ownerChain = ownerChain.tail), ownerChain.head) + def popOwner: (AliasState, ColumnOwner) = + (copy(ownerChain = ownerChain.tail), ownerChain.head) } object AliasState { @@ -206,12 +253,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self ) } - /** Trait representing an owner of an `SqlColumn + /** + * Trait representing an owner of an `SqlColumn * - * ColumnOwners are tables, SQL queries and subqueries, common - * table expressions and the like. Most, but not all have a - * name (SqlSelect, SqlUnion and SqlJoin being unnamed - * examples) + * ColumnOwners are tables, SQL queries and subqueries, common table expressions and the like. + * Most, but not all have a name (SqlSelect, SqlUnion and SqlJoin being unnamed examples) */ sealed trait ColumnOwner extends Product with Serializable { def context: Context @@ -220,7 +266,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def directlyOwns(col: SqlColumn): Boolean def findNamedOwner(col: SqlColumn): Option[TableExpr] - /** The name, if any, of this `ColumnOwner` */ + /** + * The name, if any, of this `ColumnOwner` + */ def nameOption: Option[String] = this match { case named: TableExpr => Some(named.name) @@ -242,7 +290,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self }).runA(AliasState.empty).toOption.get.toString } - /** Trait representing an SQL column */ + /** + * Trait representing an SQL column + */ trait SqlColumn { def owner: ColumnOwner def column: String @@ -252,42 +302,52 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def resultPath: List[String] - /** The named owner of this column, if any */ + /** + * The named owner of this column, if any + */ def namedOwner: Option[TableExpr] = owner.findNamedOwner(this) - /** If this column is derived, the column it was derived from, itself otherwise */ + /** + * If this column is derived, the column it was derived from, itself otherwise + */ def underlying: SqlColumn = this - /** Is this column a reference to a column of a table */ + /** + * Is this column a reference to a column of a table + */ def isRef: Boolean = false - /** Yields a copy of this column with all occurences of `from` replaced by `to` */ + /** + * Yields a copy of this column with all occurences of `from` replaced by `to` + */ def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn - /** Yields a copy of this column in `other` + /** + * Yields a copy of this column in `other` * - * Only well defined if the move doesn't lose an owner name + * Only well defined if the move doesn't lose an owner name */ def in(other: ColumnOwner): SqlColumn = { assert(other.nameOption.isDefined || owner.nameOption.isEmpty) subst(owner, other) } - /** Derives a new column with a different owner with this column as underlying. + /** + * Derives a new column with a different owner with this column as underlying. * - * Used to represent columns on the outside of subqueries and common table - * expressions. Note that column aliases are tracked across derivation so - * that derived columns will continue to refer to the same underlying data - * irrespective of renaming. + * Used to represent columns on the outside of subqueries and common table expressions. Note + * that column aliases are tracked across derivation so that derived columns will continue + * to refer to the same underlying data irrespective of renaming. */ def derive(other: ColumnOwner): SqlColumn = - if(other == owner) this + if (other == owner) this else SqlColumn.DerivedColumn(other, this) - /** Equality on `SqlColumns` + /** + * Equality on `SqlColumns` * - * Two `SqlColumns` are equal if their underlyings have the same name and owner. + * Two `SqlColumns` are equal if their underlyings have the same name and owner. */ override def equals(other: Any) = other match { @@ -303,12 +363,19 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self u.owner.context.hashCode() + u.column.hashCode() } - /** This column as a `Term` which can appear in a `Predicate` */ + /** + * This column as a `Term` which can appear in a `Predicate` + */ def toTerm: Term[Option[Unit]] = SqlColumnTerm(this) - /** Render a defining occurence of this `SqlColumn` */ + /** + * Render a defining occurence of this `SqlColumn` + */ def toDefFragment(collated: Boolean): Aliased[Fragment] - /** Render a reference to this `SqlColumn` */ + + /** + * Render a reference to this `SqlColumn` + */ def toRefFragment(collated: Boolean): Aliased[Fragment] override def toString: String = @@ -319,11 +386,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } object SqlColumn { - def mkDefFragment(prefix: Option[String], base: String, collated: Boolean, alias: String): Fragment = { - val prefix0 = prefix.map(_+".").getOrElse("") - val qualified = prefix0+base + def mkDefFragment( + prefix: Option[String], + base: String, + collated: Boolean, + alias: String): Fragment = { + val prefix0 = prefix.map(_ + ".").getOrElse("") + val qualified = prefix0 + base val collated0 = - if (collated) Fragments.const(s"($qualified") |+| collateToFragment |+| Fragments.const(")") + if (collated) + Fragments.const(s"($qualified") |+| collateToFragment |+| Fragments.const(")") else Fragments.const(qualified) if (base == alias) collated0 @@ -338,27 +410,31 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def mkRefFragment(prefix: Option[String], alias: String, collated: Boolean): Fragment = { - val prefix0 = prefix.map(_+".").getOrElse("") - val qualified = prefix0+alias + val prefix0 = prefix.map(_ + ".").getOrElse("") + val qualified = prefix0 + alias val base = Fragments.const(qualified) if (collated) Fragments.parentheses(base |+| collateToFragment) else base } - /** Representation of a column of a table/view */ - case class TableColumn(owner: ColumnOwner, cr: ColumnRef, resultPath: List[String]) extends SqlColumn { + /** + * Representation of a column of a table/view + */ + case class TableColumn(owner: ColumnOwner, cr: ColumnRef, resultPath: List[String]) + extends SqlColumn { def column: String = cr.column def codec: Codec = cr.codec def scalaTypeName: String = cr.scalaTypeName def pos: SourcePos = cr.pos def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - if(!owner.isSameOwner(from)) this - else to match { - case _: DerivedTableRef => derive(to) - case _: SubqueryRef => derive(to) - case _ => copy(owner = to) - } + if (!owner.isSameOwner(from)) this + else + to match { + case _: DerivedTableRef => derive(to) + case _: SubqueryRef => derive(to) + case _ => copy(owner = to) + } override def isRef: Boolean = true @@ -378,9 +454,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self TableColumn(TableRef(context, cr.table), cr, resultPath) } - /** Representation of a synthetic null column + /** + * Representation of a synthetic null column * - * Primarily used to pad the disjuncts of an `SqlUnion`. + * Primarily used to pad the disjuncts of an `SqlUnion`. */ case class NullColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn { def column: String = col.column @@ -393,7 +470,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def underlying: SqlColumn = col.underlying def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) + copy(owner = if (owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) def toDefFragment(collated: Boolean): Aliased[Fragment] = Aliased.columnDef(this).map { @@ -407,7 +484,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of a scalar subquery */ + /** + * Representation of a scalar subquery + */ case class SubqueryColumn(col: SqlColumn, subquery: SqlSelect) extends SqlColumn { def owner: ColumnOwner = col.owner def column: String = col.column @@ -428,8 +507,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toDefFragment(collated: Boolean): Aliased[Fragment] = for { - sub0 <- subquery.toFragment - tc0 <- Aliased.columnDef(this) + sub0 <- subquery.toFragment + tc0 <- Aliased.columnDef(this) } yield mkDefFragment(Fragments.parentheses(sub0), collated, tc0._2) def toRefFragment(collated: Boolean): Aliased[Fragment] = @@ -438,7 +517,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of COUNT aggregation */ + /** + * Representation of COUNT aggregation + */ case class CountColumn(col: SqlColumn, cols: List[SqlColumn]) extends SqlColumn { def owner: ColumnOwner = col.owner def column: String = col.column @@ -454,9 +535,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toDefFragment(collated: Boolean): Aliased[Fragment] = for { cols0 <- cols.traverse(_.toRefFragment(false)) - ct0 <- Aliased.columnDef(this) + ct0 <- Aliased.columnDef(this) } yield { - val count = Fragments.const("COUNT(DISTINCT(") |+| cols0.intercalate(Fragments.const(", ")) |+| Fragments.const(s"))") + val count = Fragments.const("COUNT(DISTINCT(") |+| cols0.intercalate( + Fragments.const(", ")) |+| Fragments.const(s"))") mkDefFragment(count, collated, ct0._2) } @@ -466,8 +548,15 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of a window aggregation */ - case class PartitionColumn(owner: ColumnOwner, column: String, partitionCols: List[SqlColumn], orders: List[OrderSelection[_]]) extends SqlColumn { + /** + * Representation of a window aggregation + */ + case class PartitionColumn( + owner: ColumnOwner, + column: String, + partitionCols: List[SqlColumn], + orders: List[OrderSelection[_]]) + extends SqlColumn { def codec: Codec = intCodec def scalaTypeName: String = "Int" def pos: SourcePos = null @@ -475,7 +564,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def resultPath: List[String] = Nil def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - copy(owner = if(owner.isSameOwner(from)) to else owner, partitionCols = partitionCols.map(_.subst(from, to))) + copy( + owner = if (owner.isSameOwner(from)) to else owner, + partitionCols = partitionCols.map(_.subst(from, to))) def partitionColsToFragment: Aliased[Fragment] = if (partitionCols.isEmpty) Aliased.pure(Fragments.empty) @@ -486,12 +577,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toDefFragment(collated: Boolean): Aliased[Fragment] = for { - cols0 <- partitionColsToFragment - tc0 <- Aliased.columnDef(this) + cols0 <- partitionColsToFragment + tc0 <- Aliased.columnDef(this) orderBy <- SqlQuery.ordersToFragment(orders) } yield { - //val base = Fragments.const("row_number() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) - val base = Fragments.const("dense_rank() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) + // val base = Fragments.const("row_number() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) + val base = + Fragments.const("dense_rank() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) mkDefFragment(base, false, tc0._2) } @@ -501,7 +593,12 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - case class FirstValueColumn(owner: ColumnOwner, col: SqlColumn, partitionCols: List[SqlColumn], orders: List[OrderSelection[_]]) extends SqlColumn { + case class FirstValueColumn( + owner: ColumnOwner, + col: SqlColumn, + partitionCols: List[SqlColumn], + orders: List[OrderSelection[_]]) + extends SqlColumn { def column: String = col.column def codec: Codec = col.codec def scalaTypeName: String = col.scalaTypeName @@ -510,7 +607,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def resultPath: List[String] = col.resultPath def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to), partitionCols = partitionCols.map(_.subst(from, to))) + copy( + owner = if (owner.isSameOwner(from)) to else owner, + col = col.subst(from, to), + partitionCols = partitionCols.map(_.subst(from, to))) def partitionColsToFragment: Aliased[Fragment] = if (partitionCols.isEmpty) Aliased.pure(Fragments.empty) @@ -521,12 +621,14 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toDefFragment(collated: Boolean): Aliased[Fragment] = for { - col0 <- col.toRefFragment(false) - cols0 <- partitionColsToFragment - tc0 <- Aliased.columnDef(this) + col0 <- col.toRefFragment(false) + cols0 <- partitionColsToFragment + tc0 <- Aliased.columnDef(this) orderBy <- SqlQuery.ordersToFragment(orders) } yield { - val base = Fragments.const("first_value") |+| Fragments.parentheses(col0) |+| Fragments.const(" OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) + val base = + Fragments.const("first_value") |+| Fragments.parentheses(col0) |+| Fragments.const( + " OVER ") |+| Fragments.parentheses(cols0 |+| orderBy) mkDefFragment(base, false, tc0._2) } @@ -536,13 +638,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of a column of an embedded subobject + /** + * Representation of a column of an embedded subobject * - * Columns of embedded subobjects have a different context path from columns of - * their enclosing object, however they resolve to columns of the same `SqlSelect`. - * To satisfy the `SqlSelect` invariant that all its columns must share the same - * context path we have to wrap the embedded column so that its context path - * conforms. + * Columns of embedded subobjects have a different context path from columns of their + * enclosing object, however they resolve to columns of the same `SqlSelect`. To satisfy the + * `SqlSelect` invariant that all its columns must share the same context path we have to + * wrap the embedded column so that its context path conforms. */ case class EmbeddedColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn { def column: String = col.column @@ -555,7 +657,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def underlying: SqlColumn = col.underlying def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) + copy(owner = if (owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) override def isRef: Boolean = col.isRef @@ -575,12 +677,12 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of a derived column + /** + * Representation of a derived column * - * Used to represent columns on the outside of subqueries and common table - * expressions. Note that column aliases are tracked across derivation so - * that derived columns will continue to refer to the same underlying data - * irrespective of renaming. + * Used to represent columns on the outside of subqueries and common table expressions. Note + * that column aliases are tracked across derivation so that derived columns will continue + * to refer to the same underlying data irrespective of renaming. */ case class DerivedColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn { def column: String = col.column @@ -593,14 +695,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def underlying: SqlColumn = col.underlying def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = - copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) + copy(owner = if (owner.isSameOwner(from)) to else owner, col = col.subst(from, to)) override def isRef: Boolean = col.isRef def toDefFragment(collated: Boolean): Aliased[Fragment] = { for { - table0 <- namedOwner.map(named => Aliased.tableDef(named).map(Option(_))).getOrElse(Aliased.pure(None)) - tc <- Aliased.columnDef(col) + table0 <- namedOwner + .map(named => Aliased.tableDef(named).map(Option(_))) + .getOrElse(Aliased.pure(None)) + tc <- Aliased.columnDef(col) } yield mkDefFragment(table0, tc._2, collated, tc._2) } @@ -611,13 +715,17 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Wraps an `SqlColumn` as a `Term` which can appear in a `Predicate` */ + /** + * Wraps an `SqlColumn` as a `Term` which can appear in a `Predicate` + */ case class SqlColumnTerm(col: SqlColumn) extends Term[Option[Unit]] { def apply(c: Cursor): Result[Option[Unit]] = Result(Option(())) def children: List[Term[_]] = Nil } - /** A pair of `ColumnRef`s, representing a SQL join. */ + /** + * A pair of `ColumnRef`s, representing a SQL join. + */ case class Join(conditions: List[(ColumnRef, ColumnRef)]) { def parentTable(parentContext: Context): TableRef = TableRef(parentContext, conditions.head._1.table) @@ -631,11 +739,15 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def childCols(childTable0: TableExpr): List[SqlColumn] = toSqlColumns(childTable0, conditions.map(_._2)) - def toSqlColumns(parentTable0: TableExpr, childTable0: TableExpr): List[(SqlColumn, SqlColumn)] = { + def toSqlColumns( + parentTable0: TableExpr, + childTable0: TableExpr): List[(SqlColumn, SqlColumn)] = { parentCols(parentTable0).zip(childCols(childTable0)) } - def toSqlColumns(parentContext: Context, childContext: Context): List[(SqlColumn, SqlColumn)] = + def toSqlColumns( + parentContext: Context, + childContext: Context): List[(SqlColumn, SqlColumn)] = toSqlColumns(parentTable(parentContext), childTable(childContext)) def toSqlJoin(parentTable0: TableExpr, childTable0: TableExpr, inner: Boolean): SqlJoin = { @@ -646,7 +758,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toSqlJoin(parentContext: Context, childContext: Context, inner: Boolean): SqlJoin = toSqlJoin(parentTable(parentContext), childTable(childContext), inner) - def toConstraints(parentContext: Context, childContext: Context): List[(SqlColumn, SqlColumn)] = + def toConstraints( + parentContext: Context, + childContext: Context): List[(SqlColumn, SqlColumn)] = parentCols(parentTable(parentContext)).zip(childCols(childTable(childContext))) def toSqlColumns(table: TableExpr, cols: List[ColumnRef]): List[SqlColumn] = @@ -661,15 +775,17 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self class SqlMappingException(msg: String) extends RuntimeException(msg) /** - * Operators which can be compiled to SQL are eliminated here, partly to avoid duplicating - * work programmatically, but also because the operation isn't necessarily idempotent and - * the result set doesn't necessarily contain the fields required for the filter predicates. - */ + * Operators which can be compiled to SQL are eliminated here, partly to avoid duplicating + * work programmatically, but also because the operation isn't necessarily idempotent and the + * result set doesn't necessarily contain the fields required for the filter predicates. + */ def stripCompiled(query: Query, context: Context): Result[Query] = { def loop(query: Query, context: Context): Query = query match { // Preserved non-Sql filters - case FilterOrderByOffsetLimit(p@Some(pred), oss, off, lim, child) if !isSqlTerm(context, pred).getOrElse(throw new SqlMappingException(s"Unmapped term $pred")) => + case FilterOrderByOffsetLimit(p @ Some(pred), oss, off, lim, child) + if !isSqlTerm(context, pred).getOrElse( + throw new SqlMappingException(s"Unmapped term $pred")) => FilterOrderByOffsetLimit(p, oss, off, lim, loop(child, context)) case Filter(_, child) => loop(child, context) @@ -679,24 +795,32 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self // Preserve OrderBy case o: OrderBy => o.copy(child = loop(o.child, context)) - case s@Select(fieldName, _, Count(_)) => - if(context.tpe.underlying.hasField(fieldName)) s.copy(child = Empty) + case s @ Select(fieldName, _, Count(_)) => + if (context.tpe.underlying.hasField(fieldName)) s.copy(child = Empty) else Empty - case s@Select(fieldName, resultName, _) => - val fieldContext = context.forField(fieldName, resultName).getOrElse(throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}")) + case s @ Select(fieldName, resultName, _) => + val fieldContext = context + .forField(fieldName, resultName) + .getOrElse( + throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}")) s.copy(child = loop(s.child, fieldContext)) - case s@UntypedSelect(fieldName, resultName, _, _, _) => - val fieldContext = context.forField(fieldName, resultName).getOrElse(throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}")) + case s @ UntypedSelect(fieldName, resultName, _, _, _) => + val fieldContext = context + .forField(fieldName, resultName) + .getOrElse( + throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}")) s.copy(child = loop(s.child, fieldContext)) case Group(queries) => Group(queries.map(q => loop(q, context)).filterNot(_ == Empty)) case u: Unique => u.copy(child = loop(u.child, context.asType(context.tpe.list))) case e: Environment => e.copy(child = loop(e.child, context)) case t: TransformCursor => t.copy(child = loop(t.child, context)) - case n@Narrow(subtpe, _) => n.copy(child = loop(n.child, context.asType(subtpe))) - case other@(_: Component[_] | _: Effect[_] | Empty | _: Introspect | _: Select | _: Count | _: UntypedFragmentSpread | _: UntypedInlineFragment) => other + case n @ Narrow(subtpe, _) => n.copy(child = loop(n.child, context.asType(subtpe))) + case other @ (_: Component[_] | _: Effect[_] | Empty | _: Introspect | _: Select | + _: Count | _: UntypedFragmentSpread | _: UntypedInlineFragment) => + other } Result.catchNonFatal { @@ -705,43 +829,52 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def sqlCursor(query: Query, env: Env): F[Result[Cursor]] = - defaultRootCursor(query, schema.queryType, Some(RootCursor(Context(schema.queryType), None, env))).map(_.map(_._2)) + defaultRootCursor( + query, + schema.queryType, + Some(RootCursor(Context(schema.queryType), None, env))).map(_.map(_._2)) // Overrides definition in Mapping - override def defaultRootCursor(query: Query, tpe: Type, parentCursor: Option[Cursor]): F[Result[(Query, Cursor)]] = { + override def defaultRootCursor( + query: Query, + tpe: Type, + parentCursor: Option[Cursor]): F[Result[(Query, Cursor)]] = { val context = Context(tpe) val rootQueries = ungroup(query) def mkGroup(queries: List[Query]): Query = - if(queries.isEmpty) Empty - else if(queries.sizeCompare(1) == 0) queries.head + if (queries.isEmpty) Empty + else if (queries.sizeCompare(1) == 0) queries.head else Group(queries) def mkCursor(query: Query): F[Result[(Query, SqlCursor)]] = MappedQuery(query, context).flatTraverse { mapped => (for { - table <- ResultT(mapped.fetch) - frag <- ResultT(mapped.fragment.pure[F]) - _ <- ResultT(monitor.queryMapped(query, frag, table.numRows, table.numCols).map(_.success)) + table <- ResultT(mapped.fetch) + frag <- ResultT(mapped.fragment.pure[F]) + _ <- ResultT( + monitor.queryMapped(query, frag, table.numRows, table.numCols).map(_.success)) stripped <- ResultT(stripCompiled(query, context).pure[F]) } yield (stripped, SqlCursor(context, table, mapped, parentCursor, Env.empty))).value } val (sqlRoots, otherRoots) = rootQueries.partition(isLocallyMapped(context, _)) - if(sqlRoots.sizeCompare(1) <= 0) + if (sqlRoots.sizeCompare(1) <= 0) mkCursor(mkGroup(sqlRoots)).map(_.map { case (sq, sc) => (mergeQueries(sq :: otherRoots), sc: Cursor) }) else { sqlRoots.traverse(mkCursor).map { qcs => - qcs.sequence.map(_.unzip match { - case (queries, cursors) => - val mergedQuery = mergeQueries(queries ++ otherRoots) - val cursor = MultiRootCursor(cursors) - (mergedQuery, cursor) - }) + qcs + .sequence + .map(_.unzip match { + case (queries, cursors) => + val mergedQuery = mergeQueries(queries ++ otherRoots) + val cursor = MultiRootCursor(cursors) + (mergedQuery, cursor) + }) } } } @@ -769,46 +902,57 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self sealed trait SqlFieldMapping extends FieldMapping case class SqlField( - fieldName: String, - columnRef: ColumnRef, - key: Boolean = false, - discriminator: Boolean = false, - hidden: Boolean = false, - associative: Boolean = false // a key which is also associative might occur multiple times in the table, ie. it is not a DB primary key - )(implicit val pos: SourcePos) extends SqlFieldMapping { + fieldName: String, + columnRef: ColumnRef, + key: Boolean = false, + discriminator: Boolean = false, + hidden: Boolean = false, + associative: Boolean = + false // a key which is also associative might occur multiple times in the table, ie. it is not a DB primary key + )(implicit val pos: SourcePos) + extends SqlFieldMapping { def subtree: Boolean = false } case class SqlObject(fieldName: String, joins: List[Join])( - implicit val pos: SourcePos + implicit val pos: SourcePos ) extends SqlFieldMapping { final def hidden = false final def subtree: Boolean = false } object SqlObject { - def apply(fieldName: String, joins: Join*)(implicit pos: SourcePos): SqlObject = apply(fieldName, joins.toList) + def apply(fieldName: String, joins: Join*)(implicit pos: SourcePos): SqlObject = + apply(fieldName, joins.toList) } case class SqlJson(fieldName: String, columnRef: ColumnRef)( - implicit val pos: SourcePos + implicit val pos: SourcePos ) extends SqlFieldMapping { def hidden: Boolean = false def subtree: Boolean = true } /** - * Common super type for mappings which have a programmatic discriminator, ie. interface and union mappings. + * Common super type for mappings which have a programmatic discriminator, ie. interface and + * union mappings. */ sealed trait SqlDiscriminatedType { def discriminator: SqlDiscriminator } - /** Discriminator for the branches of an interface/union */ + /** + * Discriminator for the branches of an interface/union + */ trait SqlDiscriminator { - /** yield a predicate suitable for filtering row corresponding to the supplied type */ + + /** + * yield a predicate suitable for filtering row corresponding to the supplied type + */ def narrowPredicate(tpe: Type): Result[Predicate] - /** compute the concrete type of the value at the cursor */ + /** + * compute the concrete type of the value at the cursor + */ def discriminate(cursor: Cursor): Result[Type] } @@ -816,48 +960,51 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self object SqlInterfaceMapping { - case class DefaultInterfaceMapping(predicate: MappingPredicate, fieldMappings: Seq[FieldMapping], discriminator: SqlDiscriminator)( - implicit val pos: SourcePos + case class DefaultInterfaceMapping( + predicate: MappingPredicate, + fieldMappings: Seq[FieldMapping], + discriminator: SqlDiscriminator)( + implicit val pos: SourcePos ) extends SqlInterfaceMapping { override def showMappingType: String = "SqlInterfaceMapping" } def apply( - predicate: MappingPredicate, - discriminator: SqlDiscriminator + predicate: MappingPredicate, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultInterfaceMapping(predicate, fieldMappings, discriminator) def apply( - tpe: NamedType, - discriminator: SqlDiscriminator + tpe: NamedType, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultInterfaceMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator) def apply( - path: Path, - discriminator: SqlDiscriminator + path: Path, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultInterfaceMapping(MappingPredicate.PathMatch(path), fieldMappings, discriminator) def apply( - tpe: NamedType, - fieldMappings: List[FieldMapping], - discriminator: SqlDiscriminator + tpe: NamedType, + fieldMappings: List[FieldMapping], + discriminator: SqlDiscriminator )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultInterfaceMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator) } @@ -866,53 +1013,58 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self object SqlUnionMapping { - case class DefaultUnionMapping(predicate: MappingPredicate, fieldMappings: Seq[FieldMapping], discriminator: SqlDiscriminator)( - implicit val pos: SourcePos + case class DefaultUnionMapping( + predicate: MappingPredicate, + fieldMappings: Seq[FieldMapping], + discriminator: SqlDiscriminator)( + implicit val pos: SourcePos ) extends SqlUnionMapping { override def showMappingType: String = "SqlUnionMapping" } def apply( - predicate: MappingPredicate, - discriminator: SqlDiscriminator + predicate: MappingPredicate, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultUnionMapping(predicate, fieldMappings, discriminator) def apply( - tpe: NamedType, - discriminator: SqlDiscriminator + tpe: NamedType, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultUnionMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator) def apply( - path: Path, - discriminator: SqlDiscriminator + path: Path, + discriminator: SqlDiscriminator )( - fieldMappings: FieldMapping* + fieldMappings: FieldMapping* )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultUnionMapping(MappingPredicate.PathMatch(path), fieldMappings, discriminator) def apply( - tpe: NamedType, - fieldMappings: List[FieldMapping], - discriminator: SqlDiscriminator, + tpe: NamedType, + fieldMappings: List[FieldMapping], + discriminator: SqlDiscriminator )( - implicit pos: SourcePos + implicit pos: SourcePos ): ObjectMapping = DefaultUnionMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator) } - override protected def unpackPrefixedMapping(prefix: List[String], om: ObjectMapping): ObjectMapping = + override protected def unpackPrefixedMapping( + prefix: List[String], + om: ObjectMapping): ObjectMapping = om match { case im: SqlInterfaceMapping.DefaultInterfaceMapping => im.copy(predicate = MappingPredicate.PrefixedTypeMatch(prefix, om.predicate.tpe)) @@ -921,38 +1073,61 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _ => super.unpackPrefixedMapping(prefix, om) } - - /** Returns the discriminator columns for the context type */ + /** + * Returns the discriminator columns for the context type + */ def discriminatorColumnsForType(context: Context): List[SqlColumn] = - typeMappings.objectMapping(context).map(_.fieldMappings.iterator.collect { - case cm: SqlField if cm.discriminator => SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath) - }.toList).getOrElse(Nil) + typeMappings + .objectMapping(context) + .map( + _.fieldMappings + .iterator + .collect { + case cm: SqlField if cm.discriminator => + SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath) + } + .toList) + .getOrElse(Nil) - /** Returns the key columns for the context type */ + /** + * Returns the key columns for the context type + */ def keyColumnsForType(context: Context): List[SqlColumn] = { val cols = - typeMappings.objectMapping(context).map { obj => - val objectKeys = obj.fieldMappings.iterator.collect { - case cm: SqlField if cm.key => SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath) - }.toList - - val interfaceKeys = context.tpe.underlyingObject match { - case Some(twf: TypeWithFields) => - twf.interfaces.flatMap(nt => keyColumnsForType(context.asType(nt))) - case _ => Nil - } + typeMappings + .objectMapping(context) + .map { obj => + val objectKeys = obj + .fieldMappings + .iterator + .collect { + case cm: SqlField if cm.key => + SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath) + } + .toList - (objectKeys ++ interfaceKeys).distinct - }.getOrElse(Nil) + val interfaceKeys = context.tpe.underlyingObject match { + case Some(twf: TypeWithFields) => + twf.interfaces.flatMap(nt => keyColumnsForType(context.asType(nt))) + case _ => Nil + } + + (objectKeys ++ interfaceKeys).distinct + } + .getOrElse(Nil) cols } - /** Returns the columns for leaf field `fieldName` in `context` */ + /** + * Returns the columns for leaf field `fieldName` in `context` + */ def columnsForLeaf(context: Context, fieldName: String): Result[List[SqlColumn]] = typeMappings.fieldMapping(context, fieldName) match { - case Some(SqlField(_, cr, _, _, _, _)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success - case Some(SqlJson(_, cr)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success + case Some(SqlField(_, cr, _, _, _, _)) => + List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success + case Some(SqlJson(_, cr)) => + List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success case Some(CursorFieldJson(_, _, required, _)) => required.flatTraverse(r => columnsForLeaf(context, r)) case Some(CursorField(_, _, _, required, _)) => @@ -967,7 +1142,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self Nil.success } - /** Returns the aliased columns corresponding to `term` in `context` */ + /** + * Returns the aliased columns corresponding to `term` in `context` + */ def columnForSqlTerm[T](context: Context, term: Term[T]): Result[SqlColumn] = term match { case termPath: PathTerm => @@ -978,16 +1155,23 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _ => Result.internalError(s"No column for term $term in context $context") } - /** Returns the aliased column corresponding to the atomic field `fieldName` in `context` */ + /** + * Returns the aliased column corresponding to the atomic field `fieldName` in `context` + */ def columnForAtomicField(context: Context, fieldName: String): Result[SqlColumn] = { typeMappings.fieldMapping(context, fieldName) match { - case Some(SqlField(_, cr, _, _, _, _)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success - case Some(SqlJson(_, cr)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success - case _ => Result.internalError(s"No column for atomic field '$fieldName' in context $context") + case Some(SqlField(_, cr, _, _, _, _)) => + SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success + case Some(SqlJson(_, cr)) => + SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success + case _ => + Result.internalError(s"No column for atomic field '$fieldName' in context $context") } } - /** Returns the `Encoder` for the given term in `context` */ + /** + * Returns the `Encoder` for the given term in `context` + */ def encoderForTerm(context: Context, term: Term[_]): Result[Encoder] = term match { case pathTerm: PathTerm => @@ -997,35 +1181,48 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case SqlColumnTerm(col) => toEncoder(col.codec).success - case (_: And)|(_: Or)|(_: Not)|(_: Eql[_])|(_: NEql[_])|(_: Lt[_])|(_: LtEql[_])|(_: Gt[_])|(_: GtEql[_]) => booleanEncoder.success - case (_: AndB)|(_: OrB)|(_: XorB)|(_: NotB) => intEncoder.success - case (_: ToUpperCase)|(_: ToLowerCase) => stringEncoder.success + case _: And | _: Or | _: Not | _: Eql[_] | _: NEql[_] | _: Lt[_] | _: LtEql[_] | + _: Gt[_] | _: GtEql[_] => + booleanEncoder.success + case _: AndB | _: OrB | _: XorB | _: NotB => intEncoder.success + case _: ToUpperCase | _: ToLowerCase => stringEncoder.success case _ => Result.internalError(s"No encoder for term $term in context $context") } - /** Returns the discriminator for the type at `context` */ + /** + * Returns the discriminator for the type at `context` + */ def discriminatorForType(context: Context): Option[SqlDiscriminatedType] = typeMappings.objectMapping(context) collect { - //case d: SqlDiscriminatedType => d // Fails in 2.13.6 due to https://github.com/scala/bug/issues/12398 + // case d: SqlDiscriminatedType => d // Fails in 2.13.6 due to https://github.com/scala/bug/issues/12398 case i: SqlInterfaceMapping => i case u: SqlUnionMapping => u } - /** Returns the table for the type at `context` */ + /** + * Returns the table for the type at `context` + */ def parentTableForType(context: Context): Result[TableRef] = { def noTable = s"No table for type ${context.tpe}" typeMappings.objectMapping(context).toResultOrError(noTable).flatMap { om => - om.fieldMappings.collectFirst { case SqlField(_, cr, _, _, _, _) => TableRef(context, cr.table) }.toResultOrError(noTable).orElse { - context.tpe.underlyingObject match { - case Some(ot: ObjectType) => - ot.interfaces.collectFirstSome(nt => parentTableForType(context.asType(nt)).toOption).toResultOrError(noTable) - case _ => Result.internalError(noTable) + om.fieldMappings + .collectFirst { case SqlField(_, cr, _, _, _, _) => TableRef(context, cr.table) } + .toResultOrError(noTable) + .orElse { + context.tpe.underlyingObject match { + case Some(ot: ObjectType) => + ot.interfaces + .collectFirstSome(nt => parentTableForType(context.asType(nt)).toOption) + .toResultOrError(noTable) + case _ => Result.internalError(noTable) + } } - } } } - /** Is `fieldName` in `context` Jsonb? */ + /** + * Is `fieldName` in `context` Jsonb? + */ def isJsonb(context: Context, fieldName: String): Boolean = typeMappings.fieldMapping(context, fieldName) match { case Some(_: SqlJson) => true @@ -1033,34 +1230,46 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _ => false } - /** Is `fieldName` in `context` computed? */ + /** + * Is `fieldName` in `context` computed? + */ def isComputedField(context: Context, fieldName: String): Boolean = typeMappings.fieldMapping(context, fieldName) match { case Some(_: CursorField[_]) => true case _ => false } - /** Is `term` in `context`expressible in SQL? */ + /** + * Is `term` in `context`expressible in SQL? + */ def isSqlTerm(context: Context, term: Term[_]): Result[Boolean] = term.forallR { case termPath: PathTerm => context.forPath(termPath.path.init).map { parentContext => !isComputedField(parentContext, termPath.path.last) } - case True | False | _: Const[_] | _: And | _: Or | _: Not | _: Eql[_] | _: NEql[_] | _: Contains[_] | _: Lt[_] | _: LtEql[_] | _: Gt[_] | - _: GtEql[_] | _: In[_] | _: AndB | _: OrB | _: XorB | _: NotB | _: Matches | _: StartsWith | _: IsNull[_] | - _: ToUpperCase | _: ToLowerCase | _: Like | _: SqlColumnTerm => true.success + case True | False | _: Const[_] | _: And | _: Or | _: Not | _: Eql[_] | _: NEql[_] | + _: Contains[_] | _: Lt[_] | _: LtEql[_] | _: Gt[_] | _: GtEql[_] | _: In[_] | + _: AndB | _: OrB | _: XorB | _: NotB | _: Matches | _: StartsWith | _: IsNull[_] | + _: ToUpperCase | _: ToLowerCase | _: Like | _: SqlColumnTerm => + true.success case _ => false.success } - /** Is the context type mapped to an associative table? */ + /** + * Is the context type mapped to an associative table? + */ def isAssociative(context: Context): Boolean = - typeMappings.objectMapping(context).exists(_.fieldMappings.exists { - case sf: SqlField => sf.associative - case _ => false - }) + typeMappings + .objectMapping(context) + .exists(_.fieldMappings.exists { + case sf: SqlField => sf.associative + case _ => false + }) - /** Does the type of `fieldName` in `context` represent a list of subobjects? */ + /** + * Does the type of `fieldName` in `context` represent a list of subobjects? + */ def nonLeafList(context: Context, fieldName: String): Boolean = context.tpe.underlyingField(fieldName).exists { fieldTpe => fieldTpe.nonNull.isList && ( @@ -1071,7 +1280,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self ) } - /** Does the supplied field correspond to a single, possibly structured, value? */ + /** + * Does the supplied field correspond to a single, possibly structured, value? + */ def isSingular(context: Context, fieldName: String, query: Query): Boolean = { def loop(query: Query): Boolean = query match { @@ -1087,40 +1298,62 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self !nonLeafList(context, fieldName) || loop(query) } - /** Representation of a table expression */ + /** + * Representation of a table expression + */ sealed trait TableExpr extends ColumnOwner { - /** The name of this `TableExpr` */ + + /** + * The name of this `TableExpr` + */ def name: String - /** Is the supplied column an immediate component of this `TableExpr`? */ + /** + * Is the supplied column an immediate component of this `TableExpr`? + */ def directlyOwns(col: SqlColumn): Boolean = this == col.owner - /** Find the innermost owner of the supplied column within this `TableExpr` */ + /** + * Find the innermost owner of the supplied column within this `TableExpr` + */ def findNamedOwner(col: SqlColumn): Option[TableExpr] - /** Render a defining occurence of this `TableExpr` */ + /** + * Render a defining occurence of this `TableExpr` + */ def toDefFragment: Aliased[Fragment] - /** Render a reference to this `TableExpr` */ + + /** + * Render a reference to this `TableExpr` + */ def toRefFragment: Aliased[Fragment] - /** Is this `TableRef` an anoymous root */ + /** + * Is this `TableRef` an anoymous root + */ def isRoot: Boolean - /** Is this `TableExpr` backed by an SQL union + /** + * Is this `TableExpr` backed by an SQL union * - * This is used to determine whether or not non-nullable columns should be weakened - * to being nullable when fetched + * This is used to determine whether or not non-nullable columns should be weakened to being + * nullable when fetched */ def isUnion: Boolean - /** Yields a copy of this `TableExpr` with all occurences of `from` replaced by `to` */ + /** + * Yields a copy of this `TableExpr` with all occurences of `from` replaced by `to` + */ def subst(from: TableExpr, to: TableExpr): TableExpr def delateral: TableExpr = this } object TableExpr { - /** Table expression corresponding to a possibly aliased table */ + + /** + * Table expression corresponding to a possibly aliased table + */ case class TableRef(context: Context, name: String) extends TableExpr { def owns(col: SqlColumn): Boolean = isSameOwner(col.owner) def contains(other: ColumnOwner): Boolean = isSameOwner(other) @@ -1160,25 +1393,35 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case object NotLateral extends Laterality { def toFragment: Fragment = Fragments.empty - def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = join.toFragmentWithoutLaterality + def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = + join.toFragmentWithoutLaterality def joinPredicates(join: SqlJoin): List[Predicate] = Nil } case object Lateral extends Laterality { def toFragment: Fragment = Fragments.const("LATERAL ") - def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = join.toFragmentWithoutLaterality + def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = + join.toFragmentWithoutLaterality def joinPredicates(join: SqlJoin): List[Predicate] = Nil } case class Apply(inner: Boolean) extends Laterality { - def toFragment: Fragment = Fragments.const(if(inner) "CROSS " else "OUTER ") |+| Fragments.const("APPLY ") - def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = subquery.toDefFragment + def toFragment: Fragment = + Fragments.const(if (inner) "CROSS " else "OUTER ") |+| Fragments.const("APPLY ") + def joinToFragment(join: SqlJoin, subquery: SubqueryRef): Aliased[Fragment] = + subquery.toDefFragment def joinPredicates(join: SqlJoin): List[Predicate] = - if(!join.isPredicate) join.on.map { case (p, c) => Eql(p.toTerm, c.toTerm) } - else Nil + if (!join.isPredicate) join.on.map { case (p, c) => Eql(p.toTerm, c.toTerm) } else Nil } } - /** Table expression corresponding to a subquery */ - case class SubqueryRef(context: Context, name: String, subquery: SqlQuery, laterality: Laterality) extends TableExpr { + /** + * Table expression corresponding to a subquery + */ + case class SubqueryRef( + context: Context, + name: String, + subquery: SqlQuery, + laterality: Laterality) + extends TableExpr { def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || subquery.owns(col) def contains(other: ColumnOwner): Boolean = isSameOwner(other) || subquery.contains(other) @@ -1196,7 +1439,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self for { alias <- Aliased.tableDef(this) sub <- defaultOffsetForSubquery(subquery).toFragment - } yield laterality.toFragment |+| Fragments.parentheses(sub) |+| aliasDefToFragment(alias) + } yield laterality.toFragment |+| Fragments.parentheses(sub) |+| aliasDefToFragment( + alias) def toRefFragment: Aliased[Fragment] = Aliased.tableRef(this).map(Fragments.const) @@ -1207,10 +1451,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def delateral: TableExpr = copy(laterality = Laterality.NotLateral) } - /** Table expression corresponding to a common table expression */ + /** + * Table expression corresponding to a common table expression + */ case class WithRef(context: Context, name: String, withQuery: SqlQuery) extends TableExpr { def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || withQuery.owns(col) - def contains(other: ColumnOwner): Boolean = isSameOwner(other) || withQuery.contains(other) + def contains(other: ColumnOwner): Boolean = + isSameOwner(other) || withQuery.contains(other) def findNamedOwner(col: SqlColumn): Option[TableExpr] = if (this == col.owner) Some(this) else withQuery.findNamedOwner(col) @@ -1231,18 +1478,25 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self Aliased.pure(Fragments.const(s"$name")) } - /** Table expression derived from the given `TableExpr`. + /** + * Table expression derived from the given `TableExpr`. * - * Typically used where we need to refer to a table defined in a subquery or - * common table expression. + * Typically used where we need to refer to a table defined in a subquery or common table + * expression. */ - case class DerivedTableRef(context: Context, alias: Option[String], underlying: TableExpr, noalias: Boolean = false) extends TableExpr { + case class DerivedTableRef( + context: Context, + alias: Option[String], + underlying: TableExpr, + noalias: Boolean = false) + extends TableExpr { assert(!underlying.isInstanceOf[WithRef] || noalias) def name = alias.getOrElse(underlying.name) def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || underlying.owns(col) - def contains(other: ColumnOwner): Boolean = isSameOwner(other) || underlying.contains(other) + def contains(other: ColumnOwner): Boolean = + isSameOwner(other) || underlying.contains(other) def findNamedOwner(col: SqlColumn): Option[TableExpr] = if (this == col.owner) Some(this) else underlying.findNamedOwner(col) @@ -1252,13 +1506,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def isUnion: Boolean = underlying.isUnion def subst(from: TableExpr, to: TableExpr): DerivedTableRef = - if(underlying == from) copy(underlying = to) + if (underlying == from) copy(underlying = to) else copy(underlying = underlying.subst(from, to)) def toDefFragment: Aliased[Fragment] = { for { uname <- if (noalias) Aliased.pure(underlying.name) else Aliased.tableDef(underlying) - name <- Aliased.tableDef(this) + name <- Aliased.tableDef(this) } yield { if (name == uname) Fragments.const(s"$name") @@ -1272,45 +1526,70 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of a SQL query in a context */ + /** + * Representation of a SQL query in a context + */ sealed trait SqlQuery extends ColumnOwner { - /** The context for this query */ + + /** + * The context for this query + */ def context: Context - /** This query in the given context */ - def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlQuery] + /** + * This query in the given context + */ + def withContext( + context: Context, + extraCols: List[SqlColumn], + extraJoins: List[SqlJoin]): Result[SqlQuery] - /** The columns of this query */ + /** + * The columns of this query + */ def cols: List[SqlColumn] - /** The codecs corresponding to the columns of this query */ + /** + * The codecs corresponding to the columns of this query + */ def codecs: List[(Boolean, Codec)] - /** Yields a copy of this query with all occurences of `from` replaced by `to` */ + /** + * Yields a copy of this query with all occurences of `from` replaced by `to` + */ def subst(from: TableExpr, to: TableExpr): SqlQuery - /** Nest this query as a subobject in the enclosing `parentContext` */ + /** + * Nest this query as a subobject in the enclosing `parentContext` + */ def nest( - parentContext: Context, - extraCols: List[SqlColumn], - oneToOne: Boolean, - lateral: Boolean + parentContext: Context, + extraCols: List[SqlColumn], + oneToOne: Boolean, + lateral: Boolean ): Result[SqlQuery] - /** Add WHERE, ORDER BY, OFFSET and LIMIT to this query */ + /** + * Add WHERE, ORDER BY, OFFSET and LIMIT to this query + */ def addFilterOrderByOffsetLimit( - filter: Option[(Predicate, List[SqlJoin])], - orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], - offset: Option[Int], - limit: Option[Int], - predIsOneToOne: Boolean, - parentConstraints: List[List[(SqlColumn, SqlColumn)]] + filter: Option[(Predicate, List[SqlJoin])], + orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], + offset: Option[Int], + limit: Option[Int], + predIsOneToOne: Boolean, + parentConstraints: List[List[(SqlColumn, SqlColumn)]] ): Result[SqlQuery] - /** Yields an equivalent query encapsulating this query as a subquery */ + /** + * Yields an equivalent query encapsulating this query as a subquery + */ def toSubquery(name: String): Result[SqlSelect] - /** Yields a collection of `SqlSelects` which when combined as a union are equivalent to this query */ + /** + * Yields a collection of `SqlSelects` which when combined as a union are equivalent to this + * query + */ def asSelects: List[SqlSelect] = this match { case ss: SqlSelect => ss :: Nil @@ -1318,20 +1597,30 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _: EmptySqlQuery => Nil } - /** Is this query an SQL Union */ + /** + * Is this query an SQL Union + */ def isUnion: Boolean - /** Does one row of this query correspond to exactly one complete GraphQL value */ + /** + * Does one row of this query correspond to exactly one complete GraphQL value + */ def oneToOne: Boolean - /** Render this query as a `Fragment` */ + /** + * Render this query as a `Fragment` + */ def toFragment: Aliased[Fragment] } object SqlQuery { - /** Combine the given queries as a single SQL query */ + + /** + * Combine the given queries as a single SQL query + */ def combineAll(queries: List[SqlQuery]): Result[SqlQuery] = { - if(queries.sizeCompare(1) <= 0) queries.headOption.toResultOrError("Expected at least one query in combineAll") + if (queries.sizeCompare(1) <= 0) + queries.headOption.toResultOrError("Expected at least one query in combineAll") else { val (selects, unions) = queries.partitionMap { @@ -1354,7 +1643,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val ctx = allSelects.head.context - assert(allSelects.forall(sel => sel.context == ctx && sel.limit.isEmpty && sel.offset.isEmpty && sel.orders.isEmpty && !sel.isDistinct)) + assert( + allSelects.forall(sel => + sel.context == ctx && sel.limit.isEmpty && sel.offset.isEmpty && sel + .orders + .isEmpty && !sel.isDistinct)) def combineCompatible(sels: List[SqlSelect]): List[SqlSelect] = { val (oneToOneSelects, multiRowSelects) = sels.partition(_.oneToOne) @@ -1369,7 +1662,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - val combinedSelects = selects.groupBy(sel => sel.table).values.flatMap(combineCompatible).toList + val combinedSelects = + selects.groupBy(sel => sel.table).values.flatMap(combineCompatible).toList (combinedSelects ++ unionSelects) match { case Nil => Result.internalError("Expected at least one select in combineAll") @@ -1398,111 +1692,121 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self }).success } - /** Compute the set of paths traversed by the given prediate */ + /** + * Compute the set of paths traversed by the given prediate + */ def wherePaths(pred: Predicate): List[List[String]] = { def loop(term: Term[_], acc: List[List[String]]): List[List[String]] = { term match { - case Const(_) => acc - case pathTerm: PathTerm => pathTerm.path :: acc - case And(x, y) => loop(y, loop(x, acc)) - case Or(x, y) => loop(y, loop(x, acc)) - case Not(x) => loop(x, acc) - case Eql(x, y) => loop(y, loop(x, acc)) - case NEql(x, y) => loop(y, loop(x, acc)) - case Contains(x, y) => loop(y, loop(x, acc)) - case Lt(x, y) => loop(y, loop(x, acc)) - case LtEql(x, y) => loop(y, loop(x, acc)) - case Gt(x, y) => loop(y, loop(x, acc)) - case GtEql(x, y) => loop(y, loop(x, acc)) - case IsNull(x, _) => loop(x, acc) - case In(x, _) => loop(x, acc) - case AndB(x, y) => loop(y, loop(x, acc)) - case OrB(x, y) => loop(y, loop(x, acc)) - case XorB(x, y) => loop(y, loop(x, acc)) - case NotB(x) => loop(x, acc) - case Matches(x, _) => loop(x, acc) + case Const(_) => acc + case pathTerm: PathTerm => pathTerm.path :: acc + case And(x, y) => loop(y, loop(x, acc)) + case Or(x, y) => loop(y, loop(x, acc)) + case Not(x) => loop(x, acc) + case Eql(x, y) => loop(y, loop(x, acc)) + case NEql(x, y) => loop(y, loop(x, acc)) + case Contains(x, y) => loop(y, loop(x, acc)) + case Lt(x, y) => loop(y, loop(x, acc)) + case LtEql(x, y) => loop(y, loop(x, acc)) + case Gt(x, y) => loop(y, loop(x, acc)) + case GtEql(x, y) => loop(y, loop(x, acc)) + case IsNull(x, _) => loop(x, acc) + case In(x, _) => loop(x, acc) + case AndB(x, y) => loop(y, loop(x, acc)) + case OrB(x, y) => loop(y, loop(x, acc)) + case XorB(x, y) => loop(y, loop(x, acc)) + case NotB(x) => loop(x, acc) + case Matches(x, _) => loop(x, acc) case StartsWith(x, _) => loop(x, acc) - case ToUpperCase(x) => loop(x, acc) - case ToLowerCase(x) => loop(x, acc) - case Like(x, _, _) => loop(x, acc) - case _ => acc + case ToUpperCase(x) => loop(x, acc) + case ToLowerCase(x) => loop(x, acc) + case Like(x, _, _) => loop(x, acc) + case _ => acc } } loop(pred, Nil) } - /** Compute the set of columns referred to by the given prediate */ + /** + * Compute the set of columns referred to by the given prediate + */ def whereCols(f: Term[_] => SqlColumn, pred: Predicate): List[SqlColumn] = { def loop[T](term: T): List[SqlColumn] = term match { - case _: PathTerm => f(term.asInstanceOf[Term[_]]) :: Nil + case _: PathTerm => f(term.asInstanceOf[Term[_]]) :: Nil case _: SqlColumnTerm => f(term.asInstanceOf[Term[_]]) :: Nil - case Const(_) => Nil - case And(x, y) => loop(x) ++ loop(y) - case Or(x, y) => loop(x) ++ loop(y) - case Not(x) => loop(x) - case Eql(x, y) => loop(x) ++ loop(y) - case NEql(x, y) => loop(x) ++ loop(y) - case Contains(x, y) => loop(x) ++ loop(y) - case Lt(x, y) => loop(x) ++ loop(y) - case LtEql(x, y) => loop(x) ++ loop(y) - case Gt(x, y) => loop(x) ++ loop(y) - case GtEql(x, y) => loop(x) ++ loop(y) - case IsNull(x, _) => loop(x) - case In(x, _) => loop(x) - case AndB(x, y) => loop(x) ++ loop(y) - case OrB(x, y) => loop(x) ++ loop(y) - case XorB(x, y) => loop(x) ++ loop(y) - case NotB(x) => loop(x) - case Matches(x, _) => loop(x) + case Const(_) => Nil + case And(x, y) => loop(x) ++ loop(y) + case Or(x, y) => loop(x) ++ loop(y) + case Not(x) => loop(x) + case Eql(x, y) => loop(x) ++ loop(y) + case NEql(x, y) => loop(x) ++ loop(y) + case Contains(x, y) => loop(x) ++ loop(y) + case Lt(x, y) => loop(x) ++ loop(y) + case LtEql(x, y) => loop(x) ++ loop(y) + case Gt(x, y) => loop(x) ++ loop(y) + case GtEql(x, y) => loop(x) ++ loop(y) + case IsNull(x, _) => loop(x) + case In(x, _) => loop(x) + case AndB(x, y) => loop(x) ++ loop(y) + case OrB(x, y) => loop(x) ++ loop(y) + case XorB(x, y) => loop(x) ++ loop(y) + case NotB(x) => loop(x) + case Matches(x, _) => loop(x) case StartsWith(x, _) => loop(x) - case ToUpperCase(x) => loop(x) - case ToLowerCase(x) => loop(x) - case Like(x, _, _) => loop(x) - case _ => Nil + case ToUpperCase(x) => loop(x) + case ToLowerCase(x) => loop(x) + case Like(x, _, _) => loop(x) + case _ => Nil } loop(pred) } - /** Contextualise all terms in the given `Predicate` to the given context and owner */ - def contextualiseWhereTerms(context: Context, owner: ColumnOwner, pred: Predicate): Result[Predicate] = { + /** + * Contextualise all terms in the given `Predicate` to the given context and owner + */ + def contextualiseWhereTerms( + context: Context, + owner: ColumnOwner, + pred: Predicate): Result[Predicate] = { def contextualise(term: Term[_]): SqlColumn = contextualiseTerm(context, owner, term) match { case Result.Success(col) => col case Result.Warning(_, col) => col - case Result.Failure(_) => throw new SqlMappingException(s"Failed to contextualise term $term") + case Result.Failure(_) => + throw new SqlMappingException(s"Failed to contextualise term $term") case Result.InternalError(err) => throw err } def loop[T](term: T): T = (term match { - case _: PathTerm => SqlColumnTerm(contextualise(term.asInstanceOf[Term[_]])) + case _: PathTerm => SqlColumnTerm(contextualise(term.asInstanceOf[Term[_]])) case _: SqlColumnTerm => SqlColumnTerm(contextualise(term.asInstanceOf[Term[_]])) - case Const(_) => term - case And(x, y) => And(loop(x), loop(y)) - case Or(x, y) => Or(loop(x), loop(y)) - case Not(x) => Not(loop(x)) - case e@Eql(x, y) => e.subst(loop(x), loop(y)) - case n@NEql(x, y) => n.subst(loop(x), loop(y)) - case c@Contains(x, y) => c.subst(loop(x), loop(y)) - case l@Lt(x, y) => l.subst(loop(x), loop(y)) - case l@LtEql(x, y) => l.subst(loop(x), loop(y)) - case g@Gt(x, y) => g.subst(loop(x), loop(y)) - case g@GtEql(x, y) => g.subst(loop(x), loop(y)) - case IsNull(x, y) => IsNull(loop(x), y) - case i@In(x, _) => i.subst(loop(x)) - case AndB(x, y) => AndB(loop(x), loop(y)) - case OrB(x, y) => OrB(loop(x), loop(y)) - case XorB(x, y) => XorB(loop(x), loop(y)) - case NotB(x) => NotB(loop(x)) - case Matches(x, y) => Matches(loop(x), y) + case Const(_) => term + case And(x, y) => And(loop(x), loop(y)) + case Or(x, y) => Or(loop(x), loop(y)) + case Not(x) => Not(loop(x)) + case e @ Eql(x, y) => e.subst(loop(x), loop(y)) + case n @ NEql(x, y) => n.subst(loop(x), loop(y)) + case c @ Contains(x, y) => c.subst(loop(x), loop(y)) + case l @ Lt(x, y) => l.subst(loop(x), loop(y)) + case l @ LtEql(x, y) => l.subst(loop(x), loop(y)) + case g @ Gt(x, y) => g.subst(loop(x), loop(y)) + case g @ GtEql(x, y) => g.subst(loop(x), loop(y)) + case IsNull(x, y) => IsNull(loop(x), y) + case i @ In(x, _) => i.subst(loop(x)) + case AndB(x, y) => AndB(loop(x), loop(y)) + case OrB(x, y) => OrB(loop(x), loop(y)) + case XorB(x, y) => XorB(loop(x), loop(y)) + case NotB(x) => NotB(loop(x)) + case Matches(x, y) => Matches(loop(x), y) case StartsWith(x, y) => StartsWith(loop(x), y) - case ToUpperCase(x) => ToUpperCase(loop(x)) - case ToLowerCase(x) => ToLowerCase(loop(x)) - case Like(x, y, z) => Like(loop(x), y, z) - case _ => term + case ToUpperCase(x) => ToUpperCase(loop(x)) + case ToLowerCase(x) => ToLowerCase(loop(x)) + case Like(x, y, z) => Like(loop(x), y, z) + case _ => term }).asInstanceOf[T] Result.catchNonFatal { @@ -1510,8 +1814,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Embed all terms in the given `Predicate` in the given table and parent table */ - def embedWhereTerms(table: TableExpr, parentTable: TableRef, pred: Predicate): Result[Predicate] = { + /** + * Embed all terms in the given `Predicate` in the given table and parent table + */ + def embedWhereTerms( + table: TableExpr, + parentTable: TableRef, + pred: Predicate): Result[Predicate] = { def embed(term: Term[_]): SqlColumn = embedTerm(table, parentTable, term) match { case Result.Success(col) => col @@ -1522,31 +1831,31 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def loop[T](term: T): T = (term match { - case _: PathTerm => SqlColumnTerm(embed(term.asInstanceOf[Term[_]])) + case _: PathTerm => SqlColumnTerm(embed(term.asInstanceOf[Term[_]])) case _: SqlColumnTerm => SqlColumnTerm(embed(term.asInstanceOf[Term[_]])) - case Const(_) => term - case And(x, y) => And(loop(x), loop(y)) - case Or(x, y) => Or(loop(x), loop(y)) - case Not(x) => Not(loop(x)) - case e@Eql(x, y) => e.subst(loop(x), loop(y)) - case n@NEql(x, y) => n.subst(loop(x), loop(y)) - case c@Contains(x, y) => c.subst(loop(x), loop(y)) - case l@Lt(x, y) => l.subst(loop(x), loop(y)) - case l@LtEql(x, y) => l.subst(loop(x), loop(y)) - case g@Gt(x, y) => g.subst(loop(x), loop(y)) - case g@GtEql(x, y) => g.subst(loop(x), loop(y)) - case IsNull(x, y) => IsNull(loop(x), y) - case i@In(x, _) => i.subst(loop(x)) - case AndB(x, y) => AndB(loop(x), loop(y)) - case OrB(x, y) => OrB(loop(x), loop(y)) - case XorB(x, y) => XorB(loop(x), loop(y)) - case NotB(x) => NotB(loop(x)) - case Matches(x, y) => Matches(loop(x), y) + case Const(_) => term + case And(x, y) => And(loop(x), loop(y)) + case Or(x, y) => Or(loop(x), loop(y)) + case Not(x) => Not(loop(x)) + case e @ Eql(x, y) => e.subst(loop(x), loop(y)) + case n @ NEql(x, y) => n.subst(loop(x), loop(y)) + case c @ Contains(x, y) => c.subst(loop(x), loop(y)) + case l @ Lt(x, y) => l.subst(loop(x), loop(y)) + case l @ LtEql(x, y) => l.subst(loop(x), loop(y)) + case g @ Gt(x, y) => g.subst(loop(x), loop(y)) + case g @ GtEql(x, y) => g.subst(loop(x), loop(y)) + case IsNull(x, y) => IsNull(loop(x), y) + case i @ In(x, _) => i.subst(loop(x)) + case AndB(x, y) => AndB(loop(x), loop(y)) + case OrB(x, y) => OrB(loop(x), loop(y)) + case XorB(x, y) => XorB(loop(x), loop(y)) + case NotB(x) => NotB(loop(x)) + case Matches(x, y) => Matches(loop(x), y) case StartsWith(x, y) => StartsWith(loop(x), y) - case ToUpperCase(x) => ToUpperCase(loop(x)) - case ToLowerCase(x) => ToLowerCase(loop(x)) - case Like(x, y, z) => Like(loop(x), y, z) - case _ => term + case ToUpperCase(x) => ToUpperCase(loop(x)) + case ToLowerCase(x) => ToLowerCase(loop(x)) + case Like(x, y, z) => Like(loop(x), y, z) + case _ => term }).asInstanceOf[T] Result.catchNonFatal { @@ -1554,47 +1863,55 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Yields a copy of the given `Predicate` with all occurences of `from` replaced by `to` */ + /** + * Yields a copy of the given `Predicate` with all occurences of `from` replaced by `to` + */ def substWhereTables(from: TableExpr, to: TableExpr, pred: Predicate): Predicate = { def loop[T](term: T): T = (term match { case SqlColumnTerm(col) => SqlColumnTerm(col.subst(from, to)) - case _: PathTerm => term - case Const(_) => term - case And(x, y) => And(loop(x), loop(y)) - case Or(x, y) => Or(loop(x), loop(y)) - case Not(x) => Not(loop(x)) - case e@Eql(x, y) => e.subst(loop(x), loop(y)) - case n@NEql(x, y) => n.subst(loop(x), loop(y)) - case c@Contains(x, y) => c.subst(loop(x), loop(y)) - case l@Lt(x, y) => l.subst(loop(x), loop(y)) - case l@LtEql(x, y) => l.subst(loop(x), loop(y)) - case g@Gt(x, y) => g.subst(loop(x), loop(y)) - case g@GtEql(x, y) => g.subst(loop(x), loop(y)) - case IsNull(x, y) => IsNull(loop(x), y) - case i@In(x, _) => i.subst(loop(x)) - case AndB(x, y) => AndB(loop(x), loop(y)) - case OrB(x, y) => OrB(loop(x), loop(y)) - case XorB(x, y) => XorB(loop(x), loop(y)) - case NotB(x) => NotB(loop(x)) - case Matches(x, y) => Matches(loop(x), y) - case StartsWith(x, y) => StartsWith(loop(x), y) - case ToUpperCase(x) => ToUpperCase(loop(x)) - case ToLowerCase(x) => ToLowerCase(loop(x)) - case Like(x, y, z) => Like(loop(x), y, z) - case _ => term + case _: PathTerm => term + case Const(_) => term + case And(x, y) => And(loop(x), loop(y)) + case Or(x, y) => Or(loop(x), loop(y)) + case Not(x) => Not(loop(x)) + case e @ Eql(x, y) => e.subst(loop(x), loop(y)) + case n @ NEql(x, y) => n.subst(loop(x), loop(y)) + case c @ Contains(x, y) => c.subst(loop(x), loop(y)) + case l @ Lt(x, y) => l.subst(loop(x), loop(y)) + case l @ LtEql(x, y) => l.subst(loop(x), loop(y)) + case g @ Gt(x, y) => g.subst(loop(x), loop(y)) + case g @ GtEql(x, y) => g.subst(loop(x), loop(y)) + case IsNull(x, y) => IsNull(loop(x), y) + case i @ In(x, _) => i.subst(loop(x)) + case AndB(x, y) => AndB(loop(x), loop(y)) + case OrB(x, y) => OrB(loop(x), loop(y)) + case XorB(x, y) => XorB(loop(x), loop(y)) + case NotB(x) => NotB(loop(x)) + case Matches(x, y) => Matches(loop(x), y) + case StartsWith(x, y) => StartsWith(loop(x), y) + case ToUpperCase(x) => ToUpperCase(loop(x)) + case ToLowerCase(x) => ToLowerCase(loop(x)) + case Like(x, y, z) => Like(loop(x), y, z) + case _ => term }).asInstanceOf[T] loop(pred) } - /** Render the given `Predicate` as a `Fragment` representing a where clause conjunct */ + /** + * Render the given `Predicate` as a `Fragment` representing a where clause conjunct + */ def whereToFragment(context: Context, pred: Predicate): Aliased[Fragment] = { def encoder1(enc: Option[Encoder], x: Term[_]): Encoder = - enc.getOrElse(encoderForTerm(context, x).getOrElse(throw new SqlMappingException(s"No encoder for term $x"))) + enc.getOrElse( + encoderForTerm(context, x).getOrElse( + throw new SqlMappingException(s"No encoder for term $x"))) def encoder2(enc: Option[Encoder], x: Term[_], y: Term[_]): Encoder = - enc.getOrElse(encoderForTerm(context, x).getOrElse(encoderForTerm(context, y).getOrElse(throw new SqlMappingException(s"No encoder for terms $x or $y")))) + enc.getOrElse( + encoderForTerm(context, x).getOrElse(encoderForTerm(context, y).getOrElse( + throw new SqlMappingException(s"No encoder for terms $x or $y")))) def loop(term: Term[_], e: Encoder): Aliased[Fragment] = { @@ -1605,7 +1922,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } yield op |+| fx } - def binaryOp(x: Term[_], y: Term[_])(op: Fragment, enc: Option[Encoder] = None): Aliased[Fragment] = { + def binaryOp(x: Term[_], y: Term[_])( + op: Fragment, + enc: Option[Encoder] = None): Aliased[Fragment] = { val e = encoder2(enc, x, y) for { fx <- loop(x, e) @@ -1613,7 +1932,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } yield Fragments.const("(") |+| fx |+| op |+| fy |+| Fragments.const(")") } - def binaryOp2(x: Term[_])(op: Fragment => Fragment, enc: Option[Encoder] = None): Aliased[Fragment] = { + def binaryOp2(x: Term[_])( + op: Fragment => Fragment, + enc: Option[Encoder] = None): Aliased[Fragment] = { val e = encoder1(enc, x) for { fx <- loop(x, e) @@ -1691,43 +2012,39 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self binaryOp2(x)( fx => Fragments.const("regexp_matches(") |+| - fx |+| Fragments.const(s", ") |+| Fragments.bind(stringEncoder, regex.toString) |+| - Fragments.const(s")"), + fx |+| Fragments.const(s", ") |+| Fragments + .bind(stringEncoder, regex.toString) |+| + Fragments.const(s")"), Some(stringEncoder) ) case StartsWith(x, prefix) => binaryOp2(x)( fx => - fx |+| Fragments.const(s" LIKE ") |+| Fragments.bind(stringEncoder, prefix + "%"), + fx |+| Fragments.const(s" LIKE ") |+| Fragments + .bind(stringEncoder, prefix + "%"), Some(stringEncoder) ) case ToUpperCase(x) => binaryOp2(x)( - fx => - Fragments.const("upper(") |+| fx |+| Fragments.const(s")"), + fx => Fragments.const("upper(") |+| fx |+| Fragments.const(s")"), Some(stringEncoder) ) case ToLowerCase(x) => binaryOp2(x)( - fx => - Fragments.const("lower(") |+| fx |+| Fragments.const(s")"), + fx => Fragments.const("lower(") |+| fx |+| Fragments.const(s")"), Some(stringEncoder) ) case IsNull(x, isNull) => val sense = if (isNull) "" else "NOT" - binaryOp2(x)( - fx => - fx |+| Fragments.const(s" IS $sense NULL "), - ) + binaryOp2(x)(fx => fx |+| Fragments.const(s" IS $sense NULL ")) case Like(x, pattern, caseInsensitive) => binaryOp2(x)( - fx => - likeToFragment(fx, pattern, caseInsensitive), + fx => likeToFragment(fx, pattern, caseInsensitive), Some(stringEncoder) ) @@ -1742,40 +2059,68 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Render the given `Predicates` as a where clause `Fragment` */ + /** + * Render the given `Predicates` as a where clause `Fragment` + */ def wheresToFragment(context: Context, wheres: List[Predicate]): Aliased[Fragment] = - wheres.traverse(pred => whereToFragment(context, pred)).map(fwheres => Fragments.const(" ") |+| Fragments.whereAnd(fwheres: _*)) + wheres + .traverse(pred => whereToFragment(context, pred)) + .map(fwheres => Fragments.const(" ") |+| Fragments.whereAnd(fwheres: _*)) - /** Contextualise all terms in the given `OrderSelection` to the given context and owner */ - def contextualiseOrderTerms[T](context: Context, owner: ColumnOwner, os: OrderSelection[T]): Result[OrderSelection[T]] = - contextualiseTerm(context, owner, os.term).map { col => os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) } + /** + * Contextualise all terms in the given `OrderSelection` to the given context and owner + */ + def contextualiseOrderTerms[T]( + context: Context, + owner: ColumnOwner, + os: OrderSelection[T]): Result[OrderSelection[T]] = + contextualiseTerm(context, owner, os.term).map { col => + os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) + } - def embedOrderTerms[T](table: TableExpr, parentTable: TableRef, os: OrderSelection[T]): Result[OrderSelection[T]] = - embedTerm(table, parentTable, os.term).map { col => os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) } + def embedOrderTerms[T]( + table: TableExpr, + parentTable: TableRef, + os: OrderSelection[T]): Result[OrderSelection[T]] = + embedTerm(table, parentTable, os.term).map { col => + os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) + } - /** Yields a copy of the given `OrderSelection` with all occurences of `from` replaced by `to` */ - def substOrderTables[T](from: TableExpr, to: TableExpr, os: OrderSelection[T]): OrderSelection[T] = + /** + * Yields a copy of the given `OrderSelection` with all occurences of `from` replaced by + * `to` + */ + def substOrderTables[T]( + from: TableExpr, + to: TableExpr, + os: OrderSelection[T]): OrderSelection[T] = os.term match { case SqlColumnTerm(col) => os.subst(SqlColumnTerm(col.subst(from, to))) case _ => os } - /** Render the given `OrderSelections` as a `Fragment` */ + /** + * Render the given `OrderSelections` as a `Fragment` + */ def ordersToFragment(orders: List[OrderSelection[_]]): Aliased[Fragment] = if (orders.isEmpty) Aliased.pure(Fragments.empty) else - orders.traverse { - case OrderSelection(term, ascending, nullsLast) => - term match { - case SqlColumnTerm(col) => - for { - fc <- col.toRefFragment(Fragments.needsCollation(col.codec)) - } yield { - orderToFragment(fc, ascending, nullsLast) - } - case other => Aliased.internalError[Fragment](s"Unresolved term $other in ORDER BY") - } - }.map(forders => Fragments.const(" ORDER BY ") |+| forders.intercalate(Fragments.const(","))) + orders + .traverse { + case OrderSelection(term, ascending, nullsLast) => + term match { + case SqlColumnTerm(col) => + for { + fc <- col.toRefFragment(Fragments.needsCollation(col.codec)) + } yield { + orderToFragment(fc, ascending, nullsLast) + } + case other => + Aliased.internalError[Fragment](s"Unresolved term $other in ORDER BY") + } + } + .map(forders => + Fragments.const(" ORDER BY ") |+| forders.intercalate(Fragments.const(","))) def isEmbeddedIn(inner: Context, outer: Context): Boolean = { def directlyEmbedded(child: Context, parent: Context): Boolean = @@ -1786,22 +2131,27 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self @tailrec def loop(inner: Context, outer: Context): Boolean = - if(inner.path.tail == outer.path) directlyEmbedded(inner, outer) + if (inner.path.tail == outer.path) directlyEmbedded(inner, outer) else inner.parent match { case Some(parent) => directlyEmbedded(inner, parent) && loop(parent, outer) case _ => false } - if(!inner.path.endsWith(outer.path) || inner.path.sizeCompare(outer.path) == 0) false + if (!inner.path.endsWith(outer.path) || inner.path.sizeCompare(outer.path) == 0) false else loop(inner, outer) } - /** Yield a copy of the given `Term` with all referenced `SqlColumns` relativised to the given - * context and owned by by the given owner */ - def contextualiseTerm(context: Context, owner: ColumnOwner, term: Term[_]): Result[SqlColumn] = { + /** + * Yield a copy of the given `Term` with all referenced `SqlColumns` relativised to the + * given context and owned by by the given owner + */ + def contextualiseTerm( + context: Context, + owner: ColumnOwner, + term: Term[_]): Result[SqlColumn] = { def subst(col: SqlColumn): SqlColumn = - if(!owner.owns(col)) col + if (!owner.owns(col)) col else col.derive(owner) term match { @@ -1809,7 +2159,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case pathTerm: PathTerm => columnForSqlTerm(context, pathTerm).map { col => val col0 = subst(col) - if(isEmbeddedIn(col0.owner.context, owner.context)) SqlColumn.EmbeddedColumn(owner, col0) + if (isEmbeddedIn(col0.owner.context, owner.context)) + SqlColumn.EmbeddedColumn(owner, col0) else col0 } @@ -1819,7 +2170,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def embedColumn(table: TableExpr, parentTable: TableRef, col: SqlColumn): SqlColumn = - if(table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col) + if (table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col) else col def embedTerm(table: TableExpr, parentTable: TableRef, term: Term[_]): Result[SqlColumn] = { @@ -1834,7 +2185,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } case class EmptySqlQuery(context: Context) extends SqlQuery { - def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlQuery] = + def withContext( + context: Context, + extraCols: List[SqlColumn], + extraJoins: List[SqlJoin]): Result[SqlQuery] = if (extraCols.isEmpty && extraJoins.isEmpty) EmptySqlQuery(context).success else mkSelect.flatMap(_.withContext(context, extraCols, extraJoins)) @@ -1846,47 +2200,73 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def codecs: List[(Boolean, Codec)] = Nil def subst(from: TableExpr, to: TableExpr): SqlQuery = this - def nest(parentContext: Context, extraCols: List[SqlColumn], oneToOne: Boolean, lateral: Boolean): Result[SqlQuery] = - if(extraCols.isEmpty) this.success + def nest( + parentContext: Context, + extraCols: List[SqlColumn], + oneToOne: Boolean, + lateral: Boolean): Result[SqlQuery] = + if (extraCols.isEmpty) this.success else mkSelect.flatMap(_.nest(parentContext, extraCols, oneToOne, lateral)) def addFilterOrderByOffsetLimit( - filter: Option[(Predicate, List[SqlJoin])], - orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], - offset: Option[Int], - limit: Option[Int], - predIsOneToOne: Boolean, - parentConstraints: List[List[(SqlColumn, SqlColumn)]] + filter: Option[(Predicate, List[SqlJoin])], + orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], + offset: Option[Int], + limit: Option[Int], + predIsOneToOne: Boolean, + parentConstraints: List[List[(SqlColumn, SqlColumn)]] ): Result[SqlQuery] = - mkSelect.flatMap(_.addFilterOrderByOffsetLimit(filter, orderBy, offset, limit, predIsOneToOne, parentConstraints)) + mkSelect.flatMap( + _.addFilterOrderByOffsetLimit( + filter, + orderBy, + offset, + limit, + predIsOneToOne, + parentConstraints)) def toSubquery(name: String): Result[SqlSelect] = mkSelect.flatMap(_.toSubquery(name)) def isUnion: Boolean = false def oneToOne: Boolean = false - def toFragment: Aliased[Fragment] = Aliased.internalError("Attempt to render empty query as fragment") + def toFragment: Aliased[Fragment] = + Aliased.internalError("Attempt to render empty query as fragment") def mkSelect: Result[SqlSelect] = parentTableForType(context).map { parentTable => - SqlSelect(context, Nil, parentTable, keyColumnsForType(context), Nil, Nil, Nil, None, None, Nil, false, false) + SqlSelect( + context, + Nil, + parentTable, + keyColumnsForType(context), + Nil, + Nil, + Nil, + None, + None, + Nil, + false, + false) } } - /** Representation of an SQL SELECT */ + /** + * Representation of an SQL SELECT + */ case class SqlSelect( - context: Context, // the GraphQL context of the query - withs: List[WithRef], // the common table expressions - table: TableExpr, // the table/subquery - cols: List[SqlColumn], // the requested columns - joins: List[SqlJoin], // joins for predicates/subobjects - wheres: List[Predicate], - orders: List[OrderSelection[_]], - offset: Option[Int], - limit: Option[Int], - distinct: List[SqlColumn], // columns this query is DISTINCT on - oneToOne: Boolean, // does one row represent exactly one complete GraphQL value - predicate: Boolean // does this SqlSelect represent a predicate + context: Context, // the GraphQL context of the query + withs: List[WithRef], // the common table expressions + table: TableExpr, // the table/subquery + cols: List[SqlColumn], // the requested columns + joins: List[SqlJoin], // joins for predicates/subobjects + wheres: List[Predicate], + orders: List[OrderSelection[_]], + offset: Option[Int], + limit: Option[Int], + distinct: List[SqlColumn], // columns this query is DISTINCT on + oneToOne: Boolean, // does one row represent exactly one complete GraphQL value + predicate: Boolean // does this SqlSelect represent a predicate ) extends SqlQuery { assert(SqlJoin.checkOrdering(table, joins)) assert(cols.forall(owns0)) @@ -1896,20 +2276,33 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self assert(distinct.diff(cols).isEmpty) private def owns0(col: SqlColumn): Boolean = - isSameOwner(col.owner) || table.owns(col) || withs.exists(_.owns(col)) || joins.exists(_.owns(col)) + isSameOwner(col.owner) || table.owns(col) || withs.exists(_.owns(col)) || joins.exists( + _.owns(col)) - /** This query in the given context */ - def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlSelect] = - copy(context = context, cols = (cols ++ extraCols).distinct, joins = extraJoins ++ joins).success + /** + * This query in the given context + */ + def withContext( + context: Context, + extraCols: List[SqlColumn], + extraJoins: List[SqlJoin]): Result[SqlSelect] = + copy( + context = context, + cols = (cols ++ extraCols).distinct, + joins = extraJoins ++ joins).success - def isUnion: Boolean = table.isUnion || context.tpe.underlying.isUnion || context.tpe.underlying.isInterface + def isUnion: Boolean = + table.isUnion || context.tpe.underlying.isUnion || context.tpe.underlying.isInterface def isDistinct: Boolean = distinct.nonEmpty - override def isSameOwner(other: ColumnOwner): Boolean = other.isSameOwner(TableRef(context, table.name)) + override def isSameOwner(other: ColumnOwner): Boolean = + other.isSameOwner(TableRef(context, table.name)) def owns(col: SqlColumn): Boolean = cols.contains(col) || owns0(col) - def contains(other: ColumnOwner): Boolean = isSameOwner(other) || table.contains(other) || joins.exists(_.contains(other)) || withs.exists(_.contains(other)) + def contains(other: ColumnOwner): Boolean = + isSameOwner(other) || table.contains(other) || joins.exists(_.contains(other)) || withs + .exists(_.contains(other)) def directlyOwns(col: SqlColumn): Boolean = (table match { @@ -1918,7 +2311,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self }) || joins.exists(_.directlyOwns(col)) || withs.exists(_.directlyOwns(col)) def findNamedOwner(col: SqlColumn): Option[TableExpr] = - table.findNamedOwner(col).orElse(joins.collectFirstSome(_.findNamedOwner(col))).orElse(withs.collectFirstSome(_.findNamedOwner(col))) + table + .findNamedOwner(col) + .orElse(joins.collectFirstSome(_.findNamedOwner(col))) + .orElse(withs.collectFirstSome(_.findNamedOwner(col))) def codecs: List[(Boolean, Codec)] = if (isUnion) @@ -1930,28 +2326,37 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self cols.map(col => (nullable(col), col.codec)) } - /** The columns, if any, on which this select is ordered */ - lazy val orderCols: Result[List[SqlColumn]] = orders.traverse(os => columnForSqlTerm(context, os.term)).map(_.distinct) + /** + * The columns, if any, on which this select is ordered + */ + lazy val orderCols: Result[List[SqlColumn]] = + orders.traverse(os => columnForSqlTerm(context, os.term)).map(_.distinct) - /** Does the given column need collation? */ + /** + * Does the given column need collation? + */ def selectedNeedsCollation(col: SqlColumn): Result[Boolean] = { - if(collateSelected && Fragments.needsCollation(col.codec)) orderCols.map(_.contains(col)) + if (collateSelected && Fragments.needsCollation(col.codec)) + orderCols.map(_.contains(col)) else false.success } - /** Yield a name for this select derived from any names associated with its - * from clauses or joins + /** + * Yield a name for this select derived from any names associated with its from clauses or + * joins */ def syntheticName(suffix: String): String = { val joinNames = joins.map(_.child.name) - (table.name :: joinNames).mkString("_").take(50-suffix.length)+suffix + (table.name :: joinNames).mkString("_").take(50 - suffix.length) + suffix } - /** Yields a copy of this select with all occurences of `from` replaced by `to` */ + /** + * Yields a copy of this select with all occurences of `from` replaced by `to` + */ def subst(from: TableExpr, to: TableExpr): SqlSelect = { copy( withs = withs.map(_.subst(from, to)), - table = if(table.isSameOwner(from)) to else table.subst(from, to), + table = if (table.isSameOwner(from)) to else table.subst(from, to), cols = cols.map(_.subst(from, to)), joins = joins.map(_.subst(from, to)), wheres = wheres.map(o => substWhereTables(from, to, o)), @@ -1960,35 +2365,53 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self ) } - /** Nest this query as a subobject in the enclosing `parentContext` */ + /** + * Nest this query as a subobject in the enclosing `parentContext` + */ def nest( - parentContext: Context, - extraCols: List[SqlColumn], - oneToOne: Boolean, - lateral: Boolean + parentContext: Context, + extraCols: List[SqlColumn], + oneToOne: Boolean, + lateral: Boolean ): Result[SqlSelect] = { parentTableForType(parentContext).flatMap { parentTable => val inner = !context.tpe.isNullable && !context.tpe.isList def mkJoins(joins: List[Join]): SqlSelect = { - def mkSubquery(multiTable: Boolean, nested: SqlSelect, joinCols: List[SqlColumn], suffix: String): SqlSelect = { + def mkSubquery( + multiTable: Boolean, + nested: SqlSelect, + joinCols: List[SqlColumn], + suffix: String): SqlSelect = { def isMergeable: Boolean = - !multiTable && !nested.joins.exists(_.isPredicate) && nested.wheres.isEmpty && nested.orders.isEmpty && nested.offset.isEmpty && nested.limit.isEmpty && !nested.isDistinct + !multiTable && !nested.joins.exists(_.isPredicate) && nested + .wheres + .isEmpty && nested.orders.isEmpty && nested.offset.isEmpty && nested + .limit + .isEmpty && !nested.isDistinct - if(isMergeable) nested + if (isMergeable) nested else { val exposeCols = nested.table match { case _: TableRef => joinCols case _ => Nil } - val base0 = nested.copy(cols = (exposeCols ++ nested.cols).distinct).toSubquery(syntheticName(suffix), Laterality(lateral, inner)) + val base0 = nested + .copy(cols = (exposeCols ++ nested.cols).distinct) + .toSubquery(syntheticName(suffix), Laterality(lateral, inner)) base0.copy(cols = nested.cols.map(_.derive(base0.table))) } } - def mkNested(outer: Boolean, withs: List[WithRef], table: TableExpr, cols: List[SqlColumn], wheres: List[Predicate], joins: List[SqlJoin]): SqlSelect = { + def mkNested( + outer: Boolean, + withs: List[WithRef], + table: TableExpr, + cols: List[SqlColumn], + wheres: List[Predicate], + joins: List[SqlJoin]): SqlSelect = { val extraWheres = - if(!outer) Nil + if (!outer) Nil else joins.flatMap { join => join.child match { @@ -2016,18 +2439,23 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val initJoins = joins.init val multiJoinHead = initJoins.headOption val multiTable = multiJoinHead.nonEmpty - val multiTableSuffixJoins = initJoins.drop(1).map(_.toSqlJoin(parentContext, parentContext, inner)) + val multiTableSuffixJoins = + initJoins.drop(1).map(_.toSqlJoin(parentContext, parentContext, inner)) val lastJoin = joins.last val lastJoinParentTable = lastJoin.parentTable(parentContext) val base = mkSubquery(multiTable, this, lastJoin.childCols(table), "_nested") val finalJoins = - if(!isAssociative(context)) { + if (!isAssociative(context)) { val finalJoin = lastJoin.toSqlJoin(lastJoinParentTable, base.table, inner) finalJoin :: Nil } else { - val assocTable = TableExpr.DerivedTableRef(context, Some(base.table.name+"_assoc"), base.table, true) + val assocTable = TableExpr.DerivedTableRef( + context, + Some(base.table.name + "_assoc"), + base.table, + true) val assocJoin = lastJoin.toSqlJoin(lastJoinParentTable, assocTable, inner) val finalJoin = @@ -2041,12 +2469,25 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } val nestedJoins = multiTableSuffixJoins ++ finalJoins ++ base.joins - val nested = mkNested(!multiTable, base.withs, nestedJoins.head.parent, base.cols, base.wheres, nestedJoins) + val nested = mkNested( + !multiTable, + base.withs, + nestedJoins.head.parent, + base.cols, + base.wheres, + nestedJoins) multiJoinHead.fold(nested) { firstJoin0 => - val nestedBase = mkSubquery(false, nested, firstJoin0.childCols(nested.table), "_multi") + val nestedBase = + mkSubquery(false, nested, firstJoin0.childCols(nested.table), "_multi") val initialJoin = firstJoin0.toSqlJoin(parentTable, nestedBase.table, inner) - mkNested(true, Nil, parentTable, nestedBase.cols, Nil, initialJoin :: nestedBase.joins) + mkNested( + true, + Nil, + parentTable, + nestedBase.cols, + Nil, + initialJoin :: nestedBase.joins) } } @@ -2055,11 +2496,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self typeMappings.fieldMapping(parentContext, fieldName) match { case Some(_: CursorFieldJson) | Some(SqlObject(_, Nil)) => val embeddedCols = cols.map { col => - if(table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col) + if (table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col) else col } val embeddedJoins = joins.map { join => - if(join.parent.isSameOwner(table)) { + if (join.parent.isSameOwner(table)) { val newOn = join.on match { case (p, c) :: tl => (SqlColumn.EmbeddedColumn(parentTable, p), c) :: tl case _ => join.on @@ -2069,23 +2510,25 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } for { - embeddedWheres <- wheres.traverse(pred => embedWhereTerms(table, parentTable, pred)) - embeddedOrders <- orders.traverse(os => embedOrderTerms(table, parentTable, os)) - } yield - copy( - context = parentContext, - table = parentTable, - cols = embeddedCols, - wheres = embeddedWheres, - orders = embeddedOrders, - joins = embeddedJoins - ) + embeddedWheres <- wheres.traverse(pred => + embedWhereTerms(table, parentTable, pred)) + embeddedOrders <- orders.traverse(os => + embedOrderTerms(table, parentTable, os)) + } yield copy( + context = parentContext, + table = parentTable, + cols = embeddedCols, + wheres = embeddedWheres, + orders = embeddedOrders, + joins = embeddedJoins + ) case Some(SqlObject(_, joins)) => mkJoins(joins).success case _ => - Result.internalError(s"Non-subobject mapping for field '$fieldName' of type ${parentContext.tpe}") + Result.internalError( + s"Non-subobject mapping for field '$fieldName' of type ${parentContext.tpe}") } nested.map { nested0 => @@ -2095,14 +2538,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Add WHERE, ORDER BY and LIMIT to this query */ + /** + * Add WHERE, ORDER BY and LIMIT to this query + */ def addFilterOrderByOffsetLimit( - filter: Option[(Predicate, List[SqlJoin])], - orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], - offset0: Option[Int], - limit0: Option[Int], - predIsOneToOne: Boolean, - parentConstraints: List[List[(SqlColumn, SqlColumn)]] + filter: Option[(Predicate, List[SqlJoin])], + orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], + offset0: Option[Int], + limit0: Option[Int], + predIsOneToOne: Boolean, + parentConstraints: List[List[(SqlColumn, SqlColumn)]] ): Result[SqlSelect] = { assert(orders.isEmpty && offset.isEmpty && limit.isEmpty && !isDistinct) assert(filter.isDefined || orderBy.isDefined || offset0.isDefined || limit0.isDefined) @@ -2132,7 +2577,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def mkWindowPred(partitionTerm: Term[Int]): Predicate = (offset0, limit0) match { case (Some(off), Some(lim)) => - And(GtEql(partitionTerm, Const(off)), LtEql(partitionTerm, Const(off+lim))) + And(GtEql(partitionTerm, Const(off)), LtEql(partitionTerm, Const(off + lim))) case (None, Some(lim)) => LtEql(partitionTerm, Const(lim)) case (Some(off), None) => @@ -2140,16 +2585,21 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case (None, None) => True } - val (pred, filterJoins) = filter.map { case (pred, joins) => (pred :: Nil, joins) }.getOrElse((Nil, Nil)) + val (pred, filterJoins) = + filter.map { case (pred, joins) => (pred :: Nil, joins) }.getOrElse((Nil, Nil)) val pred0 = parentConstraints.flatMap(_.map { case (p, c) => Eql(p.toTerm, c.toTerm) }) ++ pred - val (oss, orderJoins) = orderBy.map { case (oss, joins) => (oss, joins) }.getOrElse((Nil, Nil)) + val (oss, orderJoins) = + orderBy.map { case (oss, joins) => (oss, joins) }.getOrElse((Nil, Nil)) val orderColsR = oss.traverse { os => columnForSqlTerm(context, os.term).map { col => - orderJoins.collectFirstSome(_.findNamedOwner(col)).map(owner => col.in(owner)).getOrElse(col.in(table)) + orderJoins + .collectFirstSome(_.findNamedOwner(col)) + .map(owner => col.in(owner)) + .getOrElse(col.in(table)) } } orderColsR.flatMap { orderCols => @@ -2161,190 +2611,24 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val partitionBy = parentConstraints.head.map(_._2) - if(oneToOne && predIsOneToOne) { + if (oneToOne && predIsOneToOne) { // Case 1) one row is one object in this context pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 => - - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, nullsHigh)) - val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false)) - - // We could use row_number in this case - val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) - val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { - case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table)) - } - - val selWithRowItem = - SqlSelect( - context = context, - withs = withs, - table = table, - cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct, - joins = (filterJoins ++ orderJoins ++ joins).distinct, - wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, - orders = Nil, - offset = None, - limit = None, - distinct = distinct, - oneToOne = true, - predicate = true - ) - - val numberedName = syntheticName("_numbered") - val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - - val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val windowPred = mkWindowPred(partitionTerm) - - val predQuery = - SqlSelect( - context = context, - withs = Nil, - table = subWithRowItem, - cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)), - joins = Nil, - wheres = windowPred :: Nil, - orders = Nil, - offset = None, - limit = None, - distinct = Nil, - oneToOne = true, - predicate = true - ) - - predQuery.success - } - } - } else if ((orderBy.isEmpty || initialKeyOrder) && predIsOneToOne) { - // Case 2a) No order, or key columns at the start of the order; simple predicate means we can elide a subquery - pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 => - - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, nullsHigh)) - val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false)) - - val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) - val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { - case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table)) - } - - val selWithRowItem = - SqlSelect( - context = context, - withs = withs, - table = table, - cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct, - joins = joins, - wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, - orders = Nil, - offset = None, - limit = None, - distinct = distinct, - oneToOne = oneToOne, - predicate = true - ) - - val numberedName = syntheticName("_numbered") - val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - - val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val windowPred = mkWindowPred(partitionTerm) - - val predQuery = - SqlSelect( - context = context, - withs = Nil, - table = subWithRowItem, - cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)), - joins = Nil, - wheres = windowPred :: Nil, - orders = Nil, - offset = None, - limit = None, - distinct = Nil, - oneToOne = oneToOne, - predicate = true - ) - - predQuery.success - } - } - } else if (orderBy.isEmpty || initialKeyOrder) { - // Case 2b) No order, or key columns at the start of the order - val base0 = subqueryToWithQuery - val baseRef = base0.table - - pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 => - - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) - val predCols = keyCols.map(_.derive(baseRef)) - val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false)) - - val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) - val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { - case (_, col) => baseRef.findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(baseRef)) - } - - val selWithRowItem = - SqlSelect( - context = context, - withs = Nil, - table = baseRef, - cols = (partitionCol :: exposeCols ++ predCols).distinct, - joins = (filterJoins ++ orderJoins).distinct, - wheres = (pred1 ++ nonNullKeys).distinct, - orders = Nil, - offset = None, - limit = None, - distinct = Nil, - oneToOne = true, - predicate = true - ) - - val numberedName = syntheticName("_numbered") - val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - - val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val windowPred = mkWindowPred(partitionTerm) - - val numberedPredCols = keyCols.map(_.derive(subWithRowItem)) - - val predQuery = - SqlSelect( - context = context, - withs = Nil, - table = subWithRowItem, - cols = numberedPredCols, - joins = Nil, - wheres = windowPred :: Nil, - orders = Nil, - offset = None, - limit = None, - distinct = Nil, - oneToOne = true, - predicate = true - ) - - mkPredSubquery(base0, predQuery).success - } - } - } else if (predIsOneToOne) { - // Case 3a) There is an order orthogonal to the key; simple predicate means we can elide a subquery - pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 => - oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders => - - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, nullsHigh)) + oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { + oss0 => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => OrderSelection(col.toTerm, true, nullsHigh)) val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false)) - val distOrders = keyCols.map(col => OrderSelection(col.toTerm, true, nullsHigh)) ++ nonKeyOrders - - val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) - val distPartitionCol = SqlColumn.PartitionColumn(table, "row_item_dist", partitionBy ++ keyCols, distOrders) + // We could use row_number in this case + val partitionCol = + SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { - case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table)) + case (_, col) => + findNamedOwner(col) + .map(owner => col.derive(owner)) + .getOrElse(col.derive(table)) } val selWithRowItem = @@ -2352,23 +2636,24 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self context = context, withs = withs, table = table, - cols = (partitionCol :: distPartitionCol :: exposeCols ++ cols ++ orderCols).distinct, - joins = joins, + cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct, + joins = (filterJoins ++ orderJoins ++ joins).distinct, wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, orders = Nil, offset = None, limit = None, distinct = distinct, - oneToOne = oneToOne, + oneToOne = true, predicate = true ) val numberedName = syntheticName("_numbered") - val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) + val subWithRowItem = + SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val distPartitionTerm = distPartitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val windowPred = And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1))) + val partitionTerm = + partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val windowPred = mkWindowPred(partitionTerm) val predQuery = SqlSelect( @@ -2382,116 +2667,366 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self offset = None, limit = None, distinct = Nil, - oneToOne = oneToOne, + oneToOne = true, predicate = true ) predQuery.success - } } } - } else { - // Case 3b) There is an order orthogonal to the key - val base0 = subqueryToWithQuery - val baseRef = base0.table - - pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 => - oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders => - - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) - val predCols = keyCols.map(_.derive(baseRef)) - val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false)) - - val distOrders = keyCols.map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) ++ nonKeyOrders - - val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) - val distPartitionCol = SqlColumn.PartitionColumn(table, "row_item_dist", partitionBy ++ predCols, distOrders) + } else if ((orderBy.isEmpty || initialKeyOrder) && predIsOneToOne) { + // Case 2a) No order, or key columns at the start of the order; simple predicate means we can elide a subquery + pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => + oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { + oss0 => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => OrderSelection(col.toTerm, true, nullsHigh)) + val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false)) + val partitionCol = + SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { - case (_, col) => baseRef.findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(baseRef)) + case (_, col) => + findNamedOwner(col) + .map(owner => col.derive(owner)) + .getOrElse(col.derive(table)) } val selWithRowItem = SqlSelect( context = context, - withs = Nil, - table = baseRef, - cols = (partitionCol :: distPartitionCol :: exposeCols ++ predCols).distinct, - joins = (filterJoins ++ orderJoins).distinct, - wheres = (pred1 ++ nonNullKeys).distinct, + withs = withs, + table = table, + cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct, + joins = joins, + wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, orders = Nil, offset = None, limit = None, - distinct = Nil, - oneToOne = true, + distinct = distinct, + oneToOne = oneToOne, predicate = true ) val numberedName = syntheticName("_numbered") - val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - - val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val distPartitionTerm = distPartitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] - val windowPred = And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1))) + val subWithRowItem = + SubqueryRef(context, numberedName, selWithRowItem, Laterality.NotLateral) - val numberedPredCols = keyCols.map(_.derive(subWithRowItem)) + val partitionTerm = + partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val windowPred = mkWindowPred(partitionTerm) val predQuery = SqlSelect( context = context, withs = Nil, table = subWithRowItem, - cols = numberedPredCols, + cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)), joins = Nil, wheres = windowPred :: Nil, orders = Nil, offset = None, limit = None, distinct = Nil, - oneToOne = true, + oneToOne = oneToOne, predicate = true ) - mkPredSubquery(base0, predQuery).success + predQuery.success + } + } + } else if (orderBy.isEmpty || initialKeyOrder) { + // Case 2b) No order, or key columns at the start of the order + val base0 = subqueryToWithQuery + val baseRef = base0.table + + pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { + pred1 => + oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { + oss0 => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) + val predCols = keyCols.map(_.derive(baseRef)) + val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false)) + + val partitionCol = + SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) + val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { + case (_, col) => + baseRef + .findNamedOwner(col) + .map(owner => col.derive(owner)) + .getOrElse(col.derive(baseRef)) + } + + val selWithRowItem = + SqlSelect( + context = context, + withs = Nil, + table = baseRef, + cols = (partitionCol :: exposeCols ++ predCols).distinct, + joins = (filterJoins ++ orderJoins).distinct, + wheres = (pred1 ++ nonNullKeys).distinct, + orders = Nil, + offset = None, + limit = None, + distinct = Nil, + oneToOne = true, + predicate = true + ) + + val numberedName = syntheticName("_numbered") + val subWithRowItem = SubqueryRef( + context, + numberedName, + selWithRowItem, + Laterality.NotLateral) + + val partitionTerm = + partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val windowPred = mkWindowPred(partitionTerm) + + val numberedPredCols = keyCols.map(_.derive(subWithRowItem)) + + val predQuery = + SqlSelect( + context = context, + withs = Nil, + table = subWithRowItem, + cols = numberedPredCols, + joins = Nil, + wheres = windowPred :: Nil, + orders = Nil, + offset = None, + limit = None, + distinct = Nil, + oneToOne = true, + predicate = true + ) + + mkPredSubquery(base0, predQuery).success } + } + } else if (predIsOneToOne) { + // Case 3a) There is an order orthogonal to the key; simple predicate means we can elide a subquery + pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => + oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { + oss0 => + oss0 + .filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)) + .flatMap { nonKeyOrders => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => OrderSelection(col.toTerm, true, nullsHigh)) + val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false)) + + val distOrders = keyCols.map(col => + OrderSelection(col.toTerm, true, nullsHigh)) ++ nonKeyOrders + + val partitionCol = + SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) + val distPartitionCol = SqlColumn.PartitionColumn( + table, + "row_item_dist", + partitionBy ++ keyCols, + distOrders) + val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { + case (_, col) => + findNamedOwner(col) + .map(owner => col.derive(owner)) + .getOrElse(col.derive(table)) + } + + val selWithRowItem = + SqlSelect( + context = context, + withs = withs, + table = table, + cols = + (partitionCol :: distPartitionCol :: exposeCols ++ cols ++ orderCols).distinct, + joins = joins, + wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, + orders = Nil, + offset = None, + limit = None, + distinct = distinct, + oneToOne = oneToOne, + predicate = true + ) + + val numberedName = syntheticName("_numbered") + val subWithRowItem = SubqueryRef( + context, + numberedName, + selWithRowItem, + Laterality.NotLateral) + + val partitionTerm = + partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val distPartitionTerm = + distPartitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val windowPred = + And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1))) + + val predQuery = + SqlSelect( + context = context, + withs = Nil, + table = subWithRowItem, + cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)), + joins = Nil, + wheres = windowPred :: Nil, + orders = Nil, + offset = None, + limit = None, + distinct = Nil, + oneToOne = oneToOne, + predicate = true + ) + + predQuery.success + } } } + } else { + // Case 3b) There is an order orthogonal to the key + val base0 = subqueryToWithQuery + val baseRef = base0.table + + pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { + pred1 => + oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { + oss0 => + oss0 + .filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)) + .flatMap { nonKeyOrders => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => + OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) + val predCols = keyCols.map(_.derive(baseRef)) + val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false)) + + val distOrders = keyCols.map(col => + OrderSelection( + col.derive(baseRef).toTerm, + true, + nullsHigh)) ++ nonKeyOrders + + val partitionCol = + SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders) + val distPartitionCol = SqlColumn.PartitionColumn( + table, + "row_item_dist", + partitionBy ++ predCols, + distOrders) + + val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map { + case (_, col) => + baseRef + .findNamedOwner(col) + .map(owner => col.derive(owner)) + .getOrElse(col.derive(baseRef)) + } + + val selWithRowItem = + SqlSelect( + context = context, + withs = Nil, + table = baseRef, + cols = + (partitionCol :: distPartitionCol :: exposeCols ++ predCols).distinct, + joins = (filterJoins ++ orderJoins).distinct, + wheres = (pred1 ++ nonNullKeys).distinct, + orders = Nil, + offset = None, + limit = None, + distinct = Nil, + oneToOne = true, + predicate = true + ) + + val numberedName = syntheticName("_numbered") + val subWithRowItem = SubqueryRef( + context, + numberedName, + selWithRowItem, + Laterality.NotLateral) + + val partitionTerm = + partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]] + val distPartitionTerm = distPartitionCol + .derive(subWithRowItem) + .toTerm + .asInstanceOf[Term[Int]] + val windowPred = + And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1))) + + val numberedPredCols = keyCols.map(_.derive(subWithRowItem)) + + val predQuery = + SqlSelect( + context = context, + withs = Nil, + table = subWithRowItem, + cols = numberedPredCols, + joins = Nil, + wheres = windowPred :: Nil, + orders = Nil, + offset = None, + limit = None, + distinct = Nil, + oneToOne = true, + predicate = true + ) + + mkPredSubquery(base0, predQuery).success + } + } + } } } else { // No parent constraint so nothing to be gained from using window functions - if((oneToOne && predIsOneToOne) || (offset0.isEmpty && limit0.isEmpty && filterJoins.isEmpty && orderJoins.isEmpty)) { + if ((oneToOne && predIsOneToOne) || (offset0.isEmpty && limit0.isEmpty && filterJoins.isEmpty && orderJoins.isEmpty)) { // Case 1) one row is one object or query is simple enough to not require subqueries pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 => - val (nonNullKeys, keyOrder) = - offset0.orElse(limit0).map { _ => - val nonNullKeys0 = keyCols.map(col => IsNull(col.toTerm, false)) - val keyOrder0 = keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, nullsHigh)) - (nonNullKeys0, keyOrder0) - }.getOrElse((Nil, Nil)) - - val orders = oss0 ++ keyOrder - - val predQuery = - SqlSelect( - context = context, - withs = withs, - table = table, - cols = cols, - joins = (filterJoins ++ orderJoins ++ joins).distinct, - wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, - orders = orders, - offset = offset0, - limit = limit0, - distinct = distinct, - oneToOne = true, - predicate = true - ) - - predQuery.success + oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { + oss0 => + val (nonNullKeys, keyOrder) = + offset0 + .orElse(limit0) + .map { _ => + val nonNullKeys0 = keyCols.map(col => IsNull(col.toTerm, false)) + val keyOrder0 = keyCols + .diff(orderCols) + .map(col => OrderSelection(col.toTerm, true, nullsHigh)) + (nonNullKeys0, keyOrder0) + } + .getOrElse((Nil, Nil)) + + val orders = oss0 ++ keyOrder + + val predQuery = + SqlSelect( + context = context, + withs = withs, + table = table, + cols = cols, + joins = (filterJoins ++ orderJoins ++ joins).distinct, + wheres = (pred1 ++ nonNullKeys ++ wheres).distinct, + orders = orders, + offset = offset0, + limit = limit0, + distinct = distinct, + oneToOne = true, + predicate = true + ) + + predQuery.success } } } else { @@ -2504,76 +3039,97 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self if (orderBy.isEmpty || initialKeyOrder) { // Case 2) No order, or key columns at the start of the order - pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 => - val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) - - val predQuery = SqlSelect( - context = context, - withs = Nil, - table = baseRef, - cols = predCols, - joins = (filterJoins ++ orderJoins).distinct, - wheres = (pred1 ++ nonNullKeys).distinct, - orders = orders, - offset = offset0, - limit = limit0, - distinct = predCols, - oneToOne = true, - predicate = true - ) - - mkPredSubquery(base0, predQuery).success - } - } - } else { - // Case 3) There is an order orthogonal to the key - pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 => - oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 => - oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders => - val distOrders = keyCols.map(col => OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) ++ nonKeyOrders - val distOrderCols = orderCols.diff(keyCols).map(_.derive(baseRef)) - - val distQuery = SqlSelect( - context = context, - withs = Nil, - table = baseRef, - cols = predCols ++ distOrderCols.map(col => distinctOrderColumn(baseRef, col, predCols, distOrders)), - joins = (filterJoins ++ orderJoins).distinct, - wheres = (pred1 ++ nonNullKeys).distinct, - orders = distOrders, - offset = None, - limit = None, - distinct = predCols, - oneToOne = true, - predicate = true - ) - - val distName = "dist" - val distSub = SubqueryRef(context, distName, distQuery, Laterality.NotLateral) + pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { + pred1 => + oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { + oss0 => + val orders = oss0 ++ keyCols + .diff(orderCols) + .map(col => + OrderSelection(col.derive(baseRef).toTerm, true, nullsHigh)) - val predCols0 = keyCols.map(_.derive(distSub)) - oss.traverse(os => contextualiseOrderTerms(context, distSub, os)).flatMap { outerOss0 => - val orders = outerOss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(distSub).toTerm, true, nullsHigh)) val predQuery = SqlSelect( context = context, withs = Nil, - table = distSub, - cols = predCols0, - joins = Nil, - wheres = Nil, + table = baseRef, + cols = predCols, + joins = (filterJoins ++ orderJoins).distinct, + wheres = (pred1 ++ nonNullKeys).distinct, orders = orders, offset = offset0, limit = limit0, - distinct = Nil, + distinct = predCols, oneToOne = true, predicate = true ) mkPredSubquery(base0, predQuery).success - } } - } + } + } else { + // Case 3) There is an order orthogonal to the key + pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { + pred1 => + oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { + oss0 => + oss0 + .filterA(os => + columnForSqlTerm(context, os.term).map(keyCols.contains)) + .flatMap { nonKeyOrders => + val distOrders = keyCols.map(col => + OrderSelection( + col.derive(baseRef).toTerm, + true, + nullsHigh)) ++ nonKeyOrders + val distOrderCols = orderCols.diff(keyCols).map(_.derive(baseRef)) + + val distQuery = SqlSelect( + context = context, + withs = Nil, + table = baseRef, + cols = predCols ++ distOrderCols.map(col => + distinctOrderColumn(baseRef, col, predCols, distOrders)), + joins = (filterJoins ++ orderJoins).distinct, + wheres = (pred1 ++ nonNullKeys).distinct, + orders = distOrders, + offset = None, + limit = None, + distinct = predCols, + oneToOne = true, + predicate = true + ) + + val distName = "dist" + val distSub = + SubqueryRef(context, distName, distQuery, Laterality.NotLateral) + + val predCols0 = keyCols.map(_.derive(distSub)) + oss + .traverse(os => contextualiseOrderTerms(context, distSub, os)) + .flatMap { outerOss0 => + val orders = outerOss0 ++ keyCols + .diff(orderCols) + .map(col => + OrderSelection(col.derive(distSub).toTerm, true, nullsHigh)) + val predQuery = SqlSelect( + context = context, + withs = Nil, + table = distSub, + cols = predCols0, + joins = Nil, + wheres = Nil, + orders = orders, + offset = offset0, + limit = limit0, + distinct = Nil, + oneToOne = true, + predicate = true + ) + + mkPredSubquery(base0, predQuery).success + } + } + } } } } @@ -2581,21 +3137,36 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Yields an equivalent query encapsulating this query as a subquery */ - def toSubquery(name: String): Result[SqlSelect] = toSubquery(name, Laterality.NotLateral).success + /** + * Yields an equivalent query encapsulating this query as a subquery + */ + def toSubquery(name: String): Result[SqlSelect] = + toSubquery(name, Laterality.NotLateral).success def toSubquery(name: String, lateral: Laterality): SqlSelect = { val ref = SubqueryRef(context, name, this, lateral) - SqlSelect(context, Nil, ref, cols.map(_.derive(ref)), Nil, Nil, Nil, None, None, Nil, oneToOne, predicate) + SqlSelect( + context, + Nil, + ref, + cols.map(_.derive(ref)), + Nil, + Nil, + Nil, + None, + None, + Nil, + oneToOne, + predicate) } - /** If the from clause of this query is a subquery, convert it to a - * common table expression + /** + * If the from clause of this query is a subquery, convert it to a common table expression */ def subqueryToWithQuery: SqlSelect = { table match { case SubqueryRef(_, name, sq, _) => - val with0 = WithRef(context, name+"_base", sq) + val with0 = WithRef(context, name + "_base", sq) val ref = TableExpr.DerivedTableRef(context, Some(name), with0, true) copy(withs = with0 :: withs, table = ref) case _ => @@ -2603,43 +3174,57 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Render this `SqlSelect` as a `Fragment` */ + /** + * Render this `SqlSelect` as a `Fragment` + */ def toFragment: Aliased[Fragment] = { for { - _ <- Aliased.pushOwner(this) - withs0 <- if (withs.isEmpty) Aliased.pure(Fragments.empty) - else withs.traverse(_.toDefFragment).map(fwiths => Fragments.const("WITH ") |+| fwiths.intercalate(Fragments.const(","))) - table0 <- table.delateral.toDefFragment - cols0 <- cols.traverse(col => Aliased.liftR(selectedNeedsCollation(col)).flatMap(col.toDefFragment)) - dcols <- distinct.traverse(col => Aliased.liftR(selectedNeedsCollation(col)).flatMap(col.toRefFragment)) - dist = if (dcols.isEmpty) Fragments.empty else distinctOnToFragment(dcols) - joins0 <- joins.traverse(_.toFragment).map(_.combineAll) - select = Fragments.const(s"SELECT ") |+| dist |+| cols0.intercalate(Fragments.const(", ")) - from = if(table.isRoot) Fragments.empty else Fragments.const(" FROM ") |+| table0 - where <- wheresToFragment(context, wheres) + _ <- Aliased.pushOwner(this) + withs0 <- + if (withs.isEmpty) Aliased.pure(Fragments.empty) + else + withs + .traverse(_.toDefFragment) + .map(fwiths => + Fragments.const("WITH ") |+| fwiths.intercalate(Fragments.const(","))) + table0 <- table.delateral.toDefFragment + cols0 <- cols.traverse(col => + Aliased.liftR(selectedNeedsCollation(col)).flatMap(col.toDefFragment)) + dcols <- distinct.traverse(col => + Aliased.liftR(selectedNeedsCollation(col)).flatMap(col.toRefFragment)) + dist = if (dcols.isEmpty) Fragments.empty else distinctOnToFragment(dcols) + joins0 <- joins.traverse(_.toFragment).map(_.combineAll) + select = Fragments.const(s"SELECT ") |+| dist |+| cols0.intercalate( + Fragments.const(", ")) + from = if (table.isRoot) Fragments.empty else Fragments.const(" FROM ") |+| table0 + where <- wheresToFragment(context, wheres) orderBy <- ordersToFragment(orders) - off = offset.orElse(defaultOffsetForLimit(limit)).map(o => offsetToFragment(Fragments.const(s"$o"))).getOrElse(Fragments.empty) - lim = limit.map(l => limitToFragment(Fragments.const(s"$l"))).getOrElse(Fragments.empty) - _ <- Aliased.popOwner - } yield - withs0 |+| select |+| from |+| joins0 |+| where |+| orderBy |+| off |+| lim + off = offset + .orElse(defaultOffsetForLimit(limit)) + .map(o => offsetToFragment(Fragments.const(s"$o"))) + .getOrElse(Fragments.empty) + lim = limit + .map(l => limitToFragment(Fragments.const(s"$l"))) + .getOrElse(Fragments.empty) + _ <- Aliased.popOwner + } yield withs0 |+| select |+| from |+| joins0 |+| where |+| orderBy |+| off |+| lim } } object SqlSelect { def apply( - context: Context, // the GraphQL context of the query - withs: List[WithRef], // the common table expressions - table: TableExpr, // the table/subquery - cols: List[SqlColumn], // the requested columns - joins: List[SqlJoin], // joins for predicates/subobjects - wheres: List[Predicate], - orders: List[OrderSelection[_]], - offset: Option[Int], - limit: Option[Int], - distinct: List[SqlColumn], // columns this query is DISTINCT on - oneToOne: Boolean, // does one row represent exactly one complete GraphQL value - predicate: Boolean // does this SqlSelect represent a predicate + context: Context, // the GraphQL context of the query + withs: List[WithRef], // the common table expressions + table: TableExpr, // the table/subquery + cols: List[SqlColumn], // the requested columns + joins: List[SqlJoin], // joins for predicates/subobjects + wheres: List[Predicate], + orders: List[OrderSelection[_]], + offset: Option[Int], + limit: Option[Int], + distinct: List[SqlColumn], // columns this query is DISTINCT on + oneToOne: Boolean, // does one row represent exactly one complete GraphQL value + predicate: Boolean // does this SqlSelect represent a predicate ): SqlSelect = new SqlSelect( context, @@ -2657,36 +3242,53 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self ) } - /** Representation of a UNION ALL of SQL SELECTs */ + /** + * Representation of a UNION ALL of SQL SELECTs + */ case class SqlUnion(elems: List[SqlSelect]) extends SqlQuery { assert(elems.sizeCompare(2) >= 0) def isUnion: Boolean = true - /** Does one row of this query correspond to exactly one complete GraphQL value */ + /** + * Does one row of this query correspond to exactly one complete GraphQL value + */ def oneToOne: Boolean = elems.forall(_.oneToOne) - /** The context for this query */ + /** + * The context for this query + */ val context = elems.head.context val topLevel = context.path.sizeCompare(1) == 0 if (topLevel) - assert(elems.tail.forall(elem => elem.context.path.sizeCompare(1) == 0 || schema.isRootType(elem.context.tpe))) + assert( + elems + .tail + .forall(elem => + elem.context.path.sizeCompare(1) == 0 || schema.isRootType(elem.context.tpe))) else assert(elems.tail.forall(elem => elem.context == context)) - /** This query in the given context */ - def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlUnion] = + /** + * This query in the given context + */ + def withContext( + context: Context, + extraCols: List[SqlColumn], + extraJoins: List[SqlJoin]): Result[SqlUnion] = elems.traverse(_.withContext(context, extraCols, extraJoins)).map(SqlUnion(_)) def owns(col: SqlColumn): Boolean = cols.contains(col) || elems.exists(_.owns(col)) - def contains(other: ColumnOwner): Boolean = isSameOwner(other) || elems.exists(_.contains(other)) + def contains(other: ColumnOwner): Boolean = + isSameOwner(other) || elems.exists(_.contains(other)) def directlyOwns(col: SqlColumn): Boolean = owns(col) def findNamedOwner(col: SqlColumn): Option[TableExpr] = None override def isSameOwner(other: ColumnOwner): Boolean = other eq this - /** The union of the columns of the underlying SELECTs in the order they will be - * yielded as the columns of this UNION + /** + * The union of the columns of the underlying SELECTs in the order they will be yielded as + * the columns of this UNION */ lazy val cols: List[SqlColumn] = elems.flatMap(_.cols).distinct @@ -2698,41 +3300,64 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def toSubquery(name: String): Result[SqlSelect] = { val sub = SubqueryRef(context, name, this, Laterality.NotLateral) - SqlSelect(context, Nil, sub, cols.map(_.derive(sub)), Nil, Nil, Nil, None, None, Nil, false, false).success + SqlSelect( + context, + Nil, + sub, + cols.map(_.derive(sub)), + Nil, + Nil, + Nil, + None, + None, + Nil, + false, + false).success } - /** Nest this query as a subobject in the enclosing `parentContext` */ + /** + * Nest this query as a subobject in the enclosing `parentContext` + */ def nest( - parentContext: Context, - extraCols: List[SqlColumn], - oneToOne: Boolean, - lateral: Boolean + parentContext: Context, + extraCols: List[SqlColumn], + oneToOne: Boolean, + lateral: Boolean ): Result[SqlQuery] = elems.traverse(_.nest(parentContext, extraCols, oneToOne, lateral)).map(SqlUnion(_)) - /** Add WHERE, ORDER BY, OFFSET, and LIMIT to this query */ + /** + * Add WHERE, ORDER BY, OFFSET, and LIMIT to this query + */ def addFilterOrderByOffsetLimit( - filter: Option[(Predicate, List[SqlJoin])], - orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], - offset: Option[Int], - limit: Option[Int], - predIsOneToOne: Boolean, - parentConstraints: List[List[(SqlColumn, SqlColumn)]] + filter: Option[(Predicate, List[SqlJoin])], + orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])], + offset: Option[Int], + limit: Option[Int], + predIsOneToOne: Boolean, + parentConstraints: List[List[(SqlColumn, SqlColumn)]] ): Result[SqlQuery] = { val withFilter = (filter, limit) match { case (None, None) => this.success // Push filters, offset and limit through into the branches of a union ... case _ => - val branchLimit = limit.map(_+offset.getOrElse(0)) + val branchLimit = limit.map(_ + offset.getOrElse(0)) val branchOrderBy = limit.flatMap(_ => orderBy) val elems0 = - elems.foldLeft(List.empty[SqlSelect].success) { case (elems0, elem) => - elems0.flatMap { elems0 => - elem.addFilterOrderByOffsetLimit(filter, branchOrderBy, None, branchLimit, predIsOneToOne, parentConstraints).map { - elem0 => elem0 :: elems0 + elems.foldLeft(List.empty[SqlSelect].success) { + case (elems0, elem) => + elems0.flatMap { elems0 => + elem + .addFilterOrderByOffsetLimit( + filter, + branchOrderBy, + None, + branchLimit, + predIsOneToOne, + parentConstraints) + .map { elem0 => elem0 :: elems0 } } - } } elems0.map(SqlUnion(_)) } @@ -2742,14 +3367,22 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _ => for { withFilter0 <- withFilter - table <- parentTableForType(context) - sel <- withFilter0.toSubquery(table.name) - res <- sel.addFilterOrderByOffsetLimit(None, orderBy, offset, limit, predIsOneToOne, parentConstraints) + table <- parentTableForType(context) + sel <- withFilter0.toSubquery(table.name) + res <- sel.addFilterOrderByOffsetLimit( + None, + orderBy, + offset, + limit, + predIsOneToOne, + parentConstraints) } yield res - } - } + } + } - /** Render this `SqlUnion` as a `Fragment` */ + /** + * Render this `SqlUnion` as a `Fragment` + */ def toFragment: Aliased[Fragment] = { val alignedElems = { elems.map { elem => @@ -2763,7 +3396,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self for { frags <- alignedElems.traverse(_.toFragment) } yield { - frags.reduce((x, y) => Fragments.parentheses(x) |+| Fragments.const(" UNION ALL ") |+| Fragments.parentheses(y)) + frags.reduce((x, y) => + Fragments.parentheses(x) |+| Fragments.const(" UNION ALL ") |+| Fragments + .parentheses(y)) } } } @@ -2773,12 +3408,14 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self new SqlUnion(elems.distinct.map(encapsulateUnionBranch)) } - /** Representation of an SQL join */ + /** + * Representation of an SQL join + */ case class SqlJoin( - parent: TableExpr, // name of parent table - child: TableExpr, // child table/subquery - on: List[(SqlColumn, SqlColumn)], // join conditions - inner: Boolean + parent: TableExpr, // name of parent table + child: TableExpr, // child table/subquery + on: List[(SqlColumn, SqlColumn)], // join conditions + inner: Boolean ) extends ColumnOwner { assert(on.nonEmpty) assert(on.forall { case (p, c) => parent.owns(p) && child.owns(c) }) @@ -2797,14 +3434,17 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def isSameOwner(other: ColumnOwner): Boolean = other eq this - /** Replace references to `from` with `to` */ + /** + * Replace references to `from` with `to` + */ def subst(from: TableExpr, to: TableExpr): SqlJoin = { - val newParent = if(parent.isSameOwner(from)) to else parent + val newParent = if (parent.isSameOwner(from)) to else parent val newChild = - if(!child.isSameOwner(from)) child + if (!child.isSameOwner(from)) child else { (child, to) match { - case (sr: SubqueryRef, to: TableRef) => sr.copy(context = to.context, name = to.name) + case (sr: SubqueryRef, to: TableRef) => + sr.copy(context = to.context, name = to.name) case _ => to } } @@ -2812,19 +3452,26 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self copy(parent = newParent, child = newChild, on = newOn) } - /** Return the columns of `table` referred to by the parent side of the conditions of this join */ + /** + * Return the columns of `table` referred to by the parent side of the conditions of this + * join + */ def colsOf(other: ColumnOwner): List[SqlColumn] = if (other.isSameOwner(parent)) on.map(_._1) else Nil - /** Does this `SqlJoin` represent a predicate? */ + /** + * Does this `SqlJoin` represent a predicate? + */ def isPredicate: Boolean = child match { case SubqueryRef(_, _, sq: SqlSelect, _) => sq.predicate case _ => false } - /** Render this `SqlJoin` as a `Fragment` */ + /** + * Render this `SqlJoin` as a `Fragment` + */ def toFragment: Aliased[Fragment] = { child match { case sq: SubqueryRef => @@ -2852,13 +3499,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self for { fchild <- child.toDefFragment - fon <- onFrag + fon <- onFrag } yield Fragments.const(s" $join ") |+| fchild |+| fon } } object SqlJoin { - /** Check that the given joins are correctly ordered relative to the given parent table */ + + /** + * Check that the given joins are correctly ordered relative to the given parent table + */ def checkOrdering(parent: TableExpr, joins: List[SqlJoin]): Boolean = { @tailrec def loop(joins: List[SqlJoin], seen: List[TableExpr]): Boolean = { @@ -2873,39 +3523,64 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Represents the mapping of a GraphQL query to an SQL query */ + /** + * Represents the mapping of a GraphQL query to an SQL query + */ sealed trait MappedQuery { - /** Execute this query in `F` */ + + /** + * Execute this query in `F` + */ def fetch: F[Result[Table]] - /** The query rendered as a `Fragment` with all table and column aliases applied */ + /** + * The query rendered as a `Fragment` with all table and column aliases applied + */ def fragment: Result[Fragment] - /** Return the value of the field `fieldName` in `context` from `table` */ + /** + * Return the value of the field `fieldName` in `context` from `table` + */ def selectAtomicField(context: Context, fieldName: String, table: Table): Result[Any] - /** Does `table` contain subobjects of the type of the `narrowedContext` type */ + /** + * Does `table` contain subobjects of the type of the `narrowedContext` type + */ def narrowsTo(narrowedContext: Context, table: Table): Boolean - /** Yield a `Table` containing only subojects of the `narrowedContext` type */ + /** + * Yield a `Table` containing only subojects of the `narrowedContext` type + */ def narrow(narrowedContext: Context, table: Table): Table - /** Yield a list of `Tables` one for each of the subobjects of the context type - * contained in `table`. + /** + * Yield a list of `Tables` one for each of the subobjects of the context type contained in + * `table`. */ def group(context: Context, table: Table): Iterator[Table] - /** Return the number of subobjects of the context type contained in `table`. */ + /** + * Return the number of subobjects of the context type contained in `table`. + */ def count(context: Context, table: Table): Int - /** Does this query contain a root with the given possibly aliased name */ + /** + * Does this query contain a root with the given possibly aliased name + */ def containsRoot(fieldName: String, resultName: Option[String]): Boolean } object MappedQuery { - /** Compile the given GraphQL query to SQL in the given `Context` */ + + /** + * Compile the given GraphQL query to SQL in the given `Context` + */ def apply(q: Query, context: Context): Result[MappedQuery] = { - def loop(q: Query, context: Context, parentConstraints: List[List[(SqlColumn, SqlColumn)]], exposeJoins: Boolean): Result[SqlQuery] = { + def loop( + q: Query, + context: Context, + parentConstraints: List[List[(SqlColumn, SqlColumn)]], + exposeJoins: Boolean): Result[SqlQuery] = { object TypeCase { def unapply(q: Query): Option[(Query, List[Narrow])] = { @@ -2923,7 +3598,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } val ungrouped = ungroup(q).flatMap { - case sel@Select(fieldName, _, _) if isPolySelect(sel) => + case sel @ Select(fieldName, _, _) if isPolySelect(sel) => typeMappings.rawFieldMapping(context, fieldName) match { case Some(TypeMappings.PolymorphicFieldMapping(cands)) => cands.map { case (pred, _) => Narrow(schema.uncheckedRef(pred.tpe), sel) } @@ -2954,27 +3629,29 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def group(queries: List[Query]): Result[SqlQuery] = { - queries.foldLeft(List.empty[SqlQuery].success) { - case (nodes, q) => - loop(q, context, parentConstraints, exposeJoins).flatMap { - case _: EmptySqlQuery => - nodes - case n => - nodes.map(n :: _) - } - }.flatMap { - case Nil => EmptySqlQuery(context).success - case List(node) => node.success - case nodes => - if(schema.isRootType(context.tpe)) - SqlQuery.combineRootNodes(context, nodes) - else { - parentTableForType(context).flatMap { parentTable => - if(parentTable.isRoot) SqlQuery.combineRootNodes(context, nodes) - else SqlQuery.combineAll(nodes) + queries + .foldLeft(List.empty[SqlQuery].success) { + case (nodes, q) => + loop(q, context, parentConstraints, exposeJoins).flatMap { + case _: EmptySqlQuery => + nodes + case n => + nodes.map(n :: _) } - } - } + } + .flatMap { + case Nil => EmptySqlQuery(context).success + case List(node) => node.success + case nodes => + if (schema.isRootType(context.tpe)) + SqlQuery.combineRootNodes(context, nodes) + else { + parentTableForType(context).flatMap { parentTable => + if (parentTable.isRoot) SqlQuery.combineRootNodes(context, nodes) + else SqlQuery.combineAll(nodes) + } + } + } } def isEmbedded(context: Context, fieldName: String): Boolean = @@ -2984,12 +3661,15 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def unembed(context: Context): Context = { - if(context.path.sizeCompare(1) <= 0) context + if (context.path.sizeCompare(1) <= 0) context else { - val parentContext = context.copy(path = context.path.tail, resultPath = context.resultPath.tail, typePath = context.typePath.tail) + val parentContext = context.copy( + path = context.path.tail, + resultPath = context.resultPath.tail, + typePath = context.typePath.tail) val fieldName = context.path.head - if(isEmbedded(parentContext, fieldName)) + if (isEmbedded(parentContext, fieldName)) unembed(parentContext) else context @@ -2998,7 +3678,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self /* Compute the set of parent constraints to be inherited by the query for the value for `fieldName` */ // Either return List[List[(SqlColumn, SqlColumn)]] or maybe List[SqlJoin]? - def parentConstraintsFromJoins(parentContext: Context, fieldName: String, resultName: String): Result[List[List[(SqlColumn, SqlColumn)]]] = { + def parentConstraintsFromJoins( + parentContext: Context, + fieldName: String, + resultName: String): Result[List[List[(SqlColumn, SqlColumn)]]] = { val tableContext = unembed(parentContext) parentContext.forField(fieldName, resultName).map { childContext => typeMappings.fieldMapping(parentContext, fieldName) match { @@ -3017,7 +3700,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - def parentConstraintsToSqlJoins(parentTable: TableRef, parentConstraints: List[List[(SqlColumn, SqlColumn)]]): Result[List[SqlJoin]] = + def parentConstraintsToSqlJoins( + parentTable: TableRef, + parentConstraints: List[List[(SqlColumn, SqlColumn)]]): Result[List[SqlJoin]] = if (parentConstraints.sizeCompare(1) <= 0) Nil.success else { val constraints = parentConstraints.last @@ -3038,7 +3723,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self object NonSubobjectSelect { def unapply(q: Query): Option[String] = q match { - case Select(fieldName, _, child) if child == Empty || isJsonb(context, fieldName) || !isLocallyMapped(context, q) => + case Select(fieldName, _, child) + if child == Empty || isJsonb(context, fieldName) || !isLocallyMapped( + context, + q) => Some(fieldName) case Select(fieldName, _, Effect(_, _)) => Some(fieldName) @@ -3060,9 +3748,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self for { fieldContext <- childContext(child) - countCol <- columnForAtomicField(context, fieldName) - sq <- loop(child, context, parentConstraints, exposeJoins) - parentTable <- parentTableForType(context) + countCol <- columnForAtomicField(context, fieldName) + sq <- loop(child, context, parentConstraints, exposeJoins) + parentTable <- parentTableForType(context) res <- sq match { case sq: SqlSelect => @@ -3071,57 +3759,113 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val keyCols = keyColumnsForType(fieldContext) val parentCols0 = hd.colsOf(parentTable) val wheres = hd.on.map { case (p, c) => Eql(c.toTerm, p.toTerm) } - val ssq = sq.copy(table = hd.child, cols = SqlColumn.CountColumn(countCol.in(hd.child), keyCols.map(_.in(hd.child))) :: Nil, joins = tl, wheres = wheres) + val ssq = sq.copy( + table = hd.child, + cols = SqlColumn.CountColumn( + countCol.in(hd.child), + keyCols.map(_.in(hd.child))) :: Nil, + joins = tl, + wheres = wheres) val ssqCol = SqlColumn.SubqueryColumn(countCol, ssq) - SqlSelect(context, Nil, parentTable, (ssqCol :: parentCols0).distinct, Nil, Nil, Nil, None, None, Nil, true, false).success + SqlSelect( + context, + Nil, + parentTable, + (ssqCol :: parentCols0).distinct, + Nil, + Nil, + Nil, + None, + None, + Nil, + true, + false).success case _ => val keyCols = keyColumnsForType(fieldContext) val countTable = sq.table - val ssq = sq.copy(cols = SqlColumn.CountColumn(countCol.in(countTable), keyCols.map(_.in(countTable))) :: Nil) + val ssq = sq.copy(cols = SqlColumn.CountColumn( + countCol.in(countTable), + keyCols.map(_.in(countTable))) :: Nil) val ssqCol = SqlColumn.SubqueryColumn(countCol, ssq) - SqlSelect(context, Nil, parentTable, ssqCol :: Nil, Nil, Nil, Nil, None, None, Nil, true, false).success + SqlSelect( + context, + Nil, + parentTable, + ssqCol :: Nil, + Nil, + Nil, + Nil, + None, + None, + Nil, + true, + false).success } case _ => - Result.internalError("Implementation restriction: cannot count an SQL union") + Result.internalError( + "Implementation restriction: cannot count an SQL union") } } yield res // Leaf, Json, Effect or mixed-in element: no SQL subobjects case NonSubobjectSelect(fieldName) => columnsForLeaf(context, fieldName).flatMap { cols => - val constraintCols = if(exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) else Nil + val constraintCols = + if (exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) else Nil val extraCols = keyColumnsForType(context) ++ constraintCols for { parentTable <- parentTableForType(context) - extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints) + extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints) } yield { val allCols = (cols ++ extraCols).distinct if (allCols.isEmpty) EmptySqlQuery(context) - else SqlSelect(context, Nil, parentTable, allCols, extraJoins, Nil, Nil, None, None, Nil, true, false) + else + SqlSelect( + context, + Nil, + parentTable, + allCols, + extraJoins, + Nil, + Nil, + None, + None, + Nil, + true, + false) } } // Non-leaf non-Json element: compile subobject queries - case s@Select(fieldName, resultName, child) => + case s @ Select(fieldName, resultName, child) => context.forField(fieldName, resultName).flatMap { fieldContext => - if(schema.isRootType(context.tpe)) loop(child, fieldContext, Nil, false) + if (schema.isRootType(context.tpe)) loop(child, fieldContext, Nil, false) else { val keyCols = keyColumnsForType(context) - val constraintCols = if(exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) else Nil + val constraintCols = + if (exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) + else Nil val extraCols = keyCols ++ constraintCols for { - parentTable <- parentTableForType(context) - parentConstraints0 <- parentConstraintsFromJoins(context, fieldName, s.resultName) - extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints) + parentTable <- parentTableForType(context) + parentConstraints0 <- parentConstraintsFromJoins( + context, + fieldName, + s.resultName) + extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints) res <- loop(child, fieldContext, parentConstraints0, false).flatMap { sq => - if(parentTable.isRoot) { - assert(parentConstraints0.isEmpty && extraCols.isEmpty && extraJoins.isEmpty) + if (parentTable.isRoot) { + assert( + parentConstraints0.isEmpty && extraCols.isEmpty && extraJoins.isEmpty) sq.withContext(context, Nil, Nil) } else - sq.nest(context, extraCols, sq.oneToOne && isSingular(context, fieldName, child), parentConstraints0.nonEmpty).flatMap { sq0 => - sq0.withContext(sq0.context, Nil, extraJoins) - } + sq.nest( + context, + extraCols, + sq.oneToOne && isSingular(context, fieldName, child), + parentConstraints0.nonEmpty) + .flatMap { sq0 => sq0.withContext(sq0.context, Nil, extraJoins) } } } yield res } @@ -3143,22 +3887,27 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val narrows = narrows0.filter(_.subtpe <:< supertpe) val subtpes = narrows.map(_.subtpe) - assert(supertpe.underlying.isInterface || supertpe.underlying.isUnion || (subtpes.sizeCompare(1) == 0 && subtpes.head =:= supertpe)) + assert( + supertpe.underlying.isInterface || supertpe.underlying.isUnion || (subtpes + .sizeCompare(1) == 0 && subtpes.head =:= supertpe)) subtpes.foreach(subtpe => assert(subtpe <:< supertpe)) val exhaustive = schema.exhaustive(supertpe, subtpes) val exclusive = default == Empty val allSimple = narrows.forall(narrow => isSimple(narrow.child)) - val extraCols = (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct + val extraCols = + (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct if (allSimple) { for { - dquery <- loop(default, context, parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil)) + dquery <- loop(default, context, parentConstraints, exposeJoins).flatMap( + _.withContext(context, extraCols, Nil)) nqueries <- narrows.traverse { narrow => val subtpe0 = narrow.subtpe.withModifiersOf(context.tpe) - loop(narrow.child, context.asType(subtpe0), parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil)) + loop(narrow.child, context.asType(subtpe0), parentConstraints, exposeJoins) + .flatMap(_.withContext(context, extraCols, Nil)) } res <- SqlQuery.combineAll(dquery :: nqueries) } yield res @@ -3167,32 +3916,53 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self narrows.traverse { narrow => val subtpe0 = narrow.subtpe.withModifiersOf(context.tpe) val child = Group(List(default, narrow.child)) - loop(child, context.asType(subtpe0), parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil)) + loop(child, context.asType(subtpe0), parentConstraints, exposeJoins) + .flatMap(_.withContext(context, extraCols, Nil)) } val dquery = - if(exhaustive) EmptySqlQuery(context).success + if (exhaustive) EmptySqlQuery(context).success else - discriminatorForType(context).map { disc => - subtpes.traverse(disc.discriminator.narrowPredicate) - }.getOrElse(Nil.success).flatMap { allPreds => - if (exclusive) { - for { - parentTable <- parentTableForType(context) - allPreds0 <- allPreds.traverse(pred => SqlQuery.contextualiseWhereTerms(context, parentTable, pred).map(Not(_))) - } yield { + discriminatorForType(context) + .map { disc => subtpes.traverse(disc.discriminator.narrowPredicate) } + .getOrElse(Nil.success) + .flatMap { allPreds => + if (exclusive) { + for { + parentTable <- parentTableForType(context) + allPreds0 <- allPreds.traverse(pred => + SqlQuery + .contextualiseWhereTerms(context, parentTable, pred) + .map(Not(_))) + } yield { + val defaultPredicate = And.combineAll(allPreds0) + SqlSelect( + context, + Nil, + parentTable, + extraCols, + Nil, + defaultPredicate :: Nil, + Nil, + None, + None, + Nil, + true, + false) + } + } else { + val allPreds0 = allPreds.map(Not(_)) val defaultPredicate = And.combineAll(allPreds0) - SqlSelect(context, Nil, parentTable, extraCols, Nil, defaultPredicate :: Nil, Nil, None, None, Nil, true, false) + loop( + Filter(defaultPredicate, default), + context, + parentConstraints, + exposeJoins).flatMap(_.withContext(context, extraCols, Nil)) } - } else { - val allPreds0 = allPreds.map(Not(_)) - val defaultPredicate = And.combineAll(allPreds0) - loop(Filter(defaultPredicate, default), context, parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil)) } - } for { - dquery0 <- dquery + dquery0 <- dquery nqueries0 <- nqueries } yield { val allSels = (dquery0 :: nqueries0).flatMap(_.asSelects).distinct @@ -3210,7 +3980,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case Group(queries) => group(queries) case Unique(child) => - loop(child, context.asType(context.tpe.nonNull.list), parentConstraints, exposeJoins).flatMap(_.withContext(context, Nil, Nil)) + loop( + child, + context.asType(context.tpe.nonNull.list), + parentConstraints, + exposeJoins).flatMap(_.withContext(context, Nil, Nil)) case Filter(False, _) => EmptySqlQuery(context).success case Filter(In(_, Nil), _) => EmptySqlQuery(context).success @@ -3218,7 +3992,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case FilterOrderByOffsetLimit(pred, oss, offset, lim, child) => val filterPaths = pred.map(SqlQuery.wherePaths).getOrElse(Nil).distinct - val orderPaths = oss.map(_.map(_.term).collect { case path: PathTerm => path.path }).getOrElse(Nil).distinct + val orderPaths = oss + .map(_.map(_.term).collect { case path: PathTerm => path.path }) + .getOrElse(Nil) + .distinct val filterOrderPaths = (filterPaths ++ orderPaths).distinct if (pred.exists(p => !isSqlTerm(context, p).getOrElse(false))) { @@ -3247,33 +4024,54 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val filter0 = (for { pred0 <- OptionT(pred.success) - sq <- OptionT(loop(filterQuery, context, parentConstraints, exposeJoins).map(_.some)) + sq <- OptionT( + loop(filterQuery, context, parentConstraints, exposeJoins).map(_.some)) } yield ((pred0, extractJoins(sq)), sq.oneToOne)).value val orderBy0 = (for { oss0 <- OptionT(oss.success) - sq <- OptionT(loop(orderQuery, context, parentConstraints, exposeJoins).map(_.some)) + sq <- OptionT( + loop(orderQuery, context, parentConstraints, exposeJoins).map(_.some)) } yield ((oss0, extractJoins(sq)), sq.oneToOne)).value for { - filter <- filter0 - orderBy <- orderBy0 - predIsOneToOne = filter.forall(_._2) && orderBy.forall(_._2) - expandedChild <- loop(expandedChildQuery, context, parentConstraints, true) - res <- expandedChild.addFilterOrderByOffsetLimit(filter.map(_._1), orderBy.map(_._1), offset, lim, predIsOneToOne, parentConstraints) + filter <- filter0 + orderBy <- orderBy0 + predIsOneToOne = filter.forall(_._2) && orderBy.forall(_._2) + expandedChild <- loop(expandedChildQuery, context, parentConstraints, true) + res <- expandedChild.addFilterOrderByOffsetLimit( + filter.map(_._1), + orderBy.map(_._1), + offset, + lim, + predIsOneToOne, + parentConstraints) } yield res } - case fool@(_: Filter | _: OrderBy | _: Offset | _: Limit) => + case fool @ (_: Filter | _: OrderBy | _: Offset | _: Limit) => Result.internalError(s"Filter/OrderBy/Offset/Limit not matched by extractor: $fool") case _: Introspect => - val extraCols = (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct - if(extraCols.isEmpty) EmptySqlQuery(context).success + val extraCols = + (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct + if (extraCols.isEmpty) EmptySqlQuery(context).success else parentTableForType(context).map { parentTable => - SqlSelect(context, Nil, parentTable, extraCols.distinct, Nil, Nil, Nil, None, None, Nil, true, false) + SqlSelect( + context, + Nil, + parentTable, + extraCols.distinct, + Nil, + Nil, + Nil, + None, + None, + Nil, + true, + false) } case Environment(_, child) => @@ -3284,9 +4082,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _: Count => Result.internalError("Count node must be a child of a Select node") - case Effect(_, _) => Result.internalError("Effect node must be a child of a Select node") + case Effect(_, _) => + Result.internalError("Effect node must be a child of a Select node") - case Empty | Query.Component(_, _, _) | (_: UntypedSelect) | (_: UntypedFragmentSpread) | (_: UntypedInlineFragment) | (_: Select) => + case Empty | Query.Component(_, _, _) | _: UntypedSelect | _: UntypedFragmentSpread | + _: UntypedInlineFragment | _: Select => EmptySqlQuery(context).success } } @@ -3297,14 +4097,21 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** MappedQuery implementation for a non-trivial SQL query */ + /** + * MappedQuery implementation for a non-trivial SQL query + */ final class NonEmptyMappedQuery(query: SqlQuery) extends MappedQuery { val index: Map[SqlColumn, Int] = query.cols.zipWithIndex.toMap val colsByResultPath: Map[List[String], List[(SqlColumn, Int)]] = - query.cols.filter(_.resultPath.nonEmpty).groupMap(_.resultPath)(col => (col, index(col))) + query + .cols + .filter(_.resultPath.nonEmpty) + .groupMap(_.resultPath)(col => (col, index(col))) - /** Execute this query in `F` */ + /** + * Execute this query in `F` + */ def fetch: F[Result[Table]] = { (for { frag <- ResultT(fragment.pure[F]) @@ -3312,7 +4119,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } yield Table(rows)).value } - /** The query rendered as a `Fragment` with all table and column aliases applied */ + /** + * The query rendered as a `Fragment` with all table and column aliases applied + */ lazy val fragment: Result[Fragment] = query.toFragment.runA(AliasState.empty) def selectAtomicField(context: Context, fieldName: String, table: Table): Result[Any] = @@ -3322,13 +4131,17 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self Result.internalError(s"Expected mapping for field '$fieldName' of type $obj") case col => - table.select(col).toResultOrError( - s"Expected single value for field '$fieldName' of type ${context.tpe.dealias} at ${context.path}, found many" - ) + table + .select(col) + .toResultOrError( + s"Expected single value for field '$fieldName' of type ${context.tpe.dealias} at ${context.path}, found many" + ) } - /** Does `table` contain subobjects of the type of the `narrowedContext` type */ + /** + * Does `table` contain subobjects of the type of the `narrowedContext` type + */ def narrowsTo(narrowedContext: Context, table: Table): Boolean = keyColumnsForType(narrowedContext) match { case Nil => false @@ -3336,7 +4149,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self table.definesAll(cols) } - /** Yield a `Table` containing only subojects of the `narrowedContext` type */ + /** + * Yield a `Table` containing only subojects of the `narrowedContext` type + */ def narrow(narrowedContext: Context, table: Table): Table = keyColumnsForType(narrowedContext) match { case Nil => table @@ -3344,9 +4159,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self table.filterDefined(cols) } - /** Yield a list of `Tables` one for each of the subobjects of the context type - * contained in `table`. - */ + /** + * Yield a list of `Tables` one for each of the subobjects of the context type contained + * in `table`. + */ def group(context: Context, table: Table): Iterator[Table] = table.group(keyColumnsForType(context)) @@ -3384,14 +4200,18 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case Some(List((_, i))) => i case Some(cols) => columnForAtomicField(context, fieldName) match { - case Result.Success(cursorCol) => cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1) - case Result.Warning(_, cursorCol) => cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1) + case Result.Success(cursorCol) => + cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1) + case Result.Warning(_, cursorCol) => + cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1) case _ => -1 } } } - /** MappedQuery implementation for a trivial SQL query */ + /** + * MappedQuery implementation for a trivial SQL query + */ object EmptyMappedQuery extends MappedQuery { def fetch: F[Result[Table]] = Table.EmptyTable.success.pure[F].widen @@ -3412,21 +4232,33 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Representation of an SQL query result */ + /** + * Representation of an SQL query result + */ sealed trait Table { def numRows: Int def numCols: Int - /** Yield the value of the given column */ + /** + * Yield the value of the given column + */ def select(col: Int): Option[Any] - /** A copy of this `Table` containing only the rows for which all the given columns are defined */ + + /** + * A copy of this `Table` containing only the rows for which all the given columns are + * defined + */ def filterDefined(cols: List[Int]): Table - /** True if all the given columns are defined, false otherwise */ + /** + * True if all the given columns are defined, false otherwise + */ def definesAll(cols: List[Int]): Boolean - /** Group this `Table` by the values of the given columns */ + /** + * Group this `Table` by the values of the given columns + */ def group(cols: List[Int]): Iterator[Table] def count(cols: List[Int]): Int @@ -3441,7 +4273,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self else MultiRowTable(rows) } - /** Specialized representation of an empty table */ + /** + * Specialized representation of an empty table + */ case object EmptyTable extends Table { def numRows: Int = 0 def numCols: Int = 0 @@ -3455,7 +4289,9 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self override def isEmpty = true } - /** Specialized representation of a table with exactly one row */ + /** + * Specialized representation of a table with exactly one row + */ case class OneRowTable(row: Array[Any]) extends Table { def numRows: Int = 1 def numCols = row.length @@ -3464,7 +4300,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self Some(row(col)) def filterDefined(cols: List[Int]): Table = - if(definesAll(cols)) this else EmptyTable + if (definesAll(cols)) this else EmptyTable def definesAll(cols: List[Int]): Boolean = { val cs = cols @@ -3498,13 +4334,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val c = col var value: Any = FailedJoin val ir = rows.iterator - while(ir.hasNext) { + while (ir.hasNext) { ir.next()(c) match { case FailedJoin => case v if value == FailedJoin => value = v case v if value == v => case None => - case v@Some(_) if value == None => value = v + case v @ Some(_) if value == None => value = v case _ => return None } } @@ -3533,7 +4369,6 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case cs => row => cs.map(c => row(c)) } - val nonNull = rows.filter(r => cs.forall(c => r(c) != FailedJoin)) val grouped = nonNull.groupBy(discrim) grouped.iterator.map { case (_, rows) => Table(rows) } @@ -3552,7 +4387,6 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case cs => row => cs.map(c => row(c)) } - val nonNull = rows.filter(r => cs.forall(c => r(c) != FailedJoin)) nonNull.map(discrim).distinct.size } @@ -3560,9 +4394,19 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** Cursor positioned at a GraphQL result non-leaf */ - case class SqlCursor(context: Context, focus: Any, mapped: MappedQuery, parent: Option[Cursor], env: Env) extends Cursor { - assert(focus != Table.EmptyTable || context.tpe.isNullable || context.tpe.isList || schema.isRootType(context.tpe) || parentTableForType(context).map(_.isRoot).getOrElse(false)) + /** + * Cursor positioned at a GraphQL result non-leaf + */ + case class SqlCursor( + context: Context, + focus: Any, + mapped: MappedQuery, + parent: Option[Cursor], + env: Env) + extends Cursor { + assert( + focus != Table.EmptyTable || context.tpe.isNullable || context.tpe.isList || schema + .isRootType(context.tpe) || parentTableForType(context).map(_.isRoot).getOrElse(false)) def withEnv(env0: Env): SqlCursor = copy(env = env.add(env0)) @@ -3587,20 +4431,29 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self tpe.isList def asList[C](factory: Factory[Cursor, C]): Result[C] = - tpe.item.map(_.dealias).map(itemTpe => - asTable.map { table => - val itemContext = context.asType(itemTpe) - mapped.group(itemContext, table).map(table => mkChild(itemContext, focus = table)).to(factory) - } - ).getOrElse(Result.internalError(s"Not a list: $tpe")) + tpe + .item + .map(_.dealias) + .map(itemTpe => + asTable.map { table => + val itemContext = context.asType(itemTpe) + mapped + .group(itemContext, table) + .map(table => mkChild(itemContext, focus = table)) + .to(factory) + }) + .getOrElse(Result.internalError(s"Not a list: $tpe")) def listSize: Result[Int] = - tpe.item.map(_.dealias).map(itemTpe => - asTable.map { table => - val itemContext = context.asType(itemTpe) - mapped.count(itemContext, table) - } - ).getOrElse(Result.internalError(s"Not a list: $tpe")) + tpe + .item + .map(_.dealias) + .map(itemTpe => + asTable.map { table => + val itemContext = context.asType(itemTpe) + mapped.count(itemContext, table) + }) + .getOrElse(Result.internalError(s"Not a list: $tpe")) def isNullable: Boolean = tpe.isNullable @@ -3608,7 +4461,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def asNullable: Result[Option[Cursor]] = (tpe, focus) match { case (NullableType(_), table: Table) if table.isEmpty => None.success - case (NullableType(tpe), _) => Some(mkChild(context.asType(tpe))).success // non-nullable column as nullable schema type (ok) + case (NullableType(tpe), _) => + Some( + mkChild( + context.asType(tpe))).success // non-nullable column as nullable schema type (ok) case _ => Result.internalError(s"Not nullable at ${context.path}") } @@ -3620,7 +4476,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def narrowsTo(subtpe: TypeRef): Result[Boolean] = { def check(ctpe: Type): Boolean = - if (ctpe =:= tpe) asTable.map(table => mapped.narrowsTo(context.asType(subtpe), table)).toOption.getOrElse(false) + if (ctpe =:= tpe) + asTable + .map(table => mapped.narrowsTo(context.asType(subtpe), table)) + .toOption + .getOrElse(false) else ctpe <:< subtpe if (!(subtpe <:< tpe)) false.success @@ -3651,13 +4511,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self asTable.flatMap { table => def mkCirceCursor(f: Json): Result[Cursor] = CirceCursor(fieldContext, focus = f, parent = Some(np), Env.empty).success - mapped.selectAtomicField(context, fieldName, table).flatMap(_ match { - case Some(j: Json) if fieldTpe.isNullable => mkCirceCursor(j) - case None => mkCirceCursor(Json.Null) - case j: Json if !fieldTpe.isNullable => mkCirceCursor(j) - case other => - Result.internalError(s"$fieldTpe: expected jsonb value found ${other.getClass}: $other") - }) + mapped + .selectAtomicField(context, fieldName, table) + .flatMap(_ match { + case Some(j: Json) if fieldTpe.isNullable => mkCirceCursor(j) + case None => mkCirceCursor(Json.Null) + case j: Json if !fieldTpe.isNullable => mkCirceCursor(j) + case other => + Result.internalError( + s"$fieldTpe: expected jsonb value found ${other.getClass}: $other") + }) } case Some((np, _: SqlField)) => @@ -3671,8 +4534,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } assert(leafFocus != FailedJoin) LeafCursor(fieldContext, leafFocus, Some(np), Env.empty) - } - ) + }) case Some((np, (_: SqlObject | _: EffectMapping))) => val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName) @@ -3685,10 +4547,10 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self Result.failure(s"No field '$fieldName' for type ${context.tpe}") } - // Fall back to the general implementation if it's a logic failure, + // Fall back to the general implementation if it's a logic failure, // but retain success and internal errors. - if (localField.isFailure) - mkCursorForField(this, fieldName, resultName) + if (localField.isFailure) + mkCursorForField(this, fieldName, resultName) else localField @@ -3703,13 +4565,20 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def context: Context = roots.head.context override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = { - roots.find(_.mapped.containsRoot(fieldName, resultName)).map(_.field(fieldName, resultName)). - getOrElse(Result.internalError(s"No field '$fieldName' for type ${context.tpe}")) + roots + .find(_.mapped.containsRoot(fieldName, resultName)) + .map(_.field(fieldName, resultName)) + .getOrElse(Result.internalError(s"No field '$fieldName' for type ${context.tpe}")) } } - /** Check SqlMapping specific TypeMapping validity */ - override protected def validateTypeMapping(mappings: TypeMappings, context: Context, tm: TypeMapping): List[ValidationFailure] = { + /** + * Check SqlMapping specific TypeMapping validity + */ + override protected def validateTypeMapping( + mappings: TypeMappings, + context: Context, + tm: TypeMapping): List[ValidationFailure] = { // ObjectMappings must have a key column // Associative fields must be keys // Unions and interfaces must have a discriminator @@ -3720,20 +4589,27 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self // Union field mappings must be hidden def checkKey(om: ObjectMapping): List[ValidationFailure] = - if (keyColumnsForType(context).nonEmpty || parentTableForType(context).map(_.isRoot).getOrElse(false)) Nil + if (keyColumnsForType(context).nonEmpty || parentTableForType(context) + .map(_.isRoot) + .getOrElse(false)) Nil else List(NoKeyInObjectTypeMapping(om)) def checkAssoc(om: ObjectMapping): List[ValidationFailure] = - om.fieldMappings.iterator.collect { - case sf: SqlField if sf.associative && !sf.key => - AssocFieldNotKey(om, sf) - }.toList + om.fieldMappings + .iterator + .collect { + case sf: SqlField if sf.associative && !sf.key => + AssocFieldNotKey(om, sf) + } + .toList def checkDiscriminator(om: ObjectMapping): List[ValidationFailure] = { - val dnmes = om.fieldMappings.collect { case sf: SqlField if sf.discriminator => sf.fieldName } + val dnmes = + om.fieldMappings.collect { case sf: SqlField if sf.discriminator => sf.fieldName } dnmes.collectFirstSome { dnme => typeMappings.rawFieldMapping(context, dnme) match { - case Some(pf: TypeMappings.PolymorphicFieldMapping) => Some((pf.candidates.head._2, pf.candidates.map(_._1.tpe.name))) + case Some(pf: TypeMappings.PolymorphicFieldMapping) => + Some((pf.candidates.head._2, pf.candidates.map(_._1.tpe.name))) case _ => None } } match { @@ -3756,7 +4632,8 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self def checkSuperInterfaces(om: ObjectMapping): List[ValidationFailure] = { val allMappings = om.tpe.dealias match { - case twf: TypeWithFields => om :: twf.interfaces.flatMap(nt => mappings.objectMapping(context.asType(nt))) + case twf: TypeWithFields => + om :: twf.interfaces.flatMap(nt => mappings.objectMapping(context.asType(nt))) case _ => Nil } val tables = allTables(allMappings) @@ -3779,14 +4656,17 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } def checkUnionFields(om: ObjectMapping): List[ValidationFailure] = - om.fieldMappings.iterator.collect { - case so: SqlObject => - IllegalSubobjectInUnionTypeMapping(om, so) - case sj: SqlJson => - IllegalJsonInUnionTypeMapping(om, sj) - case sf: SqlField if !sf.hidden => - NonHiddenUnionFieldMapping(om, sf) - }.toList + om.fieldMappings + .iterator + .collect { + case so: SqlObject => + IllegalSubobjectInUnionTypeMapping(om, so) + case sj: SqlJson => + IllegalJsonInUnionTypeMapping(om, sj) + case sf: SqlField if !sf.hidden => + NonHiddenUnionFieldMapping(om, sf) + } + .toList def isSql(om: ObjectMapping): Boolean = om.fieldMappings.exists { @@ -3799,30 +4679,36 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self tm match { case im: SqlInterfaceMapping => checkKey(im) ++ - checkAssoc(im) ++ - hasDiscriminator(im) ++ - checkDiscriminator(im) ++ - checkSplit(im) ++ - checkSuperInterfaces(im) + checkAssoc(im) ++ + hasDiscriminator(im) ++ + checkDiscriminator(im) ++ + checkSplit(im) ++ + checkSuperInterfaces(im) case um: SqlUnionMapping => checkKey(um) ++ - checkAssoc(um) ++ - hasDiscriminator(um) ++ - checkSplit(um) ++ - checkUnionMembers(um) ++ - checkUnionFields(um) + checkAssoc(um) ++ + hasDiscriminator(um) ++ + checkSplit(um) ++ + checkUnionMembers(um) ++ + checkUnionFields(um) case om: ObjectMapping if isSql(om) => - (if(schema.isRootType(om.tpe)) Nil else checkKey(om)) ++ - checkAssoc(om) ++ - checkSplit(om) ++ - checkSuperInterfaces(om) + (if (schema.isRootType(om.tpe)) Nil else checkKey(om)) ++ + checkAssoc(om) ++ + checkSplit(om) ++ + checkSuperInterfaces(om) case _ => super.validateTypeMapping(mappings, context, tm) } } - /** Check SqlMapping specific FieldMapping validity */ - override protected def validateFieldMapping(mappings: TypeMappings, context: Context, om: ObjectMapping, fm: FieldMapping): List[ValidationFailure] = { + /** + * Check SqlMapping specific FieldMapping validity + */ + override protected def validateFieldMapping( + mappings: TypeMappings, + context: Context, + om: ObjectMapping, + fm: FieldMapping): List[ValidationFailure] = { // GraphQL and DB schema nullability must be compatible // GraphQL and DB schema types must be compatible // Embedded objects are in the same table as their parent @@ -3845,11 +4731,13 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val colIsNullable = isNullable(sf.columnRef.codec) val fieldContext = context.forFieldOrAttribute(sf.fieldName, None) - val leafMapping0 = mappings.typeMapping(fieldContext).collectFirst { case lm: LeafMapping[_] => lm } + val leafMapping0 = + mappings.typeMapping(fieldContext).collectFirst { case lm: LeafMapping[_] => lm } (field.tpe.dealias.nonNull, leafMapping0) match { - case ((_: ScalarType)|(_: EnumType), Some(leafMapping)) => - if(colIsNullable != fieldIsNullable) - List(InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable)) + case (_: ScalarType | _: EnumType, Some(leafMapping)) => + if (colIsNullable != fieldIsNullable) + List( + InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable)) else (sf.columnRef.scalaTypeName, leafMapping.scalaTypeName) match { case (t0, t1) if t0 == t1 => Nil @@ -3863,25 +4751,26 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case _ => // Fallback to check only matching top level nullability // Missing LeafMapping will be reported elsewhere - if(colIsNullable != fieldIsNullable) - List(InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable)) + if (colIsNullable != fieldIsNullable) + List( + InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable)) else Nil } case (sj: SqlJson, Some(field)) => - if(sj.columnRef.scalaTypeName != JsonTypeName) + if (sj.columnRef.scalaTypeName != JsonTypeName) List(InconsistentFieldTypeMapping(om, sj, field, sj.columnRef, JsonTypeName)) else Nil - case (fm@SqlObject(fieldName, Nil), _) if !schema.isRootType(tpe) => + case (fm @ SqlObject(fieldName, Nil), _) if !schema.isRootType(tpe) => val parentTables0 = allTables(List(om)) - if(parentTables0.forall(TableName.isRoot)) Nil + if (parentTables0.forall(TableName.isRoot)) Nil else { val parentTables = parentTables0.filterNot(TableName.isRoot) (for { fieldContext <- context.forField(fieldName, None).toOption - com <- mappings.objectMapping(fieldContext) + com <- mappings.objectMapping(fieldContext) } yield { val childTables = allTables(List(com)) if (parentTables.sameElements(childTables)) Nil @@ -3893,7 +4782,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self val com0 = for { fieldContext <- context.forField(fieldName, None).toOption - com <- mappings.objectMapping(fieldContext) + com <- mappings.objectMapping(fieldContext) } yield com com0 match { @@ -3905,7 +4794,7 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self (parentTables, childTables) match { case (parentTable :: _, childTable :: _) => val nonEmpty = - if(joins.forall(_.conditions.nonEmpty)) Nil + if (joins.forall(_.conditions.nonEmpty)) Nil else List(NoJoinConditions(om, fm)) def consistentConditions(j: Join): Boolean = @@ -3918,7 +4807,11 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } val parConsistent = joins.filterNot(consistentConditions).map { j => - InconsistentJoinConditions(om, fm, j.conditions.map(_._1.table).distinct, j.conditions.map(_._2.table).distinct) + InconsistentJoinConditions( + om, + fm, + j.conditions.map(_._1.table).distinct, + j.conditions.map(_._2.table).distinct) } val serConsistent = { @@ -3927,15 +4820,18 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self case Nil => Nil // Empty joins will be reported elsewhere case hd :: tl => val headIsParent = hd.conditions.head._1.table == parentTable - val lastIsChild = nonEmptyJoins.last.conditions.head._2.table == childTable + val lastIsChild = + nonEmptyJoins.last.conditions.head._2.table == childTable val consistentChain = nonEmptyJoins.zip(tl).forall { - case (j0, j1) => j0.conditions.head._2.table == j1.conditions.head._1.table + case (j0, j1) => + j0.conditions.head._2.table == j1.conditions.head._1.table } - if(headIsParent && lastIsChild && consistentChain) Nil + if (headIsParent && lastIsChild && consistentChain) Nil else { - val path = nonEmptyJoins.map(j => (j.conditions.head._1.table, j.conditions.last._2.table)) + val path = nonEmptyJoins.map(j => + (j.conditions.head._1.table, j.conditions.last._2.table)) List(MisalignedJoins(om, fm, parentTable, childTable, path)) } @@ -3954,13 +4850,15 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } private def allTables(oms: List[ObjectMapping]): List[String] = - oms.flatMap(_.fieldMappings.flatMap { - case SqlField(_, columnRef, _, _, _, _) => List(columnRef.table) - case SqlJson(_, columnRef) => List(columnRef.table) - case SqlObject(_, Nil) => Nil - case SqlObject(_, joins) => joins.head.conditions.map(_._1.table) - case _ => Nil - }).distinct + oms + .flatMap(_.fieldMappings.flatMap { + case SqlField(_, columnRef, _, _, _, _) => List(columnRef.table) + case SqlJson(_, columnRef) => List(columnRef.table) + case SqlObject(_, Nil) => Nil + case SqlObject(_, joins) => joins.head.conditions.map(_._1.table) + case _ => Nil + }) + .distinct abstract class SqlValidationFailure(severity: Severity) extends ValidationFailure(severity) { protected def sql(a: Any) = s"$GREEN$a$RESET" @@ -3970,13 +4868,16 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self /* Join has no join conditions */ case class NoJoinConditions(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends SqlValidationFailure(Severity.Error) { + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName})" override def formattedMessage: String = s"""|No join conditions in field mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with a ${scala("Join")} with no join conditions. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has a ${scala( + "SqlObject")} field mapping for the field ${graphql( + fieldMapping.fieldName)} at (2) with a ${scala("Join")} with no join conditions. |- ${UNDERLINED}Joins must include at least one join condition.$RESET | |(1) ${objectMapping.pos} @@ -3984,15 +4885,25 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Parallel joins relate different tables */ - case class InconsistentJoinConditions(objectMapping: ObjectMapping, fieldMapping: FieldMapping, parents: List[String], children: List[String]) - extends SqlValidationFailure(Severity.Error) { + /** + * Parallel joins relate different tables + */ + case class InconsistentJoinConditions( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + parents: List[String], + children: List[String]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName}, (${parents.mkString(", ")}), (${children.mkString(", ")}))" override def formattedMessage: String = s"""|Inconsistent join conditions in field mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with a Join with inconsistent join conditions: ${sql(s"(${parents.mkString(", ")}) -> (${children.mkString(", ")})")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has a ${scala( + "SqlObject")} field mapping for the field ${graphql( + fieldMapping.fieldName)} at (2) with a Join with inconsistent join conditions: ${sql( + s"(${parents.mkString(", ")}) -> (${children.mkString(", ")})")}. |- ${UNDERLINED}All join conditions must relate the same tables.$RESET | |(1) ${objectMapping.pos} @@ -4000,15 +4911,26 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Serial joins are misaligned */ - case class MisalignedJoins(objectMapping: ObjectMapping, fieldMapping: FieldMapping, parent: String, child: String, path: List[(String, String)]) - extends SqlValidationFailure(Severity.Error) { + /** + * Serial joins are misaligned + */ + case class MisalignedJoins( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + parent: String, + child: String, + path: List[(String, String)]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName}, $parent, $child, ${path.mkString(", ")})" override def formattedMessage: String = s"""|Misaligned joins in field mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with misaligned joins: ${sql(s"$parent, $child, ${path.mkString(", ")}")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has a ${scala( + "SqlObject")} field mapping for the field ${graphql( + fieldMapping.fieldName)} at (2) with misaligned joins: ${sql( + s"$parent, $child, ${path.mkString(", ")}")}. |- ${UNDERLINED}Sequential joins must relate the parent table to the child table and chain correctly.$RESET | |(1) ${objectMapping.pos} @@ -4016,45 +4938,65 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Object type mapping has no key */ + /** + * Object type mapping has no key + */ case class NoKeyInObjectTypeMapping(objectMapping: ObjectMapping) - extends SqlValidationFailure(Severity.Error) { + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)})" override def formattedMessage: String = s"""|No key field mapping in object type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has no direct or inherited key field mapping. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} at (1) has no direct or inherited key field mapping. |- ${UNDERLINED}Object type mappings must include at least one direct or inherited key field mapping.$RESET | |(1) ${objectMapping.pos} |""".stripMargin } - /** Object type mapping is split across multiple tables */ + /** + * Object type mapping is split across multiple tables + */ case class SplitObjectTypeMapping(objectMapping: ObjectMapping, tables: List[String]) - extends SqlValidationFailure(Severity.Error) { + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${tables.mkString(", ")}))" override def formattedMessage: String = s"""|Object type mapping is split across multiple tables. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} defined at (1) is split across multiple tables: ${sql(s"${tables.mkString(", ")}")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} defined at (1) is split across multiple tables: ${sql( + s"${tables.mkString(", ")}")}. |- ${UNDERLINED}Object types must map to a single database table.$RESET | |(1) ${objectMapping.pos} |""".stripMargin } - /** Embedded object type mapping is split across non-parent tables */ - case class SplitEmbeddedObjectTypeMapping(parent: ObjectMapping, parentField: FieldMapping, child: ObjectMapping, parentTables: List[String], childTables: List[String]) - extends SqlValidationFailure(Severity.Error) { + /** + * Embedded object type mapping is split across non-parent tables + */ + case class SplitEmbeddedObjectTypeMapping( + parent: ObjectMapping, + parentField: FieldMapping, + child: ObjectMapping, + parentTables: List[String], + childTables: List[String]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${parent.showMappingType}, ${showNamedType(parent.tpe)}.${parentField.fieldName}, ${child.showMappingType}, ${showNamedType(child.tpe)}, (${childTables.mkString(", ")}))" override def formattedMessage: String = s"""|Embedded object type maps to non-parent tables. | - |- The ${scala(parent.showMappingType)} for type ${graphql(showNamedType(parent.tpe))} defined at (1) embeds the ${scala(child.showMappingType)} for type ${graphql(showNamedType(child.tpe))} defined at (2) via field mapping ${graphql(s"${showNamedType(parent.tpe)}.${parentField.fieldName}")} at (3). + |- The ${scala(parent.showMappingType)} for type ${graphql( + showNamedType(parent.tpe))} defined at (1) embeds the ${scala( + child.showMappingType)} for type ${graphql( + showNamedType(child.tpe))} defined at (2) via field mapping ${graphql( + s"${showNamedType(parent.tpe)}.${parentField.fieldName}")} at (3). |- The parent object is in table(s) ${sql(parentTables.mkString(", "))}. |- The embedded object is in non-parent table(s) ${sql(childTables.mkString(", "))}. |- ${UNDERLINED}Embedded objects must map to the same database tables as their parents.$RESET @@ -4065,61 +5007,91 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Interface/union implementation mappings split across multiple tables */ - case class SplitInterfaceTypeMapping(objectMapping: ObjectMapping, intrfs: List[ObjectMapping], tables: List[String]) - extends SqlValidationFailure(Severity.Error) { + /** + * Interface/union implementation mappings split across multiple tables + */ + case class SplitInterfaceTypeMapping( + objectMapping: ObjectMapping, + intrfs: List[ObjectMapping], + tables: List[String]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${intrfs.map(_.tpe.name).mkString(", ")}), (${tables.mkString(", ")}))" override def formattedMessage: String = s"""|Interface implementors are split across multiple tables. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has implementors (${intrfs.map(_.tpe.name).mkString(", ")}) which are split across multiple tables: ${sql(s"${tables.mkString(", ")}")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has implementors (${intrfs + .map(_.tpe.name) + .mkString(", ")}) which are split across multiple tables: ${sql( + s"${tables.mkString(", ")}")}. |- ${UNDERLINED}All implementors of an interface must map to a single database table.$RESET | |(1) ${objectMapping.pos} |""".stripMargin } - /** Interface/union implementation mappings split across multiple tables */ - case class SplitUnionTypeMapping(objectMapping: ObjectMapping, members: List[ObjectMapping], tables: List[String]) - extends SqlValidationFailure(Severity.Error) { + /** + * Interface/union implementation mappings split across multiple tables + */ + case class SplitUnionTypeMapping( + objectMapping: ObjectMapping, + members: List[ObjectMapping], + tables: List[String]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${members.map(_.tpe.name).mkString(", ")}), (${tables.mkString(", ")}))" override def formattedMessage: String = s"""|Union member mappings are split across multiple tables. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has members (${members.map(_.tpe.name).mkString(", ")}) which are split across multiple tables: ${sql(s"${tables.mkString(", ")}")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has members (${members + .map(_.tpe.name) + .mkString(", ")}) which are split across multiple tables: ${sql( + s"${tables.mkString(", ")}")}. |- ${UNDERLINED}All members of a union must map to a single database table.$RESET | |(1) ${objectMapping.pos} |""".stripMargin } - /** Interface/union type mapping has no discriminator */ + /** + * Interface/union type mapping has no discriminator + */ case class NoDiscriminatorInObjectTypeMapping(objectMapping: ObjectMapping) - extends SqlValidationFailure(Severity.Error) { + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)})" override def formattedMessage: String = s"""|No discriminator field mapping in interface/union type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has no discriminator field mapping. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) has no discriminator field mapping. |- ${UNDERLINED}interface/union type mappings must include at least one discriminator field mapping.$RESET | |(1) ${objectMapping.pos} |""".stripMargin } - /** Interface/union type mapping has a polymorphic discriminator */ - case class IllegalPolymorphicDiscriminatorFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, impls: List[String]) - extends SqlValidationFailure(Severity.Error) { + /** + * Interface/union type mapping has a polymorphic discriminator + */ + case class IllegalPolymorphicDiscriminatorFieldMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + impls: List[String]) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, (${impls.mkString(", ")}))" override def formattedMessage: String = s"""|Illegal polymorphic discriminator field mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a discriminator field mapping ${graphql(fieldMapping.fieldName)} at (2). - |- This discriminator has variant field mappings in the type mappings for subtypes: ${impls.mkString(", ")}. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} at (1) contains a discriminator field mapping ${graphql( + fieldMapping.fieldName)} at (2). + |- This discriminator has variant field mappings in the type mappings for subtypes: ${impls + .mkString(", ")}. |- ${UNDERLINED}Discriminator field mappings must not be polymorphic.$RESET | |(1) ${objectMapping.pos} @@ -4127,15 +5099,22 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Subobject field mappings not allowed in union type mappings */ - case class IllegalSubobjectInUnionTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends SqlValidationFailure(Severity.Error) { + /** + * Subobject field mappings not allowed in union type mappings + */ + case class IllegalSubobjectInUnionTypeMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Illegal subobject field mapping in union type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql(fieldMapping.fieldName)} at (2). + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql( + fieldMapping.fieldName)} at (2). |- ${UNDERLINED}Subobject field mappings are not allowed in union type mappings.$RESET | |(1) ${objectMapping.pos} @@ -4143,15 +5122,22 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** SqlJson field mappings not allowed in union type mappings */ - case class IllegalJsonInUnionTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends SqlValidationFailure(Severity.Error) { + /** + * SqlJson field mappings not allowed in union type mappings + */ + case class IllegalJsonInUnionTypeMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Illegal json field mapping in union type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql(fieldMapping.fieldName)} at (2). + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql( + fieldMapping.fieldName)} at (2). |- ${UNDERLINED}SqlJson field mappings are not allowed in union type mappings.$RESET | |(1) ${objectMapping.pos} @@ -4159,15 +5145,20 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Associative field must be a key */ + /** + * Associative field must be a key + */ case class AssocFieldNotKey(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends SqlValidationFailure(Severity.Error) { + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Non-key associatitve field mapping in object type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains an associative field mapping ${graphql(fieldMapping.fieldName)} at (2) which is not a key. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType( + objectMapping.tpe))} at (1) contains an associative field mapping ${graphql( + fieldMapping.fieldName)} at (2) which is not a key. |- ${UNDERLINED}All associative field mappings must be keys.$RESET | |(1) ${objectMapping.pos} @@ -4175,15 +5166,21 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** Union field mappings must be hidden */ - case class NonHiddenUnionFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping) - extends SqlValidationFailure(Severity.Error) { + /** + * Union field mappings must be hidden + */ + case class NonHiddenUnionFieldMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping) + extends SqlValidationFailure(Severity.Error) { override def toString: String = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})" override def formattedMessage: String = s"""|Non-hidden field mapping in union type mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql(fieldMapping.fieldName)} at (2) which is not hidden. + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql( + fieldMapping.fieldName)} at (2) which is not hidden. |- ${UNDERLINED}All fields mappings in a union type mapping must be hidden.$RESET | |(1) ${objectMapping.pos} @@ -4191,17 +5188,28 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self |""".stripMargin } - /** SqlField codec and LeafMapping are inconsistent. */ - case class InconsistentFieldLeafMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, leafMapping: LeafMapping[_]) - extends SqlValidationFailure(Severity.Error) { + /** + * SqlField codec and LeafMapping are inconsistent. + */ + case class InconsistentFieldLeafMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + field: Field, + columnRef: ColumnRef, + leafMapping: LeafMapping[_]) + extends SqlValidationFailure(Severity.Error) { override def toString() = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, ${columnRef.table}.${columnRef.column}:${columnRef.scalaTypeName}, ${showNamedType(leafMapping.tpe)}:${leafMapping.scalaTypeName})" override def formattedMessage: String = { s"""|Inconsistent field leaf mapping. | - |- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1). - |- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (3) map ${graphql(showNamedType(leafMapping.tpe))} to Scala type ${scala(columnRef.scalaTypeName)}. - |- A ${scala(leafMapping.showMappingType)} at (4) maps ${graphql(showNamedType(leafMapping.tpe))} to Scala type ${scala(leafMapping.scalaTypeName)}. + |- The field ${graphql( + s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1). + |- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql( + s"${columnRef.table}.${columnRef.column}")} at (3) map ${graphql( + showNamedType(leafMapping.tpe))} to Scala type ${scala(columnRef.scalaTypeName)}. + |- A ${scala(leafMapping.showMappingType)} at (4) maps ${graphql( + showNamedType(leafMapping.tpe))} to Scala type ${scala(leafMapping.scalaTypeName)}. |- ${UNDERLINED}The Scala types are inconsistent.$RESET | |(1) ${schema.pos} @@ -4212,16 +5220,26 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - /** SqlField codec and LeafMapping are inconsistent. */ - case class InconsistentFieldTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, scalaTypeName: String) - extends SqlValidationFailure(Severity.Error) { + /** + * SqlField codec and LeafMapping are inconsistent. + */ + case class InconsistentFieldTypeMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + field: Field, + columnRef: ColumnRef, + scalaTypeName: String) + extends SqlValidationFailure(Severity.Error) { override def toString() = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}:${showType(field.tpe)}, ${columnRef.table}.${columnRef.column}:${columnRef.scalaTypeName}, ${scalaTypeName})" override def formattedMessage: String = { s"""|Inconsistent field type mapping. | - |- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1). - |- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (3) map to Scala type ${scala(columnRef.scalaTypeName)}. + |- The field ${graphql( + s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1). + |- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql( + s"${columnRef.table}.${columnRef.column}")} at (3) map to Scala type ${scala( + columnRef.scalaTypeName)}. |- The expected Scala type is ${scala(scalaTypeName)}. |- ${UNDERLINED}The Scala types are inconsistent.$RESET | @@ -4232,21 +5250,30 @@ trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self } } - - /** SqlField codec and LeafMapping are inconsistent. */ - case class InconsistentlyNullableFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, colIsNullable: Boolean) - extends SqlValidationFailure(Severity.Error) { + /** + * SqlField codec and LeafMapping are inconsistent. + */ + case class InconsistentlyNullableFieldMapping( + objectMapping: ObjectMapping, + fieldMapping: FieldMapping, + field: Field, + columnRef: ColumnRef, + colIsNullable: Boolean) + extends SqlValidationFailure(Severity.Error) { override def toString() = s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, ${columnRef.table}.${columnRef.column})" override def formattedMessage: String = { - val fieldNullability = if(field.tpe.isNullable) "nullable" else "not nullable" - val colNullability = if(colIsNullable) "nullable" else "not nullable" + val fieldNullability = if (field.tpe.isNullable) "nullable" else "not nullable" + val colNullability = if (colIsNullable) "nullable" else "not nullable" s"""|Inconsistently nullable field mapping. | - |- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql(fieldMapping.fieldName)} at (2). + |- The ${scala(objectMapping.showMappingType)} for type ${graphql( + showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql( + fieldMapping.fieldName)} at (2). |- In the schema at (3) the field is ${fieldNullability}. - |- The corresponding ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (4) is ${colNullability}. + |- The corresponding ColumnRef for ${sql( + s"${columnRef.table}.${columnRef.column}")} at (4) is ${colNullability}. |- ${UNDERLINED}The nullabilities are inconsistent.$RESET | |(1) ${objectMapping.pos} diff --git a/modules/sql-core/src/main/scala/SqlModule.scala b/modules/sql-core/src/main/scala/SqlModule.scala index 4c7a7caf..8a1667cd 100644 --- a/modules/sql-core/src/main/scala/SqlModule.scala +++ b/modules/sql-core/src/main/scala/SqlModule.scala @@ -13,47 +13,67 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql import cats.{Monoid, Reducible} -/** These are the bits that are specific to the underlying database layer. */ +/** + * These are the bits that are specific to the underlying database layer. + */ trait SqlModule[F[_]] { def monitor: SqlMonitor[F, Fragment] - /** The type of a codec that reads and writes column values of type `A`. */ + /** + * The type of a codec that reads and writes column values of type `A`. + */ type Codec - /** The type of an encoder that writes column values of type `A`. */ + /** + * The type of an encoder that writes column values of type `A`. + */ type Encoder - /** Extract an encoder from a codec. */ + /** + * Extract an encoder from a codec. + */ def toEncoder(c: Codec): Encoder def isNullable(c: Codec): Boolean - /** Typeclass for SQL fragments. */ + /** + * Typeclass for SQL fragments. + */ trait SqlFragment[T] extends Monoid[T] { def bind[A](encoder: Encoder, value: A): T def const(s: String): T - /** Returns `(f1) AND (f2) AND ... (fn)` for all fragments. */ + /** + * Returns `(f1) AND (f2) AND ... (fn)` for all fragments. + */ def and(fs: T*): T - /** Returns `(f1) AND (f2) AND ... (fn)` for all defined fragments. */ + /** + * Returns `(f1) AND (f2) AND ... (fn)` for all defined fragments. + */ def andOpt(fs: Option[T]*): T - /** Returns `(f1) OR (f2) OR ... (fn)` for all defined fragments. */ + /** + * Returns `(f1) OR (f2) OR ... (fn)` for all defined fragments. + */ def orOpt(fs: Option[T]*): T - /** Returns `WHERE (f1) AND (f2) AND ... (fn)` or the empty fragment if `fs` is empty. */ + /** + * Returns `WHERE (f1) AND (f2) AND ... (fn)` or the empty fragment if `fs` is empty. + */ def whereAnd(fs: T*): T - /** Returns `WHERE (f1) AND (f2) AND ... (fn)` for defined `f`, if any, otherwise the empty fragment. */ + /** + * Returns `WHERE (f1) AND (f2) AND ... (fn)` for defined `f`, if any, otherwise the empty + * fragment. + */ def whereAndOpt(fs: Option[T]*): T def in[G[_]: Reducible, A](f: T, fs: G[A], enc: Encoder): T @@ -65,15 +85,17 @@ trait SqlModule[F[_]] { def sqlTypeName(codec: Codec): Option[String] } - /** The type of a fragment of SQL together with any interpolated arguments. */ + /** + * The type of a fragment of SQL together with any interpolated arguments. + */ type Fragment implicit def Fragments: SqlFragment[Fragment] - def intEncoder: Encoder - def stringEncoder: Encoder + def intEncoder: Encoder + def stringEncoder: Encoder def booleanEncoder: Encoder - def doubleEncoder: Encoder + def doubleEncoder: Encoder def intCodec: Codec diff --git a/modules/sql-core/src/main/scala/SqlMonitor.scala b/modules/sql-core/src/main/scala/SqlMonitor.scala index d7ba32ad..7e9f43d4 100644 --- a/modules/sql-core/src/main/scala/SqlMonitor.scala +++ b/modules/sql-core/src/main/scala/SqlMonitor.scala @@ -13,12 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql -import QueryInterpreter.ProtoJson +import grackle._ +import grackle.QueryInterpreter.ProtoJson -/** Monitor for a `SqlMapping` in `F` with fragments of type `A`. */ +/** + * Monitor for a `SqlMapping` in `F` with fragments of type `A`. + */ trait SqlMonitor[F[_], A] { def queryMapped(query: Query, fragment: A, rows: Int, cols: Int): F[Unit] def resultComputed(result: Result[ProtoJson]): F[Unit] diff --git a/modules/sql-core/src/main/scala/SqlStatsMonitor.scala b/modules/sql-core/src/main/scala/SqlStatsMonitor.scala index 93791513..df8cbe69 100644 --- a/modules/sql-core/src/main/scala/SqlStatsMonitor.scala +++ b/modules/sql-core/src/main/scala/SqlStatsMonitor.scala @@ -13,24 +13,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sql +package grackle.sql import cats.Applicative import cats.effect.Ref import cats.syntax.all._ -import SqlStatsMonitor.SqlStats +import grackle._ +import grackle.sql.SqlStatsMonitor.SqlStats /** * A SqlMonitor that accumulates `SqlStats` in a `Ref`. Stage boundaries and results are not * tracked. */ abstract class SqlStatsMonitor[F[_]: Applicative, A]( - ref: Ref[F, List[SqlStats]] + ref: Ref[F, List[SqlStats]] ) extends SqlMonitor[F, A] { - /** Get the current state and reset it to `Nil`. */ + /** + * Get the current state and reset it to `Nil`. + */ final def take: F[List[SqlStats]] = ref.getAndSet(Nil).map(_.reverse) @@ -43,7 +45,9 @@ abstract class SqlStatsMonitor[F[_]: Applicative, A]( final def resultComputed(result: Result[QueryInterpreter.ProtoJson]): F[Unit] = Applicative[F].unit - /** Extract the SQL string and query arguments from a fragment. */ + /** + * Extract the SQL string and query arguments from a fragment. + */ def inspect(fragment: A): (String, List[Any]) } @@ -51,14 +55,16 @@ abstract class SqlStatsMonitor[F[_]: Applicative, A]( object SqlStatsMonitor { final case class SqlStats( - val query: Query, - val sql: String, - val args: List[Any], - val rows: Int, - val cols: Int + val query: Query, + val sql: String, + val args: List[Any], + val rows: Int, + val cols: Int ) { - /** Normalize whitespace in `query` for easier testing. */ + /** + * Normalize whitespace in `query` for easier testing. + */ def normalize: SqlStats = copy(sql = sql.replaceAll("\\s+", " ").trim) diff --git a/modules/sql-core/src/test/scala/SqlArrayJoinMapping.scala b/modules/sql-core/src/test/scala/SqlArrayJoinMapping.scala index 424548d6..9a40b335 100644 --- a/modules/sql-core/src/test/scala/SqlArrayJoinMapping.scala +++ b/modules/sql-core/src/test/scala/SqlArrayJoinMapping.scala @@ -64,37 +64,33 @@ trait SqlArrayJoinMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("root") - ) + fieldMappings = List( + SqlObject("root") + ) ), ObjectMapping( tpe = RootType, - fieldMappings = - List( - SqlField("id", root.id, key = true), - SqlObject("listA", Join(root.id, listA.rootId)), - SqlObject("listB", Join(root.id, listB.rootId)) - ) + fieldMappings = List( + SqlField("id", root.id, key = true), + SqlObject("listA", Join(root.id, listA.rootId)), + SqlObject("listB", Join(root.id, listB.rootId)) + ) ), ObjectMapping( tpe = ElemAType, - fieldMappings = - List( - SqlField("id", listA.id, key = true), - SqlField("rootId", listA.rootId, hidden = true), - SqlField("elemA", listA.aElem) - ) + fieldMappings = List( + SqlField("id", listA.id, key = true), + SqlField("rootId", listA.rootId, hidden = true), + SqlField("elemA", listA.aElem) + ) ), ObjectMapping( tpe = ElemBType, - fieldMappings = - List( - SqlField("id", listB.id, key = true), - SqlField("rootId", listB.rootId, hidden = true), - SqlField("elemB", listB.bElem) - ) + fieldMappings = List( + SqlField("id", listB.id, key = true), + SqlField("rootId", listB.rootId, hidden = true), + SqlField("elemB", listB.bElem) + ) ) ) } diff --git a/modules/sql-core/src/test/scala/SqlArrayJoinSuite.scala b/modules/sql-core/src/test/scala/SqlArrayJoinSuite.scala index 4849f240..abf9d525 100644 --- a/modules/sql-core/src/test/scala/SqlArrayJoinSuite.scala +++ b/modules/sql-core/src/test/scala/SqlArrayJoinSuite.scala @@ -16,10 +16,10 @@ package grackle.sql.test import cats.effect.IO -import grackle._ import io.circe.literal._ import munit.CatsEffectSuite +import grackle._ import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlArrayJoinSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlCoalesceMapping.scala b/modules/sql-core/src/test/scala/SqlCoalesceMapping.scala index 7160e506..42d3ebc7 100644 --- a/modules/sql-core/src/test/scala/SqlCoalesceMapping.scala +++ b/modules/sql-core/src/test/scala/SqlCoalesceMapping.scala @@ -15,9 +15,10 @@ package grackle.sql.test -import grackle.syntax._ import java.time.OffsetDateTime +import grackle.syntax._ + trait SqlCoalesceMapping[F[_]] extends SqlTestMapping[F] { object r extends TableDef("r") { @@ -79,47 +80,42 @@ trait SqlCoalesceMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("r") - ) + fieldMappings = List( + SqlObject("r") + ) ), ObjectMapping( tpe = RType, - fieldMappings = - List( - SqlField("id", r.id, key = true), - SqlObject("ca", Join(r.id, ca.rid)), - SqlObject("cb", Join(r.id, cb.rid)), - SqlObject("cc", Join(r.id, cc.rid)), - ) + fieldMappings = List( + SqlField("id", r.id, key = true), + SqlObject("ca", Join(r.id, ca.rid)), + SqlObject("cb", Join(r.id, cb.rid)), + SqlObject("cc", Join(r.id, cc.rid)) + ) ), ObjectMapping( tpe = CAType, - fieldMappings = - List( - SqlField("id", ca.id, key = true), - SqlField("rid", ca.rid, hidden = true), - SqlField("a", ca.a) - ) + fieldMappings = List( + SqlField("id", ca.id, key = true), + SqlField("rid", ca.rid, hidden = true), + SqlField("a", ca.a) + ) ), ObjectMapping( tpe = CBType, - fieldMappings = - List( - SqlField("id", cb.id, key = true), - SqlField("rid", cb.rid, hidden = true), - SqlField("b", cb.b) - ) + fieldMappings = List( + SqlField("id", cb.id, key = true), + SqlField("rid", cb.rid, hidden = true), + SqlField("b", cb.b) + ) ), ObjectMapping( tpe = CCType, - fieldMappings = - List( - SqlField("id", cc.id, key = true), - SqlField("rid", cc.rid, hidden = true), - SqlField("c", cc.c) - ) + fieldMappings = List( + SqlField("id", cc.id, key = true), + SqlField("rid", cc.rid, hidden = true), + SqlField("c", cc.c) + ) ), LeafMapping[OffsetDateTime](DateTimeType) ) diff --git a/modules/sql-core/src/test/scala/SqlCoalesceSuite.scala b/modules/sql-core/src/test/scala/SqlCoalesceSuite.scala index c9101eaf..c7e1a3f2 100644 --- a/modules/sql-core/src/test/scala/SqlCoalesceSuite.scala +++ b/modules/sql-core/src/test/scala/SqlCoalesceSuite.scala @@ -22,8 +22,7 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ -import sql.SqlStatsMonitor - +import grackle.sql.SqlStatsMonitor import grackle.test.GraphQLResponseTests.assertWeaklyEqual trait SqlCoalesceSuite extends CatsEffectSuite { @@ -108,20 +107,21 @@ trait SqlCoalesceSuite extends CatsEffectSuite { val prog: IO[(Json, List[SqlStatsMonitor.SqlStats])] = for { - mm <- mapping + mm <- mapping (map, mon) = mm res <- map.compileAndRun(query) - ss <- mon.take + ss <- mon.take } yield (res, ss) - prog.map { case (res, stats) => - assertWeaklyEqual(res, expected) + prog.map { + case (res, stats) => + assertWeaklyEqual(res, expected) - val numQueries = stats.length - val numCells = stats.foldMap(s => s.rows * s.cols) + val numQueries = stats.length + val numCells = stats.foldMap(s => s.rows * s.cols) - assert(numQueries == 1) - assert(numCells == 40) + assert(numQueries == 1) + assert(numCells == 40) } } @@ -201,7 +201,7 @@ trait SqlCoalesceSuite extends CatsEffectSuite { """ for { - map <- mapping.map(_._1) + map <- mapping.map(_._1) res <- map.compileAndRun(query) } yield assertWeaklyEqual(res, expected) } diff --git a/modules/sql-core/src/test/scala/SqlComposedWorldMapping.scala b/modules/sql-core/src/test/scala/SqlComposedWorldMapping.scala index 6d04ff79..cbcc8c66 100644 --- a/modules/sql-core/src/test/scala/SqlComposedWorldMapping.scala +++ b/modules/sql-core/src/test/scala/SqlComposedWorldMapping.scala @@ -17,34 +17,38 @@ package grackle.sql.test import cats.effect.{Ref, Sync} import cats.implicits._ +import io.circe.Json + import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.QueryInterpreter.ProtoJson +import grackle.Value._ import grackle.sql.Like import grackle.syntax._ -import io.circe.Json - -import Query._ -import Predicate._ -import Value._ -import QueryCompiler._ -import QueryInterpreter.ProtoJson /* Currency component */ case class Currency( - code: String, - exchangeRate: Double, - countryCode: String + code: String, + exchangeRate: Double, + countryCode: String ) case class CurrencyData(currencies: Map[String, Currency]) { def exchangeRate(code: String): Option[Double] = currencies.get(code).map(_.exchangeRate) def update(code: String, exchangeRate: Double): Option[CurrencyData] = - currencies.get(code).map(currency => CurrencyData(currencies.updated(code, currency.copy(exchangeRate = exchangeRate)))) + currencies + .get(code) + .map(currency => + CurrencyData(currencies.updated(code, currency.copy(exchangeRate = exchangeRate)))) def currencies(countryCodes: List[String]): List[Currency] = countryCodes.flatMap(cc => currencies.values.find(_.countryCode == cc).toList) } -class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[F, Int]) extends ValueMapping[F] { +class CurrencyMapping[F[_]: Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[F, Int]) + extends ValueMapping[F] { def update(code: String, exchangeRate: Double): F[Unit] = dataRef.update(data => data.update(code, exchangeRate).getOrElse(data)) @@ -69,7 +73,10 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ override val selectElaborator = SelectElaborator { case (QueryType, "exchangeRate", List(Binding("code", StringValue(code)))) => Elab.env("code", code) - case (QueryType, "currencies", List(Binding("countryCodes", StringListValue(countryCodes)))) => + case ( + QueryType, + "currencies", + List(Binding("countryCodes", StringListValue(countryCodes)))) => Elab.env("countryCodes", countryCodes) } @@ -77,44 +84,44 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ List( ValueObjectMapping[Unit]( tpe = QueryType, - fieldMappings = - List( - RootEffect.computeCursor("exchangeRate")((path, env) => - env.getR[String]("code").traverse(code => - countRef.update(_+1) *> - dataRef.get.map(data => valueCursor(path, env, data.exchangeRate(code))) - ) - ), - RootEffect.computeCursor("currencies")((path, env) => - env.getR[List[String]]("countryCodes").traverse(countryCodes => - countRef.update(_+1) *> - dataRef.get.map(data => valueCursor(path, env, data.currencies(countryCodes))) - ) - ) - ) + fieldMappings = List( + RootEffect.computeCursor("exchangeRate")((path, env) => + env + .getR[String]("code") + .traverse(code => + countRef.update(_ + 1) *> + dataRef.get.map(data => valueCursor(path, env, data.exchangeRate(code))))), + RootEffect.computeCursor("currencies")((path, env) => + env + .getR[List[String]]("countryCodes") + .traverse(countryCodes => + countRef.update(_ + 1) *> + dataRef + .get + .map(data => valueCursor(path, env, data.currencies(countryCodes))))) + ) ), ValueObjectMapping[Currency]( tpe = CurrencyType, - fieldMappings = - List( - ValueField("code", _.code), - ValueField("exchangeRate", _.exchangeRate), - ValueField("countryCode", _.countryCode) - ) + fieldMappings = List( + ValueField("code", _.code), + ValueField("exchangeRate", _.exchangeRate), + ValueField("countryCode", _.countryCode) + ) ) - ) + ) override def combineAndRun(queries: List[(Query, Cursor)]): F[Result[List[ProtoJson]]] = { import SimpleCurrencyQuery.unpackResults val expandedQueries = queries.map { - case (Select("currencies", _, child), c@Code(code)) => + case (Select("currencies", _, child), c @ Code(code)) => (SimpleCurrencyQuery(List(code), child), c) case other => other } - if(expandedQueries.sizeCompare(1) <= 0) super.combineAndRun(expandedQueries) + if (expandedQueries.sizeCompare(1) <= 0) super.combineAndRun(expandedQueries) else { val indexedQueries = expandedQueries.zipWithIndex val (groupable, ungroupable) = indexedQueries.partition { @@ -126,28 +133,35 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ (q._1._1, q._1._2.fullEnv, q._1._2.context) val grouped: List[((Query, Cursor), List[Int])] = - (groupable.collect { - case ((SimpleCurrencyQuery(code, child), cursor), i) => - ((child, cursor), code, i) - }).groupBy(mkKey).toList.map { - case ((child, _, _), xs) => - val (codes, is) = xs.foldLeft((List.empty[String], List.empty[Int])) { - case ((codes, is), (_, code, i)) => (code :: codes, i :: is) - } - val cursor = xs.head._1._2 - ((SimpleCurrencyQuery(codes, child), cursor), is) - } + groupable + .collect { + case ((SimpleCurrencyQuery(code, child), cursor), i) => + ((child, cursor), code, i) + } + .groupBy(mkKey) + .toList + .map { + case ((child, _, _), xs) => + val (codes, is) = xs.foldLeft((List.empty[String], List.empty[Int])) { + case ((codes, is), (_, code, i)) => (code :: codes, i :: is) + } + val cursor = xs.head._1._2 + ((SimpleCurrencyQuery(codes, child), cursor), is) + } val (grouped0, groupedIndices) = grouped.unzip val (ungroupable0, ungroupedIndices) = ungroupable.unzip (for { - groupedResults <- ResultT(super.combineAndRun(grouped0)) + groupedResults <- ResultT(super.combineAndRun(grouped0)) ungroupedResults <- ResultT(super.combineAndRun(ungroupable0)) } yield { - val allResults: List[(ProtoJson, Int)] = groupedResults.zip(groupedIndices).flatMap((unpackResults _).tupled) ++ ungroupedResults.zip(ungroupedIndices) + val allResults: List[(ProtoJson, Int)] = groupedResults + .zip(groupedIndices) + .flatMap((unpackResults _).tupled) ++ ungroupedResults.zip(ungroupedIndices) val repackedResults = indexedQueries.map { - case (_, i) => allResults.find(_._2 == i).map(_._1).getOrElse(ProtoJson.fromJson(Json.Null)) + case (_, i) => + allResults.find(_._2 == i).map(_._1).getOrElse(ProtoJson.fromJson(Json.Null)) } repackedResults @@ -165,7 +179,8 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ def unapply(sel: Query): Option[(String, Query)] = sel match { - case Environment(env, Select("currencies", None, child)) => env.get[List[String]]("countryCodes").flatMap(_.headOption).map((_, child)) + case Environment(env, Select("currencies", None, child)) => + env.get[List[String]]("countryCodes").flatMap(_.headOption).map((_, child)) case _ => None } @@ -177,7 +192,9 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ fld <- obj("currencies") arr <- fld.asArray } yield { - val ress = arr.toList.map { elem => ProtoJson.fromJson(Json.obj("currencies" -> Json.arr(elem))) } + val ress = arr.toList.map { elem => + ProtoJson.fromJson(Json.obj("currencies" -> Json.arr(elem))) + } ress.zip(indices) }).getOrElse(Nil) case _ => Nil @@ -186,7 +203,7 @@ class CurrencyMapping[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ } object CurrencyMapping { - def apply[F[_] : Sync]: F[CurrencyMapping[F]] = { + def apply[F[_]: Sync]: F[CurrencyMapping[F]] = { val BRL = Currency("BRL", 0.25, "BRA") val EUR = Currency("EUR", 1.12, "NLD") val GBP = Currency("GBP", 1.25, "GBR") @@ -194,7 +211,7 @@ object CurrencyMapping { val data = CurrencyData(List(BRL, EUR, GBP).map(c => (c.code, c)).toMap) for { - dataRef <- Ref[F].of(data) + dataRef <- Ref[F].of(data) countRef <- Ref[F].of(0) } yield new CurrencyMapping[F](dataRef, countRef) } @@ -202,8 +219,8 @@ object CurrencyMapping { /* Composition */ -class SqlComposedMapping[F[_] : Sync] - (world: Mapping[F], currency: Mapping[F]) extends ComposedMapping[F] { +class SqlComposedMapping[F[_]: Sync](world: Mapping[F], currency: Mapping[F]) + extends ComposedMapping[F] { val schema = schema""" type Query { @@ -258,25 +275,24 @@ class SqlComposedMapping[F[_] : Sync] List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - Delegate("country", world), - Delegate("countries", world), - Delegate("cities", world) - ) + fieldMappings = List( + Delegate("country", world), + Delegate("countries", world), + Delegate("cities", world) + ) ), ObjectMapping( tpe = CountryType, - fieldMappings = - List( - Delegate("currencies", currency) - ) + fieldMappings = List( + Delegate("currencies", currency) + ) ) ) - override val selectElaborator = SelectElaborator { + override val selectElaborator = SelectElaborator { case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CountryType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CountryType / "code", Const(code)), child))) case (QueryType, "cities", List(Binding("namePattern", StringValue(namePattern)))) => Elab.transformChild(child => Filter(Like(CityType / "name", namePattern, true), child)) } diff --git a/modules/sql-core/src/test/scala/SqlComposedWorldSuite.scala b/modules/sql-core/src/test/scala/SqlComposedWorldSuite.scala index 636c80ca..90dfca47 100644 --- a/modules/sql-core/src/test/scala/SqlComposedWorldSuite.scala +++ b/modules/sql-core/src/test/scala/SqlComposedWorldSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.{assertWeaklyEqual, assertWeaklyEqualIO} trait SqlComposedWorldSuite extends CatsEffectSuite { @@ -138,13 +137,13 @@ trait SqlComposedWorldSuite extends CatsEffectSuite { """ val prg = - (for { - cm <- mapping - m = cm._2 - n0 <- cm._1.count + for { + cm <- mapping + m = cm._2 + n0 <- cm._1.count res <- m.compileAndRun(query) - n1 <- cm._1.count - } yield (res, n0, n1)) + n1 <- cm._1.count + } yield (res, n0, n1) prg.map { case (res, n0, n1) => @@ -236,17 +235,17 @@ trait SqlComposedWorldSuite extends CatsEffectSuite { """ val prg = - (for { - cm <- mapping - c = cm._1 - m = cm._2 - n0 <- cm._1.count + for { + cm <- mapping + c = cm._1 + m = cm._2 + n0 <- cm._1.count res0 <- m.compileAndRun(query) - n1 <- cm._1.count - _ <- c.update("EUR", 1.13) + n1 <- cm._1.count + _ <- c.update("EUR", 1.13) res1 <- m.compileAndRun(query) - n2 <- cm._1.count - } yield (res0, res1, n0, n1, n2)) + n2 <- cm._1.count + } yield (res0, res1, n0, n1, n2) prg.map { case (res0, res1, n0, n1, n2) => @@ -254,7 +253,7 @@ trait SqlComposedWorldSuite extends CatsEffectSuite { assertWeaklyEqual(res0, expected0) assertWeaklyEqual(res1, expected1) - } + } } test("composed query with introspection") { diff --git a/modules/sql-core/src/test/scala/SqlCompositeKeyMapping.scala b/modules/sql-core/src/test/scala/SqlCompositeKeyMapping.scala index 2f87d50e..da09736c 100644 --- a/modules/sql-core/src/test/scala/SqlCompositeKeyMapping.scala +++ b/modules/sql-core/src/test/scala/SqlCompositeKeyMapping.scala @@ -55,28 +55,30 @@ trait SqlCompositeKeyMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("parents") - ) + fieldMappings = List( + SqlObject("parents") + ) ), ObjectMapping( tpe = ParentType, - fieldMappings = - List( - SqlField("key1", compositeKeyParent.key1, key = true), - SqlField("key2", compositeKeyParent.key2, key = true), - SqlObject("children", Join(List((compositeKeyParent.key1, compositeKeyChild.parent1), (compositeKeyParent.key2, compositeKeyChild.parent2)))) - ) + fieldMappings = List( + SqlField("key1", compositeKeyParent.key1, key = true), + SqlField("key2", compositeKeyParent.key2, key = true), + SqlObject( + "children", + Join( + List( + (compositeKeyParent.key1, compositeKeyChild.parent1), + (compositeKeyParent.key2, compositeKeyChild.parent2)))) + ) ), ObjectMapping( tpe = ChildType, - fieldMappings = - List( - SqlField("id", compositeKeyChild.id, key = true), - SqlField("parent1", compositeKeyChild.parent1), - SqlField("parent2", compositeKeyChild.parent2), - ) + fieldMappings = List( + SqlField("id", compositeKeyChild.id, key = true), + SqlField("parent1", compositeKeyChild.parent1), + SqlField("parent2", compositeKeyChild.parent2) + ) ) ) } diff --git a/modules/sql-core/src/test/scala/SqlCompositeKeySuite.scala b/modules/sql-core/src/test/scala/SqlCompositeKeySuite.scala index 4f6d96d5..6108b140 100644 --- a/modules/sql-core/src/test/scala/SqlCompositeKeySuite.scala +++ b/modules/sql-core/src/test/scala/SqlCompositeKeySuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlCompositeKeySuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlCursorJsonMapping.scala b/modules/sql-core/src/test/scala/SqlCursorJsonMapping.scala index 1b272055..7740c95d 100644 --- a/modules/sql-core/src/test/scala/SqlCursorJsonMapping.scala +++ b/modules/sql-core/src/test/scala/SqlCursorJsonMapping.scala @@ -20,11 +20,11 @@ import io.circe.Json import io.circe.syntax.EncoderOps import grackle._ -import syntax._ -import Query._ -import Predicate._ -import Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ trait SqlCursorJsonMapping[F[_]] extends SqlTestMapping[F] { @@ -85,19 +85,17 @@ trait SqlCursorJsonMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("brands") - ) + fieldMappings = List( + SqlObject("brands") + ) ), ObjectMapping( tpe = BrandType, - fieldMappings = - List( - SqlField("id", brands.id, key = true), - SqlField("encodedCategories", brands.category, hidden = true), - CursorFieldJson("categories", decodeCategories, List("encodedCategories")) - ) + fieldMappings = List( + SqlField("id", brands.id, key = true), + SqlField("encodedCategories", brands.category, hidden = true), + CursorFieldJson("categories", decodeCategories, List("encodedCategories")) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlEmbedding2Mapping.scala b/modules/sql-core/src/test/scala/SqlEmbedding2Mapping.scala index 078be325..6cad9ae8 100644 --- a/modules/sql-core/src/test/scala/SqlEmbedding2Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlEmbedding2Mapping.scala @@ -15,12 +15,11 @@ package grackle.sql.test -import grackle._ -import Predicate._ -import Query._ -import QueryCompiler._ -import Value._ -import syntax._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ trait SqlEmbedding2Mapping[F[_]] extends SqlTestMapping[F] { @@ -30,7 +29,7 @@ trait SqlEmbedding2Mapping[F[_]] extends SqlTestMapping[F] { object ObservationTable extends TableDef("t_observation") { val Pid = col("c_program_id", varchar) - val Id = col("c_observation_id", varchar) + val Id = col("c_observation_id", varchar) } val schema = schema""" @@ -49,10 +48,10 @@ trait SqlEmbedding2Mapping[F[_]] extends SqlTestMapping[F] { } """ - val QueryType = schema.ref("Query") - val ProgramType = schema.ref("Program") + val QueryType = schema.ref("Query") + val ProgramType = schema.ref("Program") val ObservationSelectResultType = schema.ref("ObservationSelectResult") - val ObservationType = schema.ref("Observation") + val ObservationType = schema.ref("Observation") val typeMappings = List( @@ -73,13 +72,13 @@ trait SqlEmbedding2Mapping[F[_]] extends SqlTestMapping[F] { ObservationSelectResultType, List( SqlField("id", ProgramTable.Id, key = true, hidden = true), - SqlObject("matches", Join(ProgramTable.Id, ObservationTable.Pid)), + SqlObject("matches", Join(ProgramTable.Id, ObservationTable.Pid)) ) ), ObjectMapping( ObservationType, List( - SqlField("id", ObservationTable.Id, key = true), + SqlField("id", ObservationTable.Id, key = true) ) ) ) @@ -89,6 +88,9 @@ trait SqlEmbedding2Mapping[F[_]] extends SqlTestMapping[F] { Elab.transformChild(child => Unique(Filter(Eql(ProgramType / "id", Const(id)), child))) case (ObservationSelectResultType, "matches", Nil) => - Elab.transformChild(child => OrderBy(OrderSelections(List(OrderSelection[String](ObservationType / "id", true, true))), child)) + Elab.transformChild(child => + OrderBy( + OrderSelections(List(OrderSelection[String](ObservationType / "id", true, true))), + child)) } } diff --git a/modules/sql-core/src/test/scala/SqlEmbedding2Suite.scala b/modules/sql-core/src/test/scala/SqlEmbedding2Suite.scala index 57ddd2b9..a2f9ad8a 100644 --- a/modules/sql-core/src/test/scala/SqlEmbedding2Suite.scala +++ b/modules/sql-core/src/test/scala/SqlEmbedding2Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlEmbedding2Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlEmbedding3Mapping.scala b/modules/sql-core/src/test/scala/SqlEmbedding3Mapping.scala index 395d323c..ed40568c 100644 --- a/modules/sql-core/src/test/scala/SqlEmbedding3Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlEmbedding3Mapping.scala @@ -25,7 +25,7 @@ trait SqlEmbedding3Mapping[F[_]] extends SqlTestMapping[F] { object ObservationTable extends TableDef("t_observation") { val Pid = col("c_program_id", varchar) - val Id = col("c_observation_id", varchar) + val Id = col("c_observation_id", varchar) } val schema = schema""" @@ -40,9 +40,9 @@ trait SqlEmbedding3Mapping[F[_]] extends SqlTestMapping[F] { } """ - val QueryType = schema.ref("Query") + val QueryType = schema.ref("Query") val ObservationSelectResultType = schema.ref("ObservationSelectResult") - val ObservationType = schema.ref("Observation") + val ObservationType = schema.ref("Observation") val typeMappings = List( @@ -56,13 +56,13 @@ trait SqlEmbedding3Mapping[F[_]] extends SqlTestMapping[F] { ObservationSelectResultType, List( SqlField("", Bogus.Id, hidden = true), - SqlObject("matches"), + SqlObject("matches") ) ), ObjectMapping( ObservationType, List( - SqlField("id", ObservationTable.Id, key = true), + SqlField("id", ObservationTable.Id, key = true) ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlEmbedding3Suite.scala b/modules/sql-core/src/test/scala/SqlEmbedding3Suite.scala index 97a3c6ee..77daf65a 100644 --- a/modules/sql-core/src/test/scala/SqlEmbedding3Suite.scala +++ b/modules/sql-core/src/test/scala/SqlEmbedding3Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlEmbedding3Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlEmbeddingMapping.scala b/modules/sql-core/src/test/scala/SqlEmbeddingMapping.scala index 58ff12f7..4e0f514f 100644 --- a/modules/sql-core/src/test/scala/SqlEmbeddingMapping.scala +++ b/modules/sql-core/src/test/scala/SqlEmbeddingMapping.scala @@ -73,73 +73,65 @@ trait SqlEmbeddingMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("films"), - SqlObject("series") - ) + fieldMappings = List( + SqlObject("films"), + SqlObject("series") + ) ), ObjectMapping( tpe = FilmType, - fieldMappings = - List( - SqlField("title", films.title, key = true), - SqlObject("synopses") - ) + fieldMappings = List( + SqlField("title", films.title, key = true), + SqlObject("synopses") + ) ), ObjectMapping( tpe = SeriesType, - fieldMappings = - List( - SqlField("title", series.title, key = true), - SqlObject("synopses"), - SqlObject("episodes", Join(series.title, episodes.seriesTitle)), - ) + fieldMappings = List( + SqlField("title", series.title, key = true), + SqlObject("synopses"), + SqlObject("episodes", Join(series.title, episodes.seriesTitle)) + ) ), ObjectMapping( tpe = EpisodeType, - fieldMappings = - List( - SqlField("title", episodes.title, key = true), - SqlObject("synopses"), - SqlField("series_title", episodes.seriesTitle, hidden = true) - ) + fieldMappings = List( + SqlField("title", episodes.title, key = true), + SqlObject("synopses"), + SqlField("series_title", episodes.seriesTitle, hidden = true) + ) ), PrefixedMapping( tpe = SynopsesType, - mappings = - List( - List("films", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("title", films.title, key = true, hidden = true), - SqlField("short", films.synopsisShort), - SqlField("long", films.synopsisLong) - ) - ), - List("series", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("title", series.title, key = true, hidden = true), - SqlField("short", series.synopsisShort), - SqlField("long", series.synopsisLong) - ) - ), - List("episodes", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("title", episodes.title, key = true, hidden = true), - SqlField("short", episodes.synopsisShort), - SqlField("long", episodes.synopsisLong) - ) + mappings = List( + List("films", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("title", films.title, key = true, hidden = true), + SqlField("short", films.synopsisShort), + SqlField("long", films.synopsisLong) ) - ) + ), + List("series", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("title", series.title, key = true, hidden = true), + SqlField("short", series.synopsisShort), + SqlField("long", series.synopsisLong) + ) + ), + List("episodes", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("title", episodes.title, key = true, hidden = true), + SqlField("short", episodes.synopsisShort), + SqlField("long", episodes.synopsisLong) + ) + ) + ) ) ) } diff --git a/modules/sql-core/src/test/scala/SqlEmbeddingSuite.scala b/modules/sql-core/src/test/scala/SqlEmbeddingSuite.scala index 8504efd2..73396a67 100644 --- a/modules/sql-core/src/test/scala/SqlEmbeddingSuite.scala +++ b/modules/sql-core/src/test/scala/SqlEmbeddingSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlEmbeddingSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlFilterJoinAliasMapping.scala b/modules/sql-core/src/test/scala/SqlFilterJoinAliasMapping.scala index 5241afc4..ac43004b 100644 --- a/modules/sql-core/src/test/scala/SqlFilterJoinAliasMapping.scala +++ b/modules/sql-core/src/test/scala/SqlFilterJoinAliasMapping.scala @@ -18,11 +18,11 @@ package grackle.sql.test import cats.implicits._ import grackle._ -import syntax._ -import Predicate.{Const, Eql} -import Query.{Binding, Filter, Unique} -import QueryCompiler.{Elab, SelectElaborator} -import Value.{AbsentValue, NullValue, ObjectValue, StringValue} +import grackle.Predicate.{Const, Eql} +import grackle.Query.{Binding, Filter, Unique} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value.{AbsentValue, NullValue, ObjectValue, StringValue} +import grackle.syntax._ trait SqlFilterJoinAliasMapping[F[_]] extends SqlTestMapping[F] { @@ -69,35 +69,31 @@ trait SqlFilterJoinAliasMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("episode") - ) + fieldMappings = List( + SqlObject("episode") + ) ), ObjectMapping( tpe = EpisodeType, - fieldMappings = - List( - SqlField("id", episode.id, key = true), - SqlField("name", episode.name, key = true), - SqlObject("images", Join(episode.id, image.id)) - ) + fieldMappings = List( + SqlField("id", episode.id, key = true), + SqlField("name", episode.name, key = true), + SqlObject("images", Join(episode.id, image.id)) + ) ), ObjectMapping( tpe = ImageType, - fieldMappings = - List( - SqlField("id", image.id), - SqlField("publicUrl", image.publicUrl, key = true), - SqlObject("inner") - ) + fieldMappings = List( + SqlField("id", image.id), + SqlField("publicUrl", image.publicUrl, key = true), + SqlObject("inner") + ) ), ObjectMapping( tpe = InnerType, - fieldMappings = - List( - SqlField("name", image.name, key = true) - ) + fieldMappings = List( + SqlField("name", image.name, key = true) + ) ) ) @@ -113,7 +109,7 @@ trait SqlFilterJoinAliasMapping[F[_]] extends SqlTestMapping[F] { def mkFilter(query: Query, filter: Value): Result[Query] = { filter match { - case AbsentValue|NullValue => query.success + case AbsentValue | NullValue => query.success case FilterValue(pred) => Filter(pred, query).success case _ => Result.failure(s"Expected filter value, found $filter") } @@ -124,6 +120,6 @@ trait SqlFilterJoinAliasMapping[F[_]] extends SqlTestMapping[F] { Elab.transformChild(child => Unique(Filter(Eql(EpisodeType / "id", Const(id)), child))) case (EpisodeType, "images", List(Binding("filter", filter))) => - Elab.transformChild(child => mkFilter(child, filter)) + Elab.transformChild(child => mkFilter(child, filter)) } } diff --git a/modules/sql-core/src/test/scala/SqlFilterJoinAliasSuite.scala b/modules/sql-core/src/test/scala/SqlFilterJoinAliasSuite.scala index e5c79f8d..59072d32 100644 --- a/modules/sql-core/src/test/scala/SqlFilterJoinAliasSuite.scala +++ b/modules/sql-core/src/test/scala/SqlFilterJoinAliasSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlFilterJoinAliasSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Mapping.scala b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Mapping.scala index ebff1963..4fded8d8 100644 --- a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Mapping.scala @@ -15,10 +15,11 @@ package grackle.sql.test -import grackle._, syntax._ -import Query.{Binding, Limit} -import QueryCompiler.{Elab, SelectElaborator} -import Value.{AbsentValue, IntValue, NullValue} +import grackle._ +import grackle.Query.{Binding, Limit} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value.{AbsentValue, IntValue, NullValue} +import grackle.syntax._ trait SqlFilterOrderOffsetLimit2Mapping[F[_]] extends SqlTestMapping[F] { @@ -76,65 +77,66 @@ trait SqlFilterOrderOffsetLimit2Mapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("root"), - SqlObject("containers") - ) + fieldMappings = List( + SqlObject("root"), + SqlObject("containers") + ) ), ObjectMapping( tpe = RootType, - fieldMappings = - List( - SqlField("id", root.id, key = true), - SqlObject("containers", Join(root.id, containers.rootId)), - SqlObject("listA", Join(root.id, containers.rootId), Join(containers.id, listA.containerId)), - SqlObject("listB", Join(root.id, containers.rootId), Join(containers.id, listB.containerId)) - ) + fieldMappings = List( + SqlField("id", root.id, key = true), + SqlObject("containers", Join(root.id, containers.rootId)), + SqlObject( + "listA", + Join(root.id, containers.rootId), + Join(containers.id, listA.containerId)), + SqlObject( + "listB", + Join(root.id, containers.rootId), + Join(containers.id, listB.containerId)) + ) ), ObjectMapping( tpe = ContainerType, - fieldMappings = - List( - SqlField("id", containers.id, key = true), - SqlObject("listA", Join(containers.id, listA.containerId)), - SqlObject("listB", Join(containers.id, listB.containerId)) - ) + fieldMappings = List( + SqlField("id", containers.id, key = true), + SqlObject("listA", Join(containers.id, listA.containerId)), + SqlObject("listB", Join(containers.id, listB.containerId)) + ) ), ObjectMapping( tpe = ElemAType, - fieldMappings = - List( - SqlField("id", listA.id, key = true), - SqlField("rootId", listA.containerId, hidden = true), - ) + fieldMappings = List( + SqlField("id", listA.id, key = true), + SqlField("rootId", listA.containerId, hidden = true) + ) ), ObjectMapping( tpe = ElemBType, - fieldMappings = - List( - SqlField("id", listB.id, key = true), - SqlField("rootId", listB.containerId, hidden = true), - ) + fieldMappings = List( + SqlField("id", listB.id, key = true), + SqlField("rootId", listB.containerId, hidden = true) + ) ) ) def mkLimit(query: Query, limit: Value): Result[Query] = limit match { - case AbsentValue|NullValue => query.success + case AbsentValue | NullValue => query.success case IntValue(num) if num > 0 => Limit(num, query).success case IntValue(num) => Result.failure(s"Expected limit > 0, found $num") - case other => Result.failure(s"Expected limit > 0, found $other") + case other => Result.failure(s"Expected limit > 0, found $other") } override val selectElaborator = SelectElaborator { - case (QueryType, "root"|"containers", List(Binding("limit", limit))) => + case (QueryType, "root" | "containers", List(Binding("limit", limit))) => Elab.transformChild(child => mkLimit(child, limit)) - case (RootType, "containers"|"listA"|"listB", List(Binding("limit", limit))) => + case (RootType, "containers" | "listA" | "listB", List(Binding("limit", limit))) => Elab.transformChild(child => mkLimit(child, limit)) - case (ContainerType, "listA"|"listB", List(Binding("limit", limit))) => + case (ContainerType, "listA" | "listB", List(Binding("limit", limit))) => Elab.transformChild(child => mkLimit(child, limit)) } } diff --git a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Suite.scala b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Suite.scala index 28e6111d..9bfb8e38 100644 --- a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Suite.scala +++ b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimit2Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlFilterOrderOffsetLimit2Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitMapping.scala b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitMapping.scala index 06ce34db..00bbeb01 100644 --- a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitMapping.scala +++ b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitMapping.scala @@ -17,11 +17,12 @@ package grackle.sql.test import cats.implicits._ -import grackle._, syntax._ -import Predicate.{Const, Eql} -import Query.{Binding, Filter, Limit, Offset, OrderBy, OrderSelection, OrderSelections} -import QueryCompiler.{Elab, SelectElaborator} -import Value.{AbsentValue, EnumValue, IntValue, NullValue, ObjectValue, StringValue} +import grackle._ +import grackle.Predicate.{Const, Eql} +import grackle.Query.{Binding, Filter, Limit, Offset, OrderBy, OrderSelection, OrderSelections} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value.{AbsentValue, EnumValue, IntValue, NullValue, ObjectValue, StringValue} +import grackle.syntax._ trait SqlFilterOrderOffsetLimitMapping[F[_]] extends SqlTestMapping[F] { @@ -97,37 +98,33 @@ trait SqlFilterOrderOffsetLimitMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("root") - ) + fieldMappings = List( + SqlObject("root") + ) ), ObjectMapping( tpe = RootType, - fieldMappings = - List( - SqlField("id", root.id, key = true), - SqlObject("listA", Join(root.id, listA.rootId)), - SqlObject("listB", Join(root.id, listB.rootId)) - ) + fieldMappings = List( + SqlField("id", root.id, key = true), + SqlObject("listA", Join(root.id, listA.rootId)), + SqlObject("listB", Join(root.id, listB.rootId)) + ) ), ObjectMapping( tpe = ElemAType, - fieldMappings = - List( - SqlField("id", listA.id, key = true), - SqlField("rootId", listA.rootId, hidden = true), - SqlField("elemA", listA.aElem) - ) + fieldMappings = List( + SqlField("id", listA.id, key = true), + SqlField("rootId", listA.rootId, hidden = true), + SqlField("elemA", listA.aElem) + ) ), ObjectMapping( tpe = ElemBType, - fieldMappings = - List( - SqlField("id", listB.id, key = true), - SqlField("rootId", listB.rootId, hidden = true), - SqlField("elemB", listB.bElem) - ) + fieldMappings = List( + SqlField("id", listB.id, key = true), + SqlField("rootId", listB.rootId, hidden = true), + SqlField("elemB", listB.bElem) + ) ) ) @@ -143,67 +140,88 @@ trait SqlFilterOrderOffsetLimitMapping[F[_]] extends SqlTestMapping[F] { def mkOffset(query: Query, limit: Value): Result[Query] = limit match { - case AbsentValue|NullValue => query.success + case AbsentValue | NullValue => query.success case IntValue(num) if num == 0 => query.success case IntValue(num) if num > 0 => Offset(num, query).success case IntValue(num) => Result.failure(s"Expected offset >= 0, found $num") - case other => Result.failure(s"Expected offset >= 0, found $other") + case other => Result.failure(s"Expected offset >= 0, found $other") } def mkLimit(query: Query, limit: Value): Result[Query] = limit match { - case AbsentValue|NullValue => query.success + case AbsentValue | NullValue => query.success case IntValue(num) if num > 0 => Limit(num, query).success case IntValue(num) => Result.failure(s"Expected limit > 0, found $num") - case other => Result.failure(s"Expected limit > 0, found $other") + case other => Result.failure(s"Expected limit > 0, found $other") } def mkFilter(query: Query, tpe: Type, filter: Value): Result[Query] = { filter match { - case AbsentValue|NullValue => query.success + case AbsentValue | NullValue => query.success case FilterValue(id) => Filter(Eql(tpe / "id", Const(id)), query).success case _ => Result.failure(s"Expected filter value, found $filter") } } - def mkOrderBy(query: Query, order: Value)(oss: ListOrder => List[OrderSelection[_]]): Result[Query] = + def mkOrderBy(query: Query, order: Value)( + oss: ListOrder => List[OrderSelection[_]]): Result[Query] = order match { - case AbsentValue|NullValue => query.success - case OrderValue(o) => oss(o) match { - case Nil => query.success - case oss => OrderBy(OrderSelections(oss), query).success - } + case AbsentValue | NullValue => query.success + case OrderValue(o) => + oss(o) match { + case Nil => query.success + case oss => OrderBy(OrderSelections(oss), query).success + } case _ => Result.failure(s"Expected sort value, found $order") } override val selectElaborator = SelectElaborator { - case (QueryType, "root", List(Binding("filter", filter), Binding("offset", offset), Binding("limit", limit))) => + case ( + QueryType, + "root", + List( + Binding("filter", filter), + Binding("offset", offset), + Binding("limit", limit))) => Elab.transformChild(child => for { fc <- mkFilter(child, RootType, filter) oc <- mkOffset(fc, offset) lc <- mkLimit(oc, limit) - } yield lc - ) + } yield lc) - case (RootType, "listA", List(Binding("filter", filter), Binding("order", order), Binding("offset", offset), Binding("limit", limit))) => + case ( + RootType, + "listA", + List( + Binding("filter", filter), + Binding("order", order), + Binding("offset", offset), + Binding("limit", limit))) => Elab.transformChild(child => for { fc <- mkFilter(child, ElemAType, filter) - sc <- mkOrderBy(fc, order)(o => List(OrderSelection[Option[String]](ElemAType / "elemA", ascending = o.ascending))) + sc <- mkOrderBy(fc, order)(o => + List(OrderSelection[Option[String]](ElemAType / "elemA", ascending = o.ascending))) oc <- mkOffset(sc, offset) lc <- mkLimit(oc, limit) - } yield lc - ) + } yield lc) - case (RootType, "listB", List(Binding("filter", filter), Binding("order", order), Binding("offset", offset), Binding("limit", limit))) => + case ( + RootType, + "listB", + List( + Binding("filter", filter), + Binding("order", order), + Binding("offset", offset), + Binding("limit", limit))) => Elab.transformChild(child => for { fc <- mkFilter(child, ElemBType, filter) - sc <- mkOrderBy(fc, order)(o => List(OrderSelection[Option[Int]](ElemBType / "elemB", ascending = o.ascending))) + sc <- mkOrderBy(fc, order)(o => + List(OrderSelection[Option[Int]](ElemBType / "elemB", ascending = o.ascending))) oc <- mkOffset(sc, offset) lc <- mkLimit(oc, limit) - } yield lc - ) + } yield lc) } } diff --git a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitSuite.scala b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitSuite.scala index f83f9f0f..ca5d79bf 100644 --- a/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitSuite.scala +++ b/modules/sql-core/src/test/scala/SqlFilterOrderOffsetLimitSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlFilterOrderOffsetLimitSuite extends CatsEffectSuite { @@ -1241,7 +1240,10 @@ trait SqlFilterOrderOffsetLimitSuite extends CatsEffectSuite { val res = mapping.compileAndRun(query) - assertWeaklyEqualIO(res, expected, strictPaths = List(List("root", "listA"), List("root", "listB"))) + assertWeaklyEqualIO( + res, + expected, + strictPaths = List(List("root", "listA"), List("root", "listB"))) } test("alias with order on one side") { @@ -1377,7 +1379,10 @@ trait SqlFilterOrderOffsetLimitSuite extends CatsEffectSuite { val res = mapping.compileAndRun(query) - assertWeaklyEqualIO(res, expected, strictPaths = List(List("root", "one"), List("root", "two"))) + assertWeaklyEqualIO( + res, + expected, + strictPaths = List(List("root", "one"), List("root", "two"))) } test("order and limit on one side") { @@ -1533,7 +1538,10 @@ trait SqlFilterOrderOffsetLimitSuite extends CatsEffectSuite { val res = mapping.compileAndRun(query) - assertWeaklyEqualIO(res, expected, strictPaths = List(List("root", "listA"), List("root", "listB"))) + assertWeaklyEqualIO( + res, + expected, + strictPaths = List(List("root", "listA"), List("root", "listB"))) } test("alias with order and limit on one side") { @@ -1633,6 +1641,9 @@ trait SqlFilterOrderOffsetLimitSuite extends CatsEffectSuite { val res = mapping.compileAndRun(query) - assertWeaklyEqualIO(res, expected, strictPaths = List(List("root", "one"), List("root", "two"))) + assertWeaklyEqualIO( + res, + expected, + strictPaths = List(List("root", "one"), List("root", "two"))) } } diff --git a/modules/sql-core/src/test/scala/SqlGraphMapping.scala b/modules/sql-core/src/test/scala/SqlGraphMapping.scala index 8887f294..ad2ea870 100644 --- a/modules/sql-core/src/test/scala/SqlGraphMapping.scala +++ b/modules/sql-core/src/test/scala/SqlGraphMapping.scala @@ -17,10 +17,11 @@ package grackle.sql.test import cats.implicits._ -import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ trait SqlGraphMapping[F[_]] extends SqlTestMapping[F] { @@ -59,28 +60,28 @@ trait SqlGraphMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("node"), - SqlObject("edge") - ) + fieldMappings = List( + SqlObject("node"), + SqlObject("edge") + ) ), ObjectMapping( tpe = NodeType, - fieldMappings = - List( - SqlField("id", graphNode.id, key = true), - SqlObject("neighbours", Join(graphNode.id, graphEdge.a), Join(graphEdge.b, graphNode.id)) - ) + fieldMappings = List( + SqlField("id", graphNode.id, key = true), + SqlObject( + "neighbours", + Join(graphNode.id, graphEdge.a), + Join(graphEdge.b, graphNode.id)) + ) ), ObjectMapping( tpe = EdgeType, - fieldMappings = - List( - SqlField("id", graphEdge.id, key = true), - SqlObject("a", Join(graphEdge.a, graphNode.id)), - SqlObject("b", Join(graphEdge.b, graphNode.id)) - ) + fieldMappings = List( + SqlField("id", graphEdge.id, key = true), + SqlObject("a", Join(graphEdge.a, graphNode.id)), + SqlObject("b", Join(graphEdge.b, graphNode.id)) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlGraphSuite.scala b/modules/sql-core/src/test/scala/SqlGraphSuite.scala index ab6c161d..239e103d 100644 --- a/modules/sql-core/src/test/scala/SqlGraphSuite.scala +++ b/modules/sql-core/src/test/scala/SqlGraphSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlGraphSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlInterfacesMapping.scala b/modules/sql-core/src/test/scala/SqlInterfacesMapping.scala index f2b44b39..7daeb920 100644 --- a/modules/sql-core/src/test/scala/SqlInterfacesMapping.scala +++ b/modules/sql-core/src/test/scala/SqlInterfacesMapping.scala @@ -19,35 +19,35 @@ import cats.kernel.Eq import io.circe.Encoder import grackle._ -import syntax._ -import Predicate._ -import Query._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.syntax._ trait SqlInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def entityType: TestCodec[EntityType] object entities extends TableDef("entities") { - val id = col("id", text) - val entityType = col("entity_type", self.entityType) - val title = col("title", nullable(text)) - val filmRating = col("film_rating", nullable(text)) - val filmLabel = col("film_label", nullable(int4)) + val id = col("id", text) + val entityType = col("entity_type", self.entityType) + val title = col("title", nullable(text)) + val filmRating = col("film_rating", nullable(text)) + val filmLabel = col("film_label", nullable(int4)) val seriesNumberOfEpisodes = col("series_number_of_episodes", nullable(int4)) - val seriesLabel = col("series_label", nullable(text)) - val synopsisShort = col("synopsis_short", nullable(text)) - val synopsisLong = col("synopsis_long", nullable(text)) - val imageUrl = col("image_url", nullable(text)) - val hiddenImageUrl = col("hidden_image_url", nullable(text)) + val seriesLabel = col("series_label", nullable(text)) + val synopsisShort = col("synopsis_short", nullable(text)) + val synopsisLong = col("synopsis_long", nullable(text)) + val imageUrl = col("image_url", nullable(text)) + val hiddenImageUrl = col("hidden_image_url", nullable(text)) } object episodes extends TableDef("episodes") { - val id = col("id", text) - val title = col("title", nullable(text)) - val seriesId = col("series_id", text) - val synopsisShort = col("synopsis_short", nullable(text)) - val synopsisLong = col("synopsis_long", nullable(text)) + val id = col("id", text) + val title = col("title", nullable(text)) + val seriesId = col("series_id", text) + val synopsisShort = col("synopsis_short", nullable(text)) + val synopsisLong = col("synopsis_long", nullable(text)) } val schema = @@ -109,89 +109,80 @@ trait SqlInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("entities"), - SqlObject("films"), - ) + fieldMappings = List( + SqlObject("entities"), + SqlObject("films") + ) ), SqlInterfaceMapping( tpe = EType, discriminator = entityTypeDiscriminator, - fieldMappings = - List( - SqlField("id", entities.id, key = true), - SqlField("entityType", entities.entityType, discriminator = true), - SqlField("title", entities.title), - SqlObject("synopses") - ) + fieldMappings = List( + SqlField("id", entities.id, key = true), + SqlField("entityType", entities.entityType, discriminator = true), + SqlField("title", entities.title), + SqlObject("synopses") + ) ), ObjectMapping( tpe = FilmType, - fieldMappings = - List( - SqlField("rating", entities.filmRating), - SqlField("label", entities.filmLabel), - SqlField("imageUrl", entities.imageUrl) - ) + fieldMappings = List( + SqlField("rating", entities.filmRating), + SqlField("label", entities.filmLabel), + SqlField("imageUrl", entities.imageUrl) + ) ), ObjectMapping( tpe = SeriesType, - fieldMappings = - List( - SqlField("title", entities.title), - SqlField("numberOfEpisodes", entities.seriesNumberOfEpisodes), - SqlObject("episodes", Join(entities.id, episodes.seriesId)), - SqlField("label", entities.seriesLabel), - SqlField("hiddenImageUrl", entities.hiddenImageUrl, hidden = true), - CursorField("imageUrl", mkSeriesImageUrl, List("hiddenImageUrl")) - ) + fieldMappings = List( + SqlField("title", entities.title), + SqlField("numberOfEpisodes", entities.seriesNumberOfEpisodes), + SqlObject("episodes", Join(entities.id, episodes.seriesId)), + SqlField("label", entities.seriesLabel), + SqlField("hiddenImageUrl", entities.hiddenImageUrl, hidden = true), + CursorField("imageUrl", mkSeriesImageUrl, List("hiddenImageUrl")) + ) ), ObjectMapping( tpe = EpisodeType, - fieldMappings = - List( - SqlField("id", episodes.id, key = true), - SqlField("title", episodes.title), - SqlField("episodeId", episodes.seriesId, hidden = true), - SqlObject("synopses"), - ) + fieldMappings = List( + SqlField("id", episodes.id, key = true), + SqlField("title", episodes.title), + SqlField("episodeId", episodes.seriesId, hidden = true), + SqlObject("synopses") + ) ), PrefixedMapping( tpe = SynopsesType, - mappings = - List( - List("entities", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("id", entities.id, key = true, hidden = true), - SqlField("short", entities.synopsisShort), - SqlField("long", entities.synopsisLong) - ) - ), - List("films", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("id", entities.id, key = true, hidden = true), - SqlField("short", entities.synopsisShort), - SqlField("long", entities.synopsisLong) - ) - ), - List("entities", "episodes", "synopses") -> - ObjectMapping( - tpe = SynopsesType, - fieldMappings = - List( - SqlField("id", episodes.id, key = true, hidden = true), - SqlField("short", episodes.synopsisShort), - SqlField("long", episodes.synopsisLong) - ) + mappings = List( + List("entities", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("id", entities.id, key = true, hidden = true), + SqlField("short", entities.synopsisShort), + SqlField("long", entities.synopsisLong) ) - ) + ), + List("films", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("id", entities.id, key = true, hidden = true), + SqlField("short", entities.synopsisShort), + SqlField("long", entities.synopsisLong) + ) + ), + List("entities", "episodes", "synopses") -> + ObjectMapping( + tpe = SynopsesType, + fieldMappings = List( + SqlField("id", episodes.id, key = true, hidden = true), + SqlField("short", episodes.synopsisShort), + SqlField("long", episodes.synopsisLong) + ) + ) + ) ), LeafMapping[EntityType](EntityTypeType) ) @@ -219,11 +210,13 @@ trait SqlInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => } def mkSeriesImageUrl(c: Cursor): Result[Option[String]] = - c.fieldAs[Option[String]]("hiddenImageUrl").map(_.map(hiu => s"http://example.com/series/$hiu")) + c.fieldAs[Option[String]]("hiddenImageUrl") + .map(_.map(hiu => s"http://example.com/series/$hiu")) override val selectElaborator = SelectElaborator { case (QueryType, "films", Nil) => - Elab.transformChild(child => Filter(Eql[EntityType](FilmType / "entityType", Const(EntityType.Film)), child)) + Elab.transformChild(child => + Filter(Eql[EntityType](FilmType / "entityType", Const(EntityType.Film)), child)) } sealed trait EntityType extends Product with Serializable @@ -235,7 +228,7 @@ trait SqlInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def fromString(s: String): Option[EntityType] = s.trim.toUpperCase match { - case "FILM" => Some(Film) + case "FILM" => Some(Film) case "SERIES" => Some(Series) case _ => None } @@ -254,7 +247,7 @@ trait SqlInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def toInt(e: EntityType): Int = e match { - case Film => 1 + case Film => 1 case Series => 2 } } diff --git a/modules/sql-core/src/test/scala/SqlInterfacesMapping2.scala b/modules/sql-core/src/test/scala/SqlInterfacesMapping2.scala index b6c92287..992f008c 100644 --- a/modules/sql-core/src/test/scala/SqlInterfacesMapping2.scala +++ b/modules/sql-core/src/test/scala/SqlInterfacesMapping2.scala @@ -16,8 +16,8 @@ package grackle.sql.test import grackle._ +import grackle.Predicate._ import grackle.syntax._ -import Predicate._ trait SqlInterfacesMapping2[F[_]] extends SqlInterfacesMapping[F] { self => diff --git a/modules/sql-core/src/test/scala/SqlInterfacesSuite.scala b/modules/sql-core/src/test/scala/SqlInterfacesSuite.scala index 3f599b9c..350f7544 100644 --- a/modules/sql-core/src/test/scala/SqlInterfacesSuite.scala +++ b/modules/sql-core/src/test/scala/SqlInterfacesSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlInterfacesSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlJsonbMapping.scala b/modules/sql-core/src/test/scala/SqlJsonbMapping.scala index 6461f482..dad216ef 100644 --- a/modules/sql-core/src/test/scala/SqlJsonbMapping.scala +++ b/modules/sql-core/src/test/scala/SqlJsonbMapping.scala @@ -17,13 +17,11 @@ package grackle.sql.test import cats.implicits._ -import grackle._ -import syntax._ - -import Query._ -import Predicate._ -import Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ trait SqlJsonbMapping[F[_]] extends SqlTestMapping[F] { @@ -80,21 +78,19 @@ trait SqlJsonbMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("record"), - SqlObject("records") - ) + fieldMappings = List( + SqlObject("record"), + SqlObject("records") + ) ), ObjectMapping( tpe = RowType, - fieldMappings = - List( - SqlField("id", records.id, key = true), - SqlJson("record", records.record), - SqlJson("nonNullRecord", records.record) - ) - ), + fieldMappings = List( + SqlField("id", records.id, key = true), + SqlJson("record", records.record), + SqlJson("nonNullRecord", records.record) + ) + ) ) override val selectElaborator = SelectElaborator { diff --git a/modules/sql-core/src/test/scala/SqlJsonbSuite.scala b/modules/sql-core/src/test/scala/SqlJsonbSuite.scala index c8147d8e..cb329796 100644 --- a/modules/sql-core/src/test/scala/SqlJsonbSuite.scala +++ b/modules/sql-core/src/test/scala/SqlJsonbSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlJsonbSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlLikeMapping.scala b/modules/sql-core/src/test/scala/SqlLikeMapping.scala index 2ecaf525..7a5a9d48 100644 --- a/modules/sql-core/src/test/scala/SqlLikeMapping.scala +++ b/modules/sql-core/src/test/scala/SqlLikeMapping.scala @@ -16,10 +16,12 @@ package grackle.sql.test import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.sql.Like import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ -import sql.Like trait SqlLikeMapping[F[_]] extends SqlTestMapping[F] { @@ -52,23 +54,21 @@ trait SqlLikeMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("likes"), - SqlObject("likeNotNullableNotNullable"), - SqlObject("likeNotNullableNullable"), - SqlObject("likeNullableNotNullable"), - SqlObject("likeNullableNullable") - ) + fieldMappings = List( + SqlObject("likes"), + SqlObject("likeNotNullableNotNullable"), + SqlObject("likeNotNullableNullable"), + SqlObject("likeNullableNotNullable"), + SqlObject("likeNullableNullable") + ) ), ObjectMapping( tpe = LikeType, - fieldMappings = - List( - SqlField("id", likes.id, key = true), - SqlField("notNullable", likes.notNullableText), - SqlField("nullable", likes.nullableText) - ) + fieldMappings = List( + SqlField("id", likes.id, key = true), + SqlField("notNullable", likes.notNullableText), + SqlField("nullable", likes.nullableText) + ) ) ) @@ -98,16 +98,30 @@ trait SqlLikeMapping[F[_]] extends SqlTestMapping[F] { } override val selectElaborator = SelectElaborator { - case (QueryType, "likeNotNullableNotNullable", List(Binding("pattern", NonNullablePattern(pattern)))) => - Elab.transformChild(child => Filter(Like(LikeType / "notNullable", pattern, false), child)) + case ( + QueryType, + "likeNotNullableNotNullable", + List(Binding("pattern", NonNullablePattern(pattern)))) => + Elab.transformChild(child => + Filter(Like(LikeType / "notNullable", pattern, false), child)) - case (QueryType, "likeNotNullableNullable", List(Binding("pattern", NullablePattern(pattern)))) => - Elab.transformChild(child => Filter(mkPredicate(LikeType / "notNullable", pattern), child)) + case ( + QueryType, + "likeNotNullableNullable", + List(Binding("pattern", NullablePattern(pattern)))) => + Elab.transformChild(child => + Filter(mkPredicate(LikeType / "notNullable", pattern), child)) - case (QueryType, "likeNullableNotNullable", List(Binding("pattern", NonNullablePattern(pattern)))) => + case ( + QueryType, + "likeNullableNotNullable", + List(Binding("pattern", NonNullablePattern(pattern)))) => Elab.transformChild(child => Filter(Like(LikeType / "nullable", pattern, false), child)) - case (QueryType, "likeNullableNullable", List(Binding("pattern", NullablePattern(pattern)))) => + case ( + QueryType, + "likeNullableNullable", + List(Binding("pattern", NullablePattern(pattern)))) => Elab.transformChild(child => Filter(mkPredicate(LikeType / "nullable", pattern), child)) } } diff --git a/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidMapping.scala b/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidMapping.scala index 415de50e..6a550b28 100644 --- a/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidMapping.scala +++ b/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidMapping.scala @@ -137,97 +137,92 @@ trait SqlMappingValidatorInvalidMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("scalars"), - SqlObject("objs"), - SqlObject("union") - ) + fieldMappings = List( + SqlObject("scalars"), + SqlObject("objs"), + SqlObject("union") + ) ), ObjectMapping( tpe = ScalarsType, - fieldMappings = - List( - SqlField("boolField1", scalars.nullableBoolCol), - SqlField("boolField2", scalars.intCol), - SqlField("nullableBoolField1", scalars.boolCol), - SqlField("nullableBoolField2", scalars.nullableIntCol), - - SqlField("stringsField", scalars.nullableStringsCol), - SqlField("nullableStringsField", scalars.stringsCol), - - SqlJson("jsonbField", scalars.boolCol), - SqlJson("nullableJsonbField", scalars.nullableBoolCol) - ) + fieldMappings = List( + SqlField("boolField1", scalars.nullableBoolCol), + SqlField("boolField2", scalars.intCol), + SqlField("nullableBoolField1", scalars.boolCol), + SqlField("nullableBoolField2", scalars.nullableIntCol), + + SqlField("stringsField", scalars.nullableStringsCol), + SqlField("nullableStringsField", scalars.stringsCol), + + SqlJson("jsonbField", scalars.boolCol), + SqlJson("nullableJsonbField", scalars.nullableBoolCol) + ) ), SqlInterfaceMapping( tpe = IntrfType, discriminator = objectTypeDiscriminator, - fieldMappings = - List( - SqlField("id", obj1.idCol), - SqlField("typeField", obj1.typeCol, hidden = true) - ) + fieldMappings = List( + SqlField("id", obj1.idCol), + SqlField("typeField", obj1.typeCol, hidden = true) + ) ), ObjectMapping( tpe = Obj1Type, - fieldMappings = - List( - SqlField("id", obj1.idCol), - SqlField("intField", obj1.intCol), - SqlObject("embedded"), - SqlObject("sub1", Join(obj1.idCol, join.parentIdCol), Join(join.childIdCol, obj1.idCol)) - ) + fieldMappings = List( + SqlField("id", obj1.idCol), + SqlField("intField", obj1.intCol), + SqlObject("embedded"), + SqlObject( + "sub1", + Join(obj1.idCol, join.parentIdCol), + Join(join.childIdCol, obj1.idCol)) + ) ), ObjectMapping( tpe = Obj2Type, - fieldMappings = - List( - SqlField("id", obj2.idCol), - SqlField("assoc", obj1.idCol, associative = true, hidden = true), - SqlField("boolField", obj2.boolCol), - SqlObject("sub2", Join(List((obj2.idCol, subObj2.idCol), (obj2.boolCol, subObj3.idCol)))) - ) + fieldMappings = List( + SqlField("id", obj2.idCol), + SqlField("assoc", obj1.idCol, associative = true, hidden = true), + SqlField("boolField", obj2.boolCol), + SqlObject( + "sub2", + Join(List((obj2.idCol, subObj2.idCol), (obj2.boolCol, subObj3.idCol)))) + ) ), ObjectMapping( tpe = Obj3Type, - fieldMappings = - List( - SqlField("id", obj2.idCol, key = true, hidden = true), - SqlField("stringField", obj2.stringCol), - SqlObject("sub3", Join(Nil)) - ) + fieldMappings = List( + SqlField("id", obj2.idCol, key = true, hidden = true), + SqlField("stringField", obj2.stringCol), + SqlObject("sub3", Join(Nil)) + ) ), SqlUnionMapping( tpe = UnionType, discriminator = objectTypeDiscriminator, - fieldMappings = - List( - SqlField("id", obj1.idCol), - SqlField("typeField", obj1.typeCol, hidden = true), - SqlObject("bogus", Nil) - ) + fieldMappings = List( + SqlField("id", obj1.idCol), + SqlField("typeField", obj1.typeCol, hidden = true), + SqlObject("bogus", Nil) + ) ), ObjectMapping( tpe = SubObj1Type, - fieldMappings = - List( - SqlField("id", subObj1.idCol, key = true) - ) + fieldMappings = List( + SqlField("id", subObj1.idCol, key = true) + ) ), ObjectMapping( tpe = SubObj2Type, - fieldMappings = - List( - SqlField("id", subObj2.idCol, key = true) - ) + fieldMappings = List( + SqlField("id", subObj2.idCol, key = true) + ) ), ObjectMapping( tpe = SubObj3Type, - fieldMappings = - List( - SqlField("id", subObj3.idCol, key = true) - ) + fieldMappings = List( + SqlField("id", subObj3.idCol, key = true) + ) ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidSuite.scala b/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidSuite.scala index cdf739c6..ce1c539d 100644 --- a/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidSuite.scala +++ b/modules/sql-core/src/test/scala/SqlMappingValidatorInvalidSuite.scala @@ -17,45 +17,72 @@ package grackle.sql.test import cats.effect.IO import cats.implicits._ -import grackle._ -import grackle.sql._ import munit.CatsEffectSuite import org.tpolecat.typename.typeName +import grackle._ +import grackle.sql._ + trait SqlMappingValidatorInvalidSuite extends CatsEffectSuite { def mapping: SqlMappingLike[IO] lazy val M = mapping - def check(es: List[ValidationFailure])(expected: PartialFunction[List[ValidationFailure], Unit]): Unit = + def check(es: List[ValidationFailure])( + expected: PartialFunction[List[ValidationFailure], Unit]): Unit = es match { case `expected`(_) => () case _ => fail(es.foldMap(_.toErrorMessage)) } object INFM { - def unapply(vf: ValidationFailure): Option[(String, String, String, Boolean, String, String, Boolean)] = + def unapply(vf: ValidationFailure) + : Option[(String, String, String, Boolean, String, String, Boolean)] = vf match { case M.InconsistentlyNullableFieldMapping(om, fm, field, columnRef, colIsNullable) => - Some((om.tpe.name, fm.fieldName, SchemaRenderer.renderType(field.tpe), field.tpe.isNullable, columnRef.table, columnRef.column, colIsNullable)) + Some( + ( + om.tpe.name, + fm.fieldName, + SchemaRenderer.renderType(field.tpe), + field.tpe.isNullable, + columnRef.table, + columnRef.column, + colIsNullable)) case _ => None } } object IFLM { - def unapply(vf: ValidationFailure): Option[(String, String, String, String, String, String)] = + def unapply( + vf: ValidationFailure): Option[(String, String, String, String, String, String)] = vf match { case M.InconsistentFieldLeafMapping(om, fm, field, columnRef, _) => - Some((om.tpe.name, fm.fieldName, SchemaRenderer.renderType(field.tpe), columnRef.table, columnRef.column, columnRef.scalaTypeName)) + Some( + ( + om.tpe.name, + fm.fieldName, + SchemaRenderer.renderType(field.tpe), + columnRef.table, + columnRef.column, + columnRef.scalaTypeName)) case _ => None } } object IFTM { - def unapply(vf: ValidationFailure): Option[(String, String, String, String, String, String)] = + def unapply( + vf: ValidationFailure): Option[(String, String, String, String, String, String)] = vf match { case M.InconsistentFieldTypeMapping(om, fm, field, columnRef, _) => - Some((om.tpe.name, fm.fieldName, SchemaRenderer.renderType(field.tpe), columnRef.table, columnRef.column, columnRef.scalaTypeName)) + Some( + ( + om.tpe.name, + fm.fieldName, + SchemaRenderer.renderType(field.tpe), + columnRef.table, + columnRef.column, + columnRef.scalaTypeName)) case _ => None } } @@ -79,7 +106,8 @@ trait SqlMappingValidatorInvalidSuite extends CatsEffectSuite { } object SEM { - def unapply(vf: ValidationFailure): Option[(String, String, String, List[String], List[String])] = + def unapply( + vf: ValidationFailure): Option[(String, String, String, List[String], List[String])] = vf match { case M.SplitEmbeddedObjectTypeMapping(om, fm, com, parentTables, childTables) => Some((om.tpe.name, fm.fieldName, com.tpe.name, parentTables, childTables)) @@ -160,7 +188,8 @@ trait SqlMappingValidatorInvalidSuite extends CatsEffectSuite { } object MJ { - def unapply(vf: ValidationFailure): Option[(String, String, String, String, List[(String, String)])] = + def unapply(vf: ValidationFailure) + : Option[(String, String, String, String, List[(String, String)])] = vf match { case M.MisalignedJoins(om, fm, parent, child, path) => Some((om.tpe.name, fm.fieldName, parent, child, path)) @@ -189,41 +218,72 @@ trait SqlMappingValidatorInvalidSuite extends CatsEffectSuite { val es = M.validate() check(es.take(8)) { - case - List( - INFM("Scalars", "boolField1", "Boolean!", false, "scalars", "nullableBoolCol", true), - IFLM("Scalars", "boolField2", "Boolean!", "scalars", "intCol", IntTypeName), - INFM("Scalars", "nullableBoolField1", "Boolean", true, "scalars", "boolCol", false), - IFLM("Scalars", "nullableBoolField2", "Boolean", "scalars", "nullableIntCol", IntTypeName), - INFM("Scalars", "stringsField", "[String]!", false, "scalars", "nullableStringsCol", true), - INFM("Scalars", "nullableStringsField", "[String]", true, "scalars", "stringsCol", false), - IFTM("Scalars", "jsonbField", "Record", "scalars", "boolCol", BooleanTypeName), - IFTM("Scalars", "nullableJsonbField", "Record!", "scalars", "nullableBoolCol", BooleanTypeName), - ) => + case List( + INFM( + "Scalars", + "boolField1", + "Boolean!", + false, + "scalars", + "nullableBoolCol", + true), + IFLM("Scalars", "boolField2", "Boolean!", "scalars", "intCol", IntTypeName), + INFM("Scalars", "nullableBoolField1", "Boolean", true, "scalars", "boolCol", false), + IFLM( + "Scalars", + "nullableBoolField2", + "Boolean", + "scalars", + "nullableIntCol", + IntTypeName), + INFM( + "Scalars", + "stringsField", + "[String]!", + false, + "scalars", + "nullableStringsCol", + true), + INFM( + "Scalars", + "nullableStringsField", + "[String]", + true, + "scalars", + "stringsCol", + false), + IFTM("Scalars", "jsonbField", "Record", "scalars", "boolCol", BooleanTypeName), + IFTM( + "Scalars", + "nullableJsonbField", + "Record!", + "scalars", + "nullableBoolCol", + BooleanTypeName) + ) => } check(es.drop(8)) { - case - List( - NK("Scalars"), - ND("Intrf"), - NK("Intrf"), - SEM("Obj1", "embedded", "Obj3", List("obj1"), List("obj2")), - MJ("Obj1", "sub1", "obj1", "subObj1", List(("obj1", "join"), ("join", "obj1"))), - NK("Obj1"), - NJC("Obj3", "sub3"), - IJC("Obj2", "sub2", List("obj2"), List("subObj2", "subObj3")), - SIM("Obj2", List("Obj2", "Intrf"), List("obj2", "obj1")), - STM("Obj2", List("obj2", "obj1")), - ASNK("Obj2", "assoc"), - NK("Obj2"), - ISMU("Union", "bogus"), - NHUFM("Union", "id"), - SUM("Union", List("Obj1", "Obj2"), List("obj1", "obj2")), - ND("Union"), - NK("Union"), - UFM("Intrf", "id") - ) => + case List( + NK("Scalars"), + ND("Intrf"), + NK("Intrf"), + SEM("Obj1", "embedded", "Obj3", List("obj1"), List("obj2")), + MJ("Obj1", "sub1", "obj1", "subObj1", List(("obj1", "join"), ("join", "obj1"))), + NK("Obj1"), + NJC("Obj3", "sub3"), + IJC("Obj2", "sub2", List("obj2"), List("subObj2", "subObj3")), + SIM("Obj2", List("Obj2", "Intrf"), List("obj2", "obj1")), + STM("Obj2", List("obj2", "obj1")), + ASNK("Obj2", "assoc"), + NK("Obj2"), + ISMU("Union", "bogus"), + NHUFM("Union", "id"), + SUM("Union", List("Obj1", "Obj2"), List("obj1", "obj2")), + ND("Union"), + NK("Union"), + UFM("Intrf", "id") + ) => } } } diff --git a/modules/sql-core/src/test/scala/SqlMappingValidatorValidMapping.scala b/modules/sql-core/src/test/scala/SqlMappingValidatorValidMapping.scala index 0502bb83..bb27c3ff 100644 --- a/modules/sql-core/src/test/scala/SqlMappingValidatorValidMapping.scala +++ b/modules/sql-core/src/test/scala/SqlMappingValidatorValidMapping.scala @@ -211,112 +211,109 @@ trait SqlMappingValidatorValidMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("scalars"), - SqlObject("objs"), - SqlObject("union") - ) + fieldMappings = List( + SqlObject("scalars"), + SqlObject("objs"), + SqlObject("union") + ) ), ObjectMapping( tpe = ScalarsType, - fieldMappings = - List( - SqlField("id", scalars.idCol, key = true, hidden = true), - - SqlField("boolField", scalars.boolCol), - SqlField("nullableBoolField", scalars.nullableBoolCol), - - SqlField("textField", scalars.textCol), - SqlField("nullableTextField", scalars.nullableTextCol), - SqlField("varcharField", scalars.varcharCol), - SqlField("nullableVarcharField", scalars.nullableVarcharCol), - SqlField("bpcharField", scalars.bpcharCol), - SqlField("nullableBpcharField", scalars.nullableBpcharCol), - - SqlField("idField", scalars.textCol), - SqlField("nullableIdField", scalars.nullableTextCol), - - SqlField("int2Field", scalars.int2Col), - SqlField("nullableInt2Field", scalars.nullableInt2Col), - SqlField("int4Field", scalars.int4Col), - SqlField("nullableInt4Field", scalars.nullableInt4Col), - SqlField("int8Field", scalars.int8Col), - SqlField("nullableInt8Field", scalars.nullableInt8Col), - - SqlField("float4Field", scalars.float4Col), - SqlField("nullableFloat4Field", scalars.nullableFloat4Col), - SqlField("float8Field", scalars.float8Col), - SqlField("nullableFloat8Field", scalars.nullableFloat8Col), - - SqlField("numericField", scalars.numericCol), - SqlField("nullablenumericField", scalars.nullableNumericCol), - - SqlField("uuidField", scalars.uuidcol), - SqlField("nullableUuidField", scalars.nullableUuidCol), - - SqlField("genreField", scalars.genreCol), - SqlField("nullableGenreField", scalars.nullableGenreCol), - - SqlField("featureField", scalars.featureCol), - SqlField("nullableFeatureField", scalars.nullableFeatureCol), - - SqlField("featuresField", scalars.featuresCol), - SqlField("nullableFeatures1Field", scalars.nullableFeatures1Col), - SqlField("nullableFeatures2Field", scalars.nullableFeatures2Col), - SqlField("nullableFeatures3Field", scalars.nullableFeatures3Col), - - SqlJson("jsonbField", scalars.jsonbCol), - SqlJson("nullableJsonbField", scalars.nullableJsonbCol) - ) + fieldMappings = List( + SqlField("id", scalars.idCol, key = true, hidden = true), + + SqlField("boolField", scalars.boolCol), + SqlField("nullableBoolField", scalars.nullableBoolCol), + + SqlField("textField", scalars.textCol), + SqlField("nullableTextField", scalars.nullableTextCol), + SqlField("varcharField", scalars.varcharCol), + SqlField("nullableVarcharField", scalars.nullableVarcharCol), + SqlField("bpcharField", scalars.bpcharCol), + SqlField("nullableBpcharField", scalars.nullableBpcharCol), + + SqlField("idField", scalars.textCol), + SqlField("nullableIdField", scalars.nullableTextCol), + + SqlField("int2Field", scalars.int2Col), + SqlField("nullableInt2Field", scalars.nullableInt2Col), + SqlField("int4Field", scalars.int4Col), + SqlField("nullableInt4Field", scalars.nullableInt4Col), + SqlField("int8Field", scalars.int8Col), + SqlField("nullableInt8Field", scalars.nullableInt8Col), + + SqlField("float4Field", scalars.float4Col), + SqlField("nullableFloat4Field", scalars.nullableFloat4Col), + SqlField("float8Field", scalars.float8Col), + SqlField("nullableFloat8Field", scalars.nullableFloat8Col), + + SqlField("numericField", scalars.numericCol), + SqlField("nullablenumericField", scalars.nullableNumericCol), + + SqlField("uuidField", scalars.uuidcol), + SqlField("nullableUuidField", scalars.nullableUuidCol), + + SqlField("genreField", scalars.genreCol), + SqlField("nullableGenreField", scalars.nullableGenreCol), + + SqlField("featureField", scalars.featureCol), + SqlField("nullableFeatureField", scalars.nullableFeatureCol), + + SqlField("featuresField", scalars.featuresCol), + SqlField("nullableFeatures1Field", scalars.nullableFeatures1Col), + SqlField("nullableFeatures2Field", scalars.nullableFeatures2Col), + SqlField("nullableFeatures3Field", scalars.nullableFeatures3Col), + + SqlJson("jsonbField", scalars.jsonbCol), + SqlJson("nullableJsonbField", scalars.nullableJsonbCol) + ) ), SqlInterfaceMapping( tpe = IntrfType, discriminator = objectTypeDiscriminator, - fieldMappings = - List( - SqlField("id", objs.idCol, key = true), - SqlField("typeField", objs.typeCol, discriminator = true, hidden = true) - ) + fieldMappings = List( + SqlField("id", objs.idCol, key = true), + SqlField("typeField", objs.typeCol, discriminator = true, hidden = true) + ) ), ObjectMapping( tpe = Obj1Type, - fieldMappings = - List( - SqlField("intField", objs.intCol), - SqlObject("sub1", Join(objs.idCol, join.parentIdCol), Join(join.childIdCol, subObj1.idCol)) - ) + fieldMappings = List( + SqlField("intField", objs.intCol), + SqlObject( + "sub1", + Join(objs.idCol, join.parentIdCol), + Join(join.childIdCol, subObj1.idCol)) + ) ), ObjectMapping( tpe = Obj2Type, - fieldMappings = - List( - SqlField("boolField", objs.boolCol), - SqlObject("sub2", Join(List((objs.idCol, subObj2.idCol), (objs.boolCol, subObj2.parentBolCol)))) - ) + fieldMappings = List( + SqlField("boolField", objs.boolCol), + SqlObject( + "sub2", + Join(List((objs.idCol, subObj2.idCol), (objs.boolCol, subObj2.parentBolCol)))) + ) ), SqlUnionMapping( tpe = UnionType, discriminator = objectTypeDiscriminator, - fieldMappings = - List( - SqlField("id", objs.idCol, key = true, hidden = true), - SqlField("typeField", objs.typeCol, discriminator = true, hidden = true) - ) + fieldMappings = List( + SqlField("id", objs.idCol, key = true, hidden = true), + SqlField("typeField", objs.typeCol, discriminator = true, hidden = true) + ) ), ObjectMapping( tpe = SubObj1Type, - fieldMappings = - List( - SqlField("id", subObj1.idCol, key = true) - ) + fieldMappings = List( + SqlField("id", subObj1.idCol, key = true) + ) ), ObjectMapping( tpe = SubObj2Type, - fieldMappings = - List( - SqlField("id", subObj2.idCol, key = true) - ) + fieldMappings = List( + SqlField("id", subObj2.idCol, key = true) + ) ), LeafMapping[UUID](UUIDType), LeafMapping[Genre](GenreType), @@ -342,7 +339,7 @@ trait SqlMappingValidatorValidMapping[F[_]] extends SqlTestMapping[F] { def fromString(s: String): Option[Genre] = s.trim.toUpperCase match { - case "DRAMA" => Some(Drama) + case "DRAMA" => Some(Drama) case "ACTION" => Some(Action) case "COMEDY" => Some(Comedy) case _ => None @@ -364,7 +361,7 @@ trait SqlMappingValidatorValidMapping[F[_]] extends SqlTestMapping[F] { def toInt(f: Genre): Int = f match { - case Drama => 1 + case Drama => 1 case Action => 2 case Comedy => 3 } diff --git a/modules/sql-core/src/test/scala/SqlMappingValidatorValidSuite.scala b/modules/sql-core/src/test/scala/SqlMappingValidatorValidSuite.scala index e3230cb1..f645aa91 100644 --- a/modules/sql-core/src/test/scala/SqlMappingValidatorValidSuite.scala +++ b/modules/sql-core/src/test/scala/SqlMappingValidatorValidSuite.scala @@ -17,9 +17,10 @@ package grackle.sql.test import cats.effect.IO import cats.implicits._ -import grackle.sql._ import munit.CatsEffectSuite +import grackle.sql._ + trait SqlMappingValidatorValidSuite extends CatsEffectSuite { def mapping: SqlMappingLike[IO] @@ -30,7 +31,7 @@ trait SqlMappingValidatorValidSuite extends CatsEffectSuite { es match { case Nil => () - case _ => fail(es.foldMap(_.toErrorMessage)) + case _ => fail(es.foldMap(_.toErrorMessage)) } } } diff --git a/modules/sql-core/src/test/scala/SqlMixedMapping.scala b/modules/sql-core/src/test/scala/SqlMixedMapping.scala index 912da8e3..eb9b21fd 100644 --- a/modules/sql-core/src/test/scala/SqlMixedMapping.scala +++ b/modules/sql-core/src/test/scala/SqlMixedMapping.scala @@ -23,11 +23,11 @@ import cats.implicits._ import io.circe.literal._ import grackle._ -import syntax._ -import Query._ -import Predicate._ -import Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ // Mapping illustrating arbitrary mapping mixins with root query fields // defined by sql, value and circe mappings. @@ -73,31 +73,28 @@ trait SqlMixedMapping[F[_]] extends SqlTestMapping[F] with ValueMappingLike[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("movie"), - SqlObject("movies"), - ValueField[Unit]("foo", _ => Foo(23)), - CirceField("bar", json"""{ "message": "Hello world" }""") - ) + fieldMappings = List( + SqlObject("movie"), + SqlObject("movies"), + ValueField[Unit]("foo", _ => Foo(23)), + CirceField("bar", json"""{ "message": "Hello world" }""") + ) ), ObjectMapping( tpe = MovieType, - fieldMappings = - List( - SqlField("id", movies.id, key = true), - SqlField("title", movies.title), - CirceField("nested", json"""{ "message": "Hello world nested" }"""), - ValueField[Unit]("genre", _ => "comedy"), - CirceField("rating", json"""7.8""") - ) + fieldMappings = List( + SqlField("id", movies.id, key = true), + SqlField("title", movies.title), + CirceField("nested", json"""{ "message": "Hello world nested" }"""), + ValueField[Unit]("genre", _ => "comedy"), + CirceField("rating", json"""7.8""") + ) ), ValueObjectMapping[Foo]( tpe = FooType, - fieldMappings = - List( - ValueField("value", _.value) - ) + fieldMappings = List( + ValueField("value", _.value) + ) ), LeafMapping[UUID](UUIDType) ) diff --git a/modules/sql-core/src/test/scala/SqlMixedSuite.scala b/modules/sql-core/src/test/scala/SqlMixedSuite.scala index db7b97e9..1a7e22e9 100644 --- a/modules/sql-core/src/test/scala/SqlMixedSuite.scala +++ b/modules/sql-core/src/test/scala/SqlMixedSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlMixedSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlMovieMapping.scala b/modules/sql-core/src/test/scala/SqlMovieMapping.scala index 025baead..e3d4f893 100644 --- a/modules/sql-core/src/test/scala/SqlMovieMapping.scala +++ b/modules/sql-core/src/test/scala/SqlMovieMapping.scala @@ -15,7 +15,6 @@ package grackle.sql.test - import java.time.{Duration, LocalDate, LocalTime, OffsetDateTime} import java.util.UUID @@ -26,11 +25,11 @@ import cats.implicits._ import io.circe.{Decoder, Encoder} import grackle._ -import syntax._ -import Query._ -import Predicate._ -import Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => @@ -107,36 +106,34 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("movieById"), - SqlObject("moviesByGenre"), - SqlObject("moviesByGenres"), - SqlObject("moviesReleasedBetween"), - SqlObject("moviesLongerThan"), - SqlObject("moviesShownLaterThan"), - SqlObject("moviesShownBetween"), - SqlObject("longMovies"), - SqlObject("allMovies") - ) + fieldMappings = List( + SqlObject("movieById"), + SqlObject("moviesByGenre"), + SqlObject("moviesByGenres"), + SqlObject("moviesReleasedBetween"), + SqlObject("moviesLongerThan"), + SqlObject("moviesShownLaterThan"), + SqlObject("moviesShownBetween"), + SqlObject("longMovies"), + SqlObject("allMovies") + ) ), ObjectMapping( tpe = MovieType, - fieldMappings = - List( - SqlField("id", movies.id, key = true), - SqlField("title", movies.title), - SqlField("genre", movies.genre), - SqlField("releaseDate", movies.releaseDate), - SqlField("showTime", movies.showTime), - SqlField("nextShowing", movies.nextShowing), - CursorField("nextEnding", nextEnding, List("nextShowing", "duration")), - SqlField("duration", movies.duration), - SqlField("categories", movies.categories), - SqlField("features", movies.features), - CursorField("isLong", isLong, List("duration"), hidden = true), - SqlField("tags", movies.tags) - ) + fieldMappings = List( + SqlField("id", movies.id, key = true), + SqlField("title", movies.title), + SqlField("genre", movies.genre), + SqlField("releaseDate", movies.releaseDate), + SqlField("showTime", movies.showTime), + SqlField("nextShowing", movies.nextShowing), + CursorField("nextEnding", nextEnding, List("nextShowing", "duration")), + SqlField("duration", movies.duration), + SqlField("categories", movies.categories), + SqlField("features", movies.features), + CursorField("isLong", isLong, List("duration"), hidden = true), + SqlField("tags", movies.tags) + ) ), LeafMapping[UUID](UUIDType), LeafMapping[LocalTime](TimeType), @@ -150,12 +147,12 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => def nextEnding(c: Cursor): Result[OffsetDateTime] = for { nextShowing <- c.fieldAs[OffsetDateTime]("nextShowing") - duration <- c.fieldAs[Duration]("duration") + duration <- c.fieldAs[Duration]("duration") } yield nextShowing.plus(duration) def isLong(c: Cursor): Result[Boolean] = for { - duration <- c.fieldAs[Duration]("duration") + duration <- c.fieldAs[Duration]("duration") } yield duration.toHours >= 3 object UUIDValue { @@ -203,7 +200,10 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => Elab.transformChild(child => Filter(Eql(MovieType / "genre", Const(genre)), child)) case (QueryType, "moviesByGenres", List(Binding("genres", GenreListValue(genres)))) => Elab.transformChild(child => Filter(In(MovieType / "genre", genres), child)) - case (QueryType, "moviesReleasedBetween", List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => + case ( + QueryType, + "moviesReleasedBetween", + List(Binding("from", DateValue(from)), Binding("to", DateValue(to)))) => Elab.transformChild(child => Filter( And( @@ -211,23 +211,23 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => Lt(MovieType / "releaseDate", Const(to)) ), child - ) - ) + )) case (QueryType, "moviesLongerThan", List(Binding("duration", IntervalValue(duration)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "duration", Const(duration))), child - ) - ) + )) case (QueryType, "moviesShownLaterThan", List(Binding("time", TimeValue(time)))) => Elab.transformChild(child => Filter( Not(Lt(MovieType / "showTime", Const(time))), child - ) - ) - case (QueryType, "moviesShownBetween", List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => + )) + case ( + QueryType, + "moviesShownBetween", + List(Binding("from", DateTimeValue(from)), Binding("to", DateTimeValue(to)))) => Elab.transformChild(child => Filter( And( @@ -235,8 +235,7 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => Lt(MovieType / "nextShowing", Const(to)) ), child - ) - ) + )) case (QueryType, "longMovies", Nil) => Elab.transformChild(child => Filter(Eql(MovieType / "isLong", Const(true)), child)) } @@ -251,7 +250,7 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => def fromString(s: String): Option[Genre] = s.trim.toUpperCase match { - case "DRAMA" => Some(Drama) + case "DRAMA" => Some(Drama) case "ACTION" => Some(Action) case "COMEDY" => Some(Comedy) case _ => None @@ -273,7 +272,7 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => def toInt(f: Genre): Int = f match { - case Drama => 1 + case Drama => 1 case Action => 2 case Comedy => 3 } @@ -299,16 +298,15 @@ trait SqlMovieMapping[F[_]] extends SqlTestMapping[F] { self => object Tags { val tags = List("tag1", "tag2", "tag3") - def fromInt(i: Int): List[String] = { def getTag(m: Int): List[String] = - if((i&(1 << m)) != 0) List(tags(m)) else Nil + if ((i & (1 << m)) != 0) List(tags(m)) else Nil (0 to 2).flatMap(getTag).toList } def toInt(tags: List[String]): Int = { def getBit(m: Int): Int = - if(tags.contains(tags(m))) 1 << m else 0 + if (tags.contains(tags(m))) 1 << m else 0 (0 to 2).foldLeft(0)((acc, m) => acc | getBit(m)) } } diff --git a/modules/sql-core/src/test/scala/SqlMovieSuite.scala b/modules/sql-core/src/test/scala/SqlMovieSuite.scala index 5334a33f..ac3899ee 100644 --- a/modules/sql-core/src/test/scala/SqlMovieSuite.scala +++ b/modules/sql-core/src/test/scala/SqlMovieSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlMovieSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlMutationMapping.scala b/modules/sql-core/src/test/scala/SqlMutationMapping.scala index a8ba4bde..19117d01 100644 --- a/modules/sql-core/src/test/scala/SqlMutationMapping.scala +++ b/modules/sql-core/src/test/scala/SqlMutationMapping.scala @@ -17,12 +17,11 @@ package grackle.sql.test import cats.syntax.all._ -import grackle._ -import syntax._ -import Predicate._ -import Query._ -import QueryCompiler._ -import Value._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.syntax._ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { object country extends TableDef("country") { @@ -31,10 +30,10 @@ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { } object city extends TableDef("city_copy") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(2)) - val name = col("name", text) - val population = col("population", int4) + val name = col("name", text) + val population = col("population", int4) } val schema = @@ -64,10 +63,10 @@ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { def updatePopulation(id: Int, population: Int): F[Unit] def createCity(name: String, countryCode: String, population: Int): F[Int] - val QueryType = schema.ref("Query") + val QueryType = schema.ref("Query") val MutationType = schema.ref("Mutation") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") case class UpdatePopulation(id: Int, population: Int) case class CreateCity(name: String, countryCode: String, population: Int) @@ -77,8 +76,8 @@ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { ObjectMapping( tpe = QueryType, fieldMappings = List( - SqlObject("city"), - ), + SqlObject("city") + ) ), ObjectMapping( tpe = MutationType, @@ -86,25 +85,23 @@ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { RootEffect.computeUnit("updatePopulation")(env => env.getR[UpdatePopulation]("updatePopulation").traverse { case UpdatePopulation(id, pop) => updatePopulation(id, pop) - } - ), + }), RootEffect.computeChild("createCity")((child, _, env) => env.getR[CreateCity]("createCity").flatTraverse { case CreateCity(name, cc, pop) => createCity(name, cc, pop).map { id => Unique(Filter(Eql(CityType / "id", Const(id)), child)).success } - } - ) + }) ) ), ObjectMapping( tpe = CountryType, fieldMappings = List( SqlField("code", country.code, key = true, hidden = true), - SqlField("name", country.name), - SqlObject("cities", Join(country.code, city.countrycode)), - ), + SqlField("name", country.name), + SqlObject("cities", Join(country.code, city.countrycode)) + ) ), ObjectMapping( tpe = CityType, @@ -113,22 +110,32 @@ trait SqlMutationMapping[F[_]] extends SqlTestMapping[F] { SqlField("countrycode", city.countrycode, hidden = true), SqlField("name", city.name), SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlObject("country", Join(city.countrycode, country.code)) ) - ), + ) ) override val selectElaborator = SelectElaborator { case (QueryType, "city", List(Binding("id", IntValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(CityType / "id", Const(id)), child))) - case (MutationType, "updatePopulation", List(Binding("id", IntValue(id)), Binding("population", IntValue(pop)))) => + case ( + MutationType, + "updatePopulation", + List(Binding("id", IntValue(id)), Binding("population", IntValue(pop)))) => for { _ <- Elab.env("updatePopulation", UpdatePopulation(id, pop)) - _ <- Elab.transformChild(child => Unique(Filter(Eql(CityType / "id", Const(id)), child))) + _ <- Elab.transformChild(child => + Unique(Filter(Eql(CityType / "id", Const(id)), child))) } yield () - case (MutationType, "createCity", List(Binding("name", StringValue(name)), Binding("countryCode", StringValue(code)), Binding("population", IntValue(pop)))) => + case ( + MutationType, + "createCity", + List( + Binding("name", StringValue(name)), + Binding("countryCode", StringValue(code)), + Binding("population", IntValue(pop)))) => Elab.env("createCity", CreateCity(name, code, pop)) } } diff --git a/modules/sql-core/src/test/scala/SqlMutationSuite.scala b/modules/sql-core/src/test/scala/SqlMutationSuite.scala index 974a7ff7..ef295aff 100644 --- a/modules/sql-core/src/test/scala/SqlMutationSuite.scala +++ b/modules/sql-core/src/test/scala/SqlMutationSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlMutationSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlNestedEffectsMapping.scala b/modules/sql-core/src/test/scala/SqlNestedEffectsMapping.scala index ebfe16c5..95de34e6 100644 --- a/modules/sql-core/src/test/scala/SqlNestedEffectsMapping.scala +++ b/modules/sql-core/src/test/scala/SqlNestedEffectsMapping.scala @@ -18,25 +18,27 @@ package grackle.sql.test import cats.effect.{Ref, Sync} import cats.implicits._ import io.circe.{Encoder, Json} -import io.circe.syntax._ import io.circe.generic.semiauto.deriveEncoder +import io.circe.syntax._ import grackle._ -import sql.Like -import syntax._ -import Query._ -import Predicate._, Value._ -import QueryCompiler._ - -class CurrencyService[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[F, Int]) { +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.sql.Like +import grackle.syntax._ + +class CurrencyService[F[_]: Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[F, Int]) { implicit val currencyEncoder: Encoder[Currency] = deriveEncoder def get(countryCodes: List[String]): F[Json] = for { - _ <- countRef.update(_+1) + _ <- countRef.update(_ + 1) data <- dataRef.get } yield { - val currencies = data.currencies.values.filter(cur => countryCodes.contains(cur.countryCode)) + val currencies = + data.currencies.values.filter(cur => countryCodes.contains(cur.countryCode)) Json.fromValues(currencies.map(_.asJson)) } @@ -47,7 +49,7 @@ class CurrencyService[F[_] : Sync](dataRef: Ref[F, CurrencyData], countRef: Ref[ } object CurrencyService { - def apply[F[_] : Sync]: F[CurrencyService[F]] = { + def apply[F[_]: Sync]: F[CurrencyService[F]] = { val BRL = Currency("BRL", 0.25, "BR") val EUR = Currency("EUR", 1.12, "NL") val GBP = Currency("GBP", 1.25, "GB") @@ -55,7 +57,7 @@ object CurrencyService { val data = CurrencyData(List(BRL, EUR, GBP).map(c => (c.code, c)).toMap) for { - dataRef <- Ref[F].of(data) + dataRef <- Ref[F].of(data) countRef <- Ref[F].of(0) } yield new CurrencyService[F](dataRef, countRef) } @@ -65,30 +67,30 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { def currencyService: CurrencyService[F] object country extends TableDef("country") { - val code = col("code", bpchar(3)) - val name = col("name", text) - val continent = col("continent", text) - val region = col("region", text) - val surfacearea = col("surfacearea", float4) - val indepyear = col("indepyear", nullable(int2)) - val population = col("population", int4) + val code = col("code", bpchar(3)) + val name = col("name", text) + val continent = col("continent", text) + val region = col("region", text) + val surfacearea = col("surfacearea", float4) + val indepyear = col("indepyear", nullable(int2)) + val population = col("population", int4) val lifeexpectancy = col("lifeexpectancy", nullable(float4)) - val gnp = col("gnp", nullable(numeric(10, 2))) - val gnpold = col("gnpold", nullable(numeric(10, 2))) - val localname = col("localname", text) + val gnp = col("gnp", nullable(numeric(10, 2))) + val gnpold = col("gnpold", nullable(numeric(10, 2))) + val localname = col("localname", text) val governmentform = col("governmentform", text) - val headofstate = col("headofstate", nullable(text)) - val capitalId = col("capital", nullable(int4)) - val numCities = col("num_cities", int8) - val code2 = col("code2", bpchar(2)) + val headofstate = col("headofstate", nullable(text)) + val capitalId = col("capital", nullable(int4)) + val numCities = col("num_cities", int8) + val code2 = col("code2", bpchar(2)) } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", text) - val district = col("district", text) - val population = col("population", int4) + val name = col("name", text) + val district = col("district", text) + val population = col("population", int4) } object countrylanguage extends TableDef("countrylanguage") { @@ -143,9 +145,9 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { } """ - val QueryType = schema.ref("Query") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val QueryType = schema.ref("Query") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") val LanguageType = schema.ref("Language") val CurrencyType = schema.ref("Currency") @@ -161,51 +163,51 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { ObjectMapping( tpe = CountryType, fieldMappings = List( - SqlField("code", country.code, key = true, hidden = true), - SqlField("name", country.name), - SqlField("continent", country.continent), - SqlField("region", country.region), - SqlField("surfacearea", country.surfacearea), - SqlField("indepyear", country.indepyear), - SqlField("population", country.population), - SqlField("lifeexpectancy", country.lifeexpectancy), - SqlField("gnp", country.gnp), - SqlField("gnpold", country.gnpold), - SqlField("localname", country.localname), - SqlField("governmentform", country.governmentform), - SqlField("headofstate", country.headofstate), - SqlField("capitalId", country.capitalId), - SqlField("code2", country.code2), - SqlObject("cities", Join(country.code, city.countrycode)), - SqlObject("languages", Join(country.code, countrylanguage.countrycode)), - EffectField("currencies", CurrencyQueryHandler, List("code2")) - ), + SqlField("code", country.code, key = true, hidden = true), + SqlField("name", country.name), + SqlField("continent", country.continent), + SqlField("region", country.region), + SqlField("surfacearea", country.surfacearea), + SqlField("indepyear", country.indepyear), + SqlField("population", country.population), + SqlField("lifeexpectancy", country.lifeexpectancy), + SqlField("gnp", country.gnp), + SqlField("gnpold", country.gnpold), + SqlField("localname", country.localname), + SqlField("governmentform", country.governmentform), + SqlField("headofstate", country.headofstate), + SqlField("capitalId", country.capitalId), + SqlField("code2", country.code2), + SqlObject("cities", Join(country.code, city.countrycode)), + SqlObject("languages", Join(country.code, countrylanguage.countrycode)), + EffectField("currencies", CurrencyQueryHandler, List("code2")) + ) ), ObjectMapping( tpe = CityType, fieldMappings = List( - SqlField("id", city.id, key = true, hidden = true), - SqlField("countrycode", city.countrycode, hidden = true), - SqlField("name", city.name), - SqlField("district", city.district), - SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlField("id", city.id, key = true, hidden = true), + SqlField("countrycode", city.countrycode, hidden = true), + SqlField("name", city.name), + SqlField("district", city.district), + SqlField("population", city.population), + SqlObject("country", Join(city.countrycode, country.code)) ) ), ObjectMapping( tpe = LanguageType, fieldMappings = List( - SqlField("language", countrylanguage.language, key = true, associative = true), - SqlField("isOfficial", countrylanguage.isOfficial), - SqlField("percentage", countrylanguage.percentage), - SqlField("countrycode", countrylanguage.countrycode, hidden = true), - SqlObject("countries", Join(countrylanguage.countrycode, country.code)) + SqlField("language", countrylanguage.language, key = true, associative = true), + SqlField("isOfficial", countrylanguage.isOfficial), + SqlField("percentage", countrylanguage.percentage), + SqlField("countrycode", countrylanguage.countrycode, hidden = true), + SqlObject("countries", Join(countrylanguage.countrycode, country.code)) ) ), ObjectMapping( tpe = CurrencyType, fieldMappings = List( - EffectField("country", CountryQueryHandler) + EffectField("country", CountryQueryHandler) ) ) ) @@ -223,22 +225,21 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { def unpackResults(res: Json): List[Json] = (for { arr <- res.asArray - } yield - countryCodes.map { - case Some(countryCode) => - Json.fromValues(arr.find { elem => - (for { - obj <- elem.asObject - fld <- obj("countryCode") - code <- fld.asString - } yield countryCode == code).getOrElse(false) - }) - case _ => Json.Null + } yield countryCodes.map { + case Some(countryCode) => + Json.fromValues(arr.find { elem => + (for { + obj <- elem.asObject + fld <- obj("countryCode") + code <- fld.asString + } yield countryCode == code).getOrElse(false) + }) + case _ => Json.Null }).getOrElse(Nil) (for { children <- ResultT(children0.pure[F]) - res <- ResultT(currencyService.get(distinctCodes).map(_.success)) + res <- ResultT(currencyService.get(distinctCodes).map(_.success)) } yield { unpackResults(res).zip(children).map { case (res, (childContext, parentCursor)) => @@ -253,9 +254,12 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { val toCode = Map("BR" -> "BRA", "GB" -> "GBR", "NL" -> "NLD") def runEffects(queries: List[(Query, Cursor)]): F[Result[List[Cursor]]] = { - def mkListCursor(cursor: Cursor, fieldName: String, resultName: Option[String]): Result[Cursor] = + def mkListCursor( + cursor: Cursor, + fieldName: String, + resultName: Option[String]): Result[Cursor] = for { - c <- cursor.field(fieldName, resultName) + c <- cursor.field(fieldName, resultName) lc <- c.preunique } yield lc @@ -265,7 +269,7 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { def partitionCursor(codes: List[String], cursor: Cursor): Result[List[Cursor]] = { for { cursors <- cursor.asList - tagged <- cursors.traverse(c => (extractCode(c).map { code => (code, c) })) + tagged <- cursors.traverse(c => extractCode(c).map { code => (code, c) }) } yield { val m = tagged.toMap codes.map(code => m(code)) @@ -274,13 +278,16 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { runGrouped(queries) { case (Select(_, _, child), cursors, indices) => - val codes = cursors.flatMap(_.fieldAs[Json]("countryCode").toOption.flatMap(_.asString).toList).map(toCode) - val combinedQuery = Select("country", None, Filter(In(CountryType / "code", codes), child)) + val codes = cursors + .flatMap(_.fieldAs[Json]("countryCode").toOption.flatMap(_.asString).toList) + .map(toCode) + val combinedQuery = + Select("country", None, Filter(In(CountryType / "code", codes), child)) (for { - cursor <- ResultT(sqlCursor(combinedQuery, Env.empty)) + cursor <- ResultT(sqlCursor(combinedQuery, Env.empty)) cursor0 <- ResultT(mkListCursor(cursor, "country", None).pure[F]) - pcs <- ResultT(partitionCursor(codes, cursor0).pure[F]) + pcs <- ResultT(partitionCursor(codes, cursor0).pure[F]) } yield { pcs.zip(indices) }).value.widen @@ -289,12 +296,15 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { } } - def runGrouped(ts: List[(Query, Cursor)])(op: (Query, List[Cursor], List[Int]) => F[Result[List[(Cursor, Int)]]]): F[Result[List[Cursor]]] = { + def runGrouped(ts: List[(Query, Cursor)])( + op: (Query, List[Cursor], List[Int]) => F[Result[List[(Cursor, Int)]]]) + : F[Result[List[Cursor]]] = { val groupedAndIndexed = ts.zipWithIndex.groupMap(_._1._1)(ti => (ti._1._2, ti._2)).toList val groupedResults = - groupedAndIndexed.map { case (q, cis) => - val (cursors, indices) = cis.unzip - op(q, cursors, indices) + groupedAndIndexed.map { + case (q, cis) => + val (cursors, indices) = cis.unzip + op(q, cursors, indices) } groupedResults.sequence.map(_.sequence.map(_.flatten.sortBy(_._2).map(_._1))) @@ -309,6 +319,7 @@ trait SqlNestedEffectsMapping[F[_]] extends SqlTestMapping[F] { Elab.transformChild(child => Filter(Like(CityType / "name", namePattern, true), child)) case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CountryType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CountryType / "code", Const(code)), child))) } } diff --git a/modules/sql-core/src/test/scala/SqlNestedEffectsSuite.scala b/modules/sql-core/src/test/scala/SqlNestedEffectsSuite.scala index f0e91617..ab4eab88 100644 --- a/modules/sql-core/src/test/scala/SqlNestedEffectsSuite.scala +++ b/modules/sql-core/src/test/scala/SqlNestedEffectsSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.{assertWeaklyEqual, assertWeaklyEqualIO} trait SqlNestedEffectsSuite extends CatsEffectSuite { @@ -203,17 +202,18 @@ trait SqlNestedEffectsSuite extends CatsEffectSuite { """ val prg = - (for { - cm <- mapping - m = cm._2 - n0 <- cm._1.count + for { + cm <- mapping + m = cm._2 + n0 <- cm._1.count res <- m.compileAndRun(query) - n1 <- cm._1.count - } yield (res, n0, n1)) + n1 <- cm._1.count + } yield (res, n0, n1) - prg.map { case (res, n0, n1) => - assert(n0 == 0 && n1 == 1) - assertWeaklyEqual(res, expected) + prg.map { + case (res, n0, n1) => + assert(n0 == 0 && n1 == 1) + assertWeaklyEqual(res, expected) } } @@ -300,23 +300,24 @@ trait SqlNestedEffectsSuite extends CatsEffectSuite { """ val prg = - (for { - cm <- mapping - c = cm._1 - m = cm._2 - n0 <- cm._1.count + for { + cm <- mapping + c = cm._1 + m = cm._2 + n0 <- cm._1.count res0 <- m.compileAndRun(query) - n1 <- cm._1.count - _ <- c.update("EUR", 1.13) + n1 <- cm._1.count + _ <- c.update("EUR", 1.13) res1 <- m.compileAndRun(query) - n2 <- cm._1.count - } yield (res0, res1, n0, n1, n2)) + n2 <- cm._1.count + } yield (res0, res1, n0, n1, n2) - prg.map { case (res0, res1, n0, n1, n2) => - assert(n0 == 0 && n1 == 1 && n2 == 2) + prg.map { + case (res0, res1, n0, n1, n2) => + assert(n0 == 0 && n1 == 1 && n2 == 2) - assertWeaklyEqual(res0, expected0) - assertWeaklyEqual(res1, expected1) + assertWeaklyEqual(res0, expected0) + assertWeaklyEqual(res1, expected1) } } diff --git a/modules/sql-core/src/test/scala/SqlPaging1Mapping.scala b/modules/sql-core/src/test/scala/SqlPaging1Mapping.scala index 6bf0bbf1..ea3a83c6 100644 --- a/modules/sql-core/src/test/scala/SqlPaging1Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlPaging1Mapping.scala @@ -17,10 +17,11 @@ package grackle.sql.test import cats.implicits._ -import grackle._, syntax._ -import Query.{Binding, Count, FilterOrderByOffsetLimit, OrderSelection, Select} -import QueryCompiler.{Elab, SelectElaborator} -import Value.IntValue +import grackle._ +import grackle.Query.{Binding, Count, FilterOrderByOffsetLimit, OrderSelection, Select} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value.IntValue +import grackle.syntax._ // Mapping illustrating paging in "counted" style: paged results can // report the current offet, limit and total number of items in the @@ -34,15 +35,15 @@ trait SqlPaging1Mapping[F[_]] extends SqlTestMapping[F] { } object country extends TableDef("country") { - val code = col("code", bpchar(3)) - val name = col("name", nvarchar) - val numCities = col("num_cities", int8) + val code = col("code", bpchar(3)) + val name = col("name", nvarchar) + val numCities = col("num_cities", int8) } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", nvarchar) + val name = col("name", nvarchar) } val schema = @@ -82,49 +83,44 @@ trait SqlPaging1Mapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("countries"), - ) + fieldMappings = List( + SqlObject("countries") + ) ), ObjectMapping( tpe = PagedCountryType, - fieldMappings = - List( - CursorField("offset", CountryPaging.genOffset, Nil), - CursorField("limit", CountryPaging.genLimit, Nil), - SqlField("total", root.numCountries), - SqlObject("items") - ) + fieldMappings = List( + CursorField("offset", CountryPaging.genOffset, Nil), + CursorField("limit", CountryPaging.genLimit, Nil), + SqlField("total", root.numCountries), + SqlObject("items") + ) ), ObjectMapping( tpe = CountryType, - fieldMappings = - List( - SqlField("code", country.code, key = true), - SqlField("name", country.name), - SqlObject("cities") - ) + fieldMappings = List( + SqlField("code", country.code, key = true), + SqlField("name", country.name), + SqlObject("cities") + ) ), ObjectMapping( tpe = PagedCityType, - fieldMappings = - List( - SqlField("code", country.code, key = true, hidden = true), - SqlObject("items", Join(country.code, city.countrycode)), - CursorField("offset", CityPaging.genOffset, Nil), - CursorField("limit", CityPaging.genLimit, Nil), - SqlField("total", country.numCities), - ) + fieldMappings = List( + SqlField("code", country.code, key = true, hidden = true), + SqlObject("items", Join(country.code, city.countrycode)), + CursorField("offset", CityPaging.genOffset, Nil), + CursorField("limit", CityPaging.genLimit, Nil), + SqlField("total", country.numCities) + ) ), ObjectMapping( tpe = CityType, - fieldMappings = - List( - SqlField("id", city.id, key = true, hidden = true), - SqlField("countrycode", city.countrycode, hidden = true), - SqlField("name", city.name) - ) + fieldMappings = List( + SqlField("id", city.id, key = true, hidden = true), + SqlField("countrycode", city.countrycode, hidden = true), + SqlField("name", city.name) + ) ) ) @@ -147,7 +143,12 @@ trait SqlPaging1Mapping[F[_]] extends SqlTestMapping[F] { case class PagingInfo(offset: Int, limit: Int) { def elabItems: Elab[Unit] = Elab.transformChild { child => - FilterOrderByOffsetLimit(None, Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), Some(offset), Some(limit), child) + FilterOrderByOffsetLimit( + None, + Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), + Some(offset), + Some(limit), + child) } def elabTotal: Elab[Unit] = @@ -159,7 +160,10 @@ trait SqlPaging1Mapping[F[_]] extends SqlTestMapping[F] { object CityPaging extends PagingConfig("cityPaging", "id", CityType / "name") override val selectElaborator = SelectElaborator { - case (QueryType, "countries", List(Binding("offset", IntValue(off)), Binding("limit", IntValue(lim)))) => + case ( + QueryType, + "countries", + List(Binding("offset", IntValue(off)), Binding("limit", IntValue(lim)))) => CountryPaging.setup(off, lim) case (PagedCountryType, "items", Nil) => @@ -168,7 +172,10 @@ trait SqlPaging1Mapping[F[_]] extends SqlTestMapping[F] { case (PagedCountryType, "total", Nil) => CountryPaging.elabTotal - case (CountryType, "cities", List(Binding("offset", IntValue(off)), Binding("limit", IntValue(lim)))) => + case ( + CountryType, + "cities", + List(Binding("offset", IntValue(off)), Binding("limit", IntValue(lim)))) => CityPaging.setup(off, lim) case (PagedCityType, "items", Nil) => diff --git a/modules/sql-core/src/test/scala/SqlPaging1Suite.scala b/modules/sql-core/src/test/scala/SqlPaging1Suite.scala index 6b53ed99..3ec4697a 100644 --- a/modules/sql-core/src/test/scala/SqlPaging1Suite.scala +++ b/modules/sql-core/src/test/scala/SqlPaging1Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlPaging1Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlPaging2Mapping.scala b/modules/sql-core/src/test/scala/SqlPaging2Mapping.scala index 5186a160..2a7f1fcb 100644 --- a/modules/sql-core/src/test/scala/SqlPaging2Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlPaging2Mapping.scala @@ -17,10 +17,11 @@ package grackle.sql.test import cats.implicits._ -import grackle._, syntax._ -import Query.{Binding, Count, FilterOrderByOffsetLimit, OrderSelection, Select} -import QueryCompiler.{Elab, SelectElaborator} -import Value._ +import grackle._ +import grackle.Query.{Binding, Count, FilterOrderByOffsetLimit, OrderSelection, Select} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value._ +import grackle.syntax._ // Mapping illustrating paging in "has more" style: paged results can // report whether there are more elements beyond the current sub list. @@ -33,15 +34,15 @@ trait SqlPaging2Mapping[F[_]] extends SqlTestMapping[F] { } object country extends TableDef("country") { - val code = col("code", bpchar(3)) - val name = col("name", nvarchar) + val code = col("code", bpchar(3)) + val name = col("name", nvarchar) val numCities = col("num_cities", int8) } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", nvarchar) + val name = col("name", nvarchar) } val schema = @@ -77,67 +78,71 @@ trait SqlPaging2Mapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("countries"), - ) + fieldMappings = List( + SqlObject("countries") + ) ), ObjectMapping( tpe = PagedCountryType, - fieldMappings = - List( - SqlObject("items"), - CursorField("hasMore", CountryPaging.genHasMore, List("numCountries")), - SqlField("numCountries", root.numCountries, hidden = true) - ) + fieldMappings = List( + SqlObject("items"), + CursorField("hasMore", CountryPaging.genHasMore, List("numCountries")), + SqlField("numCountries", root.numCountries, hidden = true) + ) ), ObjectMapping( tpe = CountryType, - fieldMappings = - List( - SqlField("code", country.code, key = true), - SqlField("name", country.name), - SqlObject("cities") - ), + fieldMappings = List( + SqlField("code", country.code, key = true), + SqlField("name", country.name), + SqlObject("cities") + ) ), ObjectMapping( tpe = PagedCityType, - fieldMappings = - List( - SqlField("code", country.code, key = true, hidden = true), - SqlObject("items", Join(country.code, city.countrycode)), - CursorField("hasMore", CityPaging.genHasMore, List("numCities")), - SqlField("numCities", country.numCities, hidden = true) - ) + fieldMappings = List( + SqlField("code", country.code, key = true, hidden = true), + SqlObject("items", Join(country.code, city.countrycode)), + CursorField("hasMore", CityPaging.genHasMore, List("numCities")), + SqlField("numCities", country.numCities, hidden = true) + ) ), ObjectMapping( tpe = CityType, - fieldMappings = - List( - SqlField("id", city.id, key = true, hidden = true), - SqlField("countrycode", city.countrycode, hidden = true), - SqlField("name", city.name) - ) + fieldMappings = List( + SqlField("id", city.id, key = true, hidden = true), + SqlField("countrycode", city.countrycode, hidden = true), + SqlField("name", city.name) + ) ) ) - abstract class PagingConfig(key: String, countField: String, countAttr: String, orderTerm: Term[String]) { + abstract class PagingConfig( + key: String, + countField: String, + countAttr: String, + orderTerm: Term[String]) { def setup(offset: Option[Int], limit: Option[Int]): Elab[Unit] = Elab.env(key -> new PagingInfo(offset, limit)) def elabItems = Elab.envE[PagingInfo](key).flatMap(_.elabItems) def elabHasMore = Elab.envE[PagingInfo](key).flatMap(_.elabHasMore) - def genHasMore(c : Cursor): Result[Boolean] = + def genHasMore(c: Cursor): Result[Boolean] = for { info <- c.envR[PagingInfo](key) - c0 <- info.genHasMore(c) + c0 <- info.genHasMore(c) } yield c0 case class PagingInfo(offset: Option[Int], limit: Option[Int]) { def elabItems: Elab[Unit] = Elab.transformChild { child => - FilterOrderByOffsetLimit(None, Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), offset, limit, child) + FilterOrderByOffsetLimit( + None, + Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), + offset, + limit, + child) } def elabHasMore: Elab[Unit] = @@ -146,11 +151,12 @@ trait SqlPaging2Mapping[F[_]] extends SqlTestMapping[F] { def genHasMore(c: Cursor): Result[Boolean] = for { num <- c.fieldAs[Long](countField) - } yield num > offset.getOrElse(0)+limit.getOrElse(num.toInt) + } yield num > offset.getOrElse(0) + limit.getOrElse(num.toInt) } } - object CountryPaging extends PagingConfig("countryPaging", "numCountries", "code", CountryType / "code") + object CountryPaging + extends PagingConfig("countryPaging", "numCountries", "code", CountryType / "code") object CityPaging extends PagingConfig("cityPaging", "numCities", "name", CityType / "name") object OptIntValue { @@ -162,7 +168,10 @@ trait SqlPaging2Mapping[F[_]] extends SqlTestMapping[F] { } override val selectElaborator = SelectElaborator { - case (QueryType, "countries", List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => + case ( + QueryType, + "countries", + List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => CountryPaging.setup(off, lim) case (PagedCountryType, "hasMore", Nil) => @@ -171,7 +180,10 @@ trait SqlPaging2Mapping[F[_]] extends SqlTestMapping[F] { case (PagedCountryType, "items", Nil) => CountryPaging.elabItems - case (CountryType, "cities", List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => + case ( + CountryType, + "cities", + List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => CityPaging.setup(off, lim) case (PagedCityType, "hasMore", Nil) => diff --git a/modules/sql-core/src/test/scala/SqlPaging2Suite.scala b/modules/sql-core/src/test/scala/SqlPaging2Suite.scala index b0e440f2..ff05d55e 100644 --- a/modules/sql-core/src/test/scala/SqlPaging2Suite.scala +++ b/modules/sql-core/src/test/scala/SqlPaging2Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlPaging2Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlPaging3Mapping.scala b/modules/sql-core/src/test/scala/SqlPaging3Mapping.scala index 2a75dfe4..c8f9b3cc 100644 --- a/modules/sql-core/src/test/scala/SqlPaging3Mapping.scala +++ b/modules/sql-core/src/test/scala/SqlPaging3Mapping.scala @@ -17,11 +17,19 @@ package grackle.sql.test import cats.implicits._ -import grackle._, syntax._ -import Cursor.ListTransformCursor -import Query.{Binding, Count, FilterOrderByOffsetLimit, OrderSelection, Select, TransformCursor} -import QueryCompiler.{Elab, SelectElaborator} -import Value._ +import grackle._ +import grackle.Cursor.ListTransformCursor +import grackle.Query.{ + Binding, + Count, + FilterOrderByOffsetLimit, + OrderSelection, + Select, + TransformCursor +} +import grackle.QueryCompiler.{Elab, SelectElaborator} +import grackle.Value._ +import grackle.syntax._ // Mapping illustrating paging in "has more" style: paged results can report // whether there are more elements beyond the current sub list. @@ -41,15 +49,15 @@ trait SqlPaging3Mapping[F[_]] extends SqlTestMapping[F] { } object country extends TableDef("country") { - val code = col("code", bpchar(3)) - val name = col("name", nvarchar) + val code = col("code", bpchar(3)) + val name = col("name", nvarchar) val numCities = col("num_cities", int8) } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", nvarchar) + val name = col("name", nvarchar) } val schema = @@ -85,104 +93,116 @@ trait SqlPaging3Mapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("countries"), - ) + fieldMappings = List( + SqlObject("countries") + ) ), ObjectMapping( tpe = PagedCountryType, - fieldMappings = - List( - SqlObject("items"), - CursorField("hasMore", CountryPaging.genHasMore), - SqlField("numCountries", root.numCountries, hidden = true) - ) + fieldMappings = List( + SqlObject("items"), + CursorField("hasMore", CountryPaging.genHasMore), + SqlField("numCountries", root.numCountries, hidden = true) + ) ), ObjectMapping( tpe = CountryType, - fieldMappings = - List( - SqlField("code", country.code, key = true), - SqlField("name", country.name), - SqlObject("cities") - ) + fieldMappings = List( + SqlField("code", country.code, key = true), + SqlField("name", country.name), + SqlObject("cities") + ) ), ObjectMapping( tpe = PagedCityType, - fieldMappings = - List( - SqlField("code", country.code, key = true, hidden = true), - SqlObject("items", Join(country.code, city.countrycode)), - CursorField("hasMore", CityPaging.genHasMore), - SqlField("numCities", country.numCities, hidden = true) - ) + fieldMappings = List( + SqlField("code", country.code, key = true, hidden = true), + SqlObject("items", Join(country.code, city.countrycode)), + CursorField("hasMore", CityPaging.genHasMore), + SqlField("numCities", country.numCities, hidden = true) + ) ), ObjectMapping( tpe = CityType, - fieldMappings = - List( - SqlField("id", city.id, key = true, hidden = true), - SqlField("countrycode", city.countrycode, hidden = true), - SqlField("name", city.name) - ) + fieldMappings = List( + SqlField("id", city.id, key = true, hidden = true), + SqlField("countrycode", city.countrycode, hidden = true), + SqlField("name", city.name) + ) ) ) - abstract class PagingConfig(key: String, countField: String, countAttr: String, orderTerm: Term[String]) { + abstract class PagingConfig( + key: String, + countField: String, + countAttr: String, + orderTerm: Term[String]) { def setup(offset: Option[Int], limit: Option[Int]): Elab[Unit] = for { hasHasMore <- Elab.hasField("hasMore") - hasItems <- Elab.hasField("items") + hasItems <- Elab.hasField("items") resultName <- Elab.fieldAlias("items") - _ <- Elab.env(key -> new PagingInfo(offset, limit, hasItems, resultName, hasHasMore)) + _ <- Elab.env(key -> new PagingInfo(offset, limit, hasItems, resultName, hasHasMore)) } yield () def elabItems = Elab.envE[PagingInfo](key).flatMap(_.elabItems) def elabHasMore = Elab.envE[PagingInfo](key).flatMap(_.elabHasMore) - def genHasMore(c : Cursor): Result[Boolean] = + def genHasMore(c: Cursor): Result[Boolean] = for { info <- c.envR[PagingInfo](key) - c0 <- info.genHasMore(c) + c0 <- info.genHasMore(c) } yield c0 - case class PagingInfo(offset: Option[Int], limit: Option[Int], hasItems: Boolean, itemsAlias: Option[String], hasHasMore: Boolean) { + case class PagingInfo( + offset: Option[Int], + limit: Option[Int], + hasItems: Boolean, + itemsAlias: Option[String], + hasHasMore: Boolean) { def elabItems: Elab[Unit] = Elab.transformChild { child => - val lim0 = if (hasHasMore) limit.map(_+1) else limit - val items = FilterOrderByOffsetLimit(None, Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), offset, lim0, child) + val lim0 = if (hasHasMore) limit.map(_ + 1) else limit + val items = FilterOrderByOffsetLimit( + None, + Some(List(OrderSelection(orderTerm, nullsLast = nullsHigh))), + offset, + lim0, + child) if (hasHasMore) TransformCursor(genItems, items) else items } def genItems(c: Cursor): Result[Cursor] = { for { - size <- c.listSize + size <- c.listSize elems <- c.asList(Seq) } yield { - if(limit.forall(size <= _)) c - else ListTransformCursor(c, size-1, elems.init) + if (limit.forall(size <= _)) c + else ListTransformCursor(c, size - 1, elems.init) } } def elabHasMore: Elab[Unit] = - Elab.addAttribute(countField, Count(Select("items", Select(countAttr)))).whenA(!hasItems) + Elab + .addAttribute(countField, Count(Select("items", Select(countAttr)))) + .whenA(!hasItems) def genHasMore(c: Cursor): Result[Boolean] = - if(hasItems) { + if (hasItems) { for { items <- c.field("items", itemsAlias) - size <- items.listSize + size <- items.listSize } yield limit.exists(size > _) } else { for { num <- c.fieldAs[Long](countField) - } yield num > offset.getOrElse(0)+limit.getOrElse(num.toInt) + } yield num > offset.getOrElse(0) + limit.getOrElse(num.toInt) } } } - object CountryPaging extends PagingConfig("countryPaging", "numCountries", "code", CountryType / "code") + object CountryPaging + extends PagingConfig("countryPaging", "numCountries", "code", CountryType / "code") object CityPaging extends PagingConfig("cityPaging", "numCities", "name", CityType / "name") object OptIntValue { @@ -194,7 +214,10 @@ trait SqlPaging3Mapping[F[_]] extends SqlTestMapping[F] { } override val selectElaborator = SelectElaborator { - case (QueryType, "countries", List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => + case ( + QueryType, + "countries", + List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => CountryPaging.setup(off, lim) case (PagedCountryType, "items", Nil) => @@ -203,7 +226,10 @@ trait SqlPaging3Mapping[F[_]] extends SqlTestMapping[F] { case (PagedCountryType, "hasMore", Nil) => CountryPaging.elabHasMore - case (CountryType, "cities", List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => + case ( + CountryType, + "cities", + List(Binding("offset", OptIntValue(off)), Binding("limit", OptIntValue(lim)))) => CityPaging.setup(off, lim) case (PagedCityType, "items", Nil) => diff --git a/modules/sql-core/src/test/scala/SqlPaging3Suite.scala b/modules/sql-core/src/test/scala/SqlPaging3Suite.scala index b9e537b9..8df29aba 100644 --- a/modules/sql-core/src/test/scala/SqlPaging3Suite.scala +++ b/modules/sql-core/src/test/scala/SqlPaging3Suite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlPaging3Suite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlProjectionMapping.scala b/modules/sql-core/src/test/scala/SqlProjectionMapping.scala index a1672e87..bf695714 100644 --- a/modules/sql-core/src/test/scala/SqlProjectionMapping.scala +++ b/modules/sql-core/src/test/scala/SqlProjectionMapping.scala @@ -17,11 +17,12 @@ package grackle.sql.test import cats.implicits._ -import grackle._, syntax._ -import Predicate.{Const, Eql} -import Query.{Binding, Filter} -import QueryCompiler._ -import Value.{BooleanValue, ObjectValue} +import grackle._ +import grackle.Predicate.{Const, Eql} +import grackle.Query.{Binding, Filter} +import grackle.QueryCompiler._ +import grackle.Value.{BooleanValue, ObjectValue} +import grackle.syntax._ trait SqlProjectionMapping[F[_]] extends SqlTestMapping[F] { @@ -73,38 +74,34 @@ trait SqlProjectionMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("level0"), - SqlObject("level1"), - SqlObject("level2") - ) + fieldMappings = List( + SqlObject("level0"), + SqlObject("level1"), + SqlObject("level2") + ) ), ObjectMapping( tpe = Level0Type, - fieldMappings = - List( - SqlField("id", level0.id, key = true), - SqlObject("level1", Join(level0.id, level1.level0Id)) - ) + fieldMappings = List( + SqlField("id", level0.id, key = true), + SqlObject("level1", Join(level0.id, level1.level0Id)) + ) ), ObjectMapping( tpe = Level1Type, - fieldMappings = - List( - SqlField("id", level1.id, key = true), - SqlField("level0_id", level1.level0Id, hidden = true), - SqlObject("level2", Join(level1.id, level2.level1Id)) - ) + fieldMappings = List( + SqlField("id", level1.id, key = true), + SqlField("level0_id", level1.level0Id, hidden = true), + SqlObject("level2", Join(level1.id, level2.level1Id)) + ) ), ObjectMapping( tpe = Level2Type, - fieldMappings = - List( - SqlField("id", level2.id, key = true), - SqlField("attr", level2.attr), - SqlField("level1_id", level2.level1Id, hidden = true) - ) + fieldMappings = List( + SqlField("id", level2.id, key = true), + SqlField("attr", level2.attr), + SqlField("level1_id", level2.level1Id, hidden = true) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlProjectionSuite.scala b/modules/sql-core/src/test/scala/SqlProjectionSuite.scala index 7db44374..d90515a8 100644 --- a/modules/sql-core/src/test/scala/SqlProjectionSuite.scala +++ b/modules/sql-core/src/test/scala/SqlProjectionSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlProjectionSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlRecursiveInterfacesMapping.scala b/modules/sql-core/src/test/scala/SqlRecursiveInterfacesMapping.scala index d258cffa..1dcad104 100644 --- a/modules/sql-core/src/test/scala/SqlRecursiveInterfacesMapping.scala +++ b/modules/sql-core/src/test/scala/SqlRecursiveInterfacesMapping.scala @@ -19,19 +19,19 @@ import cats.kernel.Eq import io.circe.Encoder import grackle._ -import syntax._ -import Predicate._ +import grackle.Predicate._ +import grackle.syntax._ trait SqlRecursiveInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def itemType: TestCodec[ItemType] object items extends TableDef("recursive_interface_items") { - val id = col("id", text) + val id = col("id", text) val itemType = col("item_type", self.itemType) } object nextItems extends TableDef("recursive_interface_next_items") { - val id = col("id", text) + val id = col("id", text) val nextItem = col("next_item", nullable(text)) } @@ -70,33 +70,35 @@ trait SqlRecursiveInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("items") - ) + fieldMappings = List( + SqlObject("items") + ) ), SqlInterfaceMapping( tpe = IType, discriminator = itemTypeDiscriminator, - fieldMappings = - List( - SqlField("id", items.id, key = true), - SqlField("itemType", items.itemType, discriminator = true), - ) + fieldMappings = List( + SqlField("id", items.id, key = true), + SqlField("itemType", items.itemType, discriminator = true) + ) ), ObjectMapping( tpe = ItemAType, - fieldMappings = - List( - SqlObject("nextItem", Join(items.id, nextItems.id), Join(nextItems.nextItem, items.id)) - ) + fieldMappings = List( + SqlObject( + "nextItem", + Join(items.id, nextItems.id), + Join(nextItems.nextItem, items.id)) + ) ), ObjectMapping( tpe = ItemBType, - fieldMappings = - List( - SqlObject("nextItem", Join(items.id, nextItems.id), Join(nextItems.nextItem, items.id)) - ) + fieldMappings = List( + SqlObject( + "nextItem", + Join(items.id, nextItems.id), + Join(nextItems.nextItem, items.id)) + ) ), LeafMapping[ItemType](ItemTypeType) ) @@ -132,7 +134,7 @@ trait SqlRecursiveInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def fromString(s: String): Option[ItemType] = s.trim.toUpperCase match { - case "ITEM_A" => Some(ItemA) + case "ITEM_A" => Some(ItemA) case "ITEM_B" => Some(ItemB) case _ => None } @@ -151,7 +153,7 @@ trait SqlRecursiveInterfacesMapping[F[_]] extends SqlTestMapping[F] { self => def toInt(i: ItemType): Int = i match { - case ItemA => 1 + case ItemA => 1 case ItemB => 2 } } diff --git a/modules/sql-core/src/test/scala/SqlRecursiveInterfacesSuite.scala b/modules/sql-core/src/test/scala/SqlRecursiveInterfacesSuite.scala index ef561102..67eca2c2 100644 --- a/modules/sql-core/src/test/scala/SqlRecursiveInterfacesSuite.scala +++ b/modules/sql-core/src/test/scala/SqlRecursiveInterfacesSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlRecursiveInterfacesSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlSiblingListsMapping.scala b/modules/sql-core/src/test/scala/SqlSiblingListsMapping.scala index f95d31dd..549e7cdc 100644 --- a/modules/sql-core/src/test/scala/SqlSiblingListsMapping.scala +++ b/modules/sql-core/src/test/scala/SqlSiblingListsMapping.scala @@ -16,12 +16,12 @@ package grackle.sql.test import cats.implicits._ -import grackle._ -import Predicate.{Const, Eql} -import Query.{Binding, Filter, Unique} -import QueryCompiler._ -import Value.StringValue -import syntax._ + +import grackle.Predicate.{Const, Eql} +import grackle.Query.{Binding, Filter, Unique} +import grackle.QueryCompiler._ +import grackle.Value.StringValue +import grackle.syntax._ trait SqlSiblingListsData[F[_]] extends SqlTestMapping[F] { @@ -77,43 +77,38 @@ trait SqlSiblingListsData[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("a") - ) + fieldMappings = List( + SqlObject("a") + ) ), ObjectMapping( tpe = AType, - fieldMappings = - List( - SqlField("id", aTable.id, key = true), - SqlObject("bs", Join(aTable.id, bTable.aId)) - ) + fieldMappings = List( + SqlField("id", aTable.id, key = true), + SqlObject("bs", Join(aTable.id, bTable.aId)) + ) ), ObjectMapping( tpe = BType, - fieldMappings = - List( - SqlField("id", bTable.id, key = true), - SqlObject("cs", Join(bTable.id, cTable.bId)), - SqlObject("ds", Join(bTable.id, dTable.bId)) - ) + fieldMappings = List( + SqlField("id", bTable.id, key = true), + SqlObject("cs", Join(bTable.id, cTable.bId)), + SqlObject("ds", Join(bTable.id, dTable.bId)) + ) ), ObjectMapping( tpe = CType, - fieldMappings = - List( - SqlField("id", cTable.id, key = true), - SqlField("nameC", cTable.nameC) - ) + fieldMappings = List( + SqlField("id", cTable.id, key = true), + SqlField("nameC", cTable.nameC) + ) ), ObjectMapping( tpe = DType, - fieldMappings = - List( - SqlField("id", dTable.id, key = true), - SqlField("nameD", dTable.nameD) - ) + fieldMappings = List( + SqlField("id", dTable.id, key = true), + SqlField("nameD", dTable.nameD) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlSiblingListsSuite.scala b/modules/sql-core/src/test/scala/SqlSiblingListsSuite.scala index 7038af68..be4c9bc4 100644 --- a/modules/sql-core/src/test/scala/SqlSiblingListsSuite.scala +++ b/modules/sql-core/src/test/scala/SqlSiblingListsSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlSiblingListsSuite extends CatsEffectSuite { @@ -62,7 +61,7 @@ trait SqlSiblingListsSuite extends CatsEffectSuite { """ val res = mapping.compileAndRun(query) - //println(res) + // println(res) assertWeaklyEqualIO(res, expected) } diff --git a/modules/sql-core/src/test/scala/SqlTestMapping.scala b/modules/sql-core/src/test/scala/SqlTestMapping.scala index 12eafe7e..15e47246 100644 --- a/modules/sql-core/src/test/scala/SqlTestMapping.scala +++ b/modules/sql-core/src/test/scala/SqlTestMapping.scala @@ -22,8 +22,7 @@ import io.circe.{Decoder => CDecoder, Encoder => CEncoder, Json} import org.tpolecat.sourcepos.SourcePos import org.tpolecat.typename.TypeName -import grackle._ -import sql.SqlMappingLike +import grackle.sql.SqlMappingLike trait SqlTestMapping[F[_]] extends SqlMappingLike[F] { outer => type TestCodec[T] <: Codec @@ -49,8 +48,11 @@ trait SqlTestMapping[F[_]] extends SqlMappingLike[F] { outer => def jsonb: TestCodec[Json] def nullable[T](c: TestCodec[T]): TestCodec[T] - def list[T: CDecoder : CEncoder](c: TestCodec[T]): TestCodec[List[T]] + def list[T: CDecoder: CEncoder](c: TestCodec[T]): TestCodec[List[T]] - def col[T](colName: String, codec: TestCodec[T])(implicit tableName: TableName, typeName: TypeName[T], pos: SourcePos): ColumnRef = + def col[T](colName: String, codec: TestCodec[T])( + implicit tableName: TableName, + typeName: TypeName[T], + pos: SourcePos): ColumnRef = ColumnRef(tableName.name, colName, codec, typeName.value, pos) } diff --git a/modules/sql-core/src/test/scala/SqlTreeMapping.scala b/modules/sql-core/src/test/scala/SqlTreeMapping.scala index 2801d934..4ebc595b 100644 --- a/modules/sql-core/src/test/scala/SqlTreeMapping.scala +++ b/modules/sql-core/src/test/scala/SqlTreeMapping.scala @@ -17,10 +17,11 @@ package grackle.sql.test import cats.implicits._ -import grackle._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ import grackle.syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ trait SqlTreeMapping[F[_]] extends SqlTestMapping[F] { @@ -49,19 +50,17 @@ trait SqlTreeMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("bintree") - ) + fieldMappings = List( + SqlObject("bintree") + ) ), ObjectMapping( tpe = BinTreeType, - fieldMappings = - List( - SqlField("id", bintree.id, key = true), - SqlObject("left", Join(bintree.leftChild, bintree.id)), - SqlObject("right", Join(bintree.rightChild, bintree.id)), - ) + fieldMappings = List( + SqlField("id", bintree.id, key = true), + SqlObject("left", Join(bintree.leftChild, bintree.id)), + SqlObject("right", Join(bintree.rightChild, bintree.id)) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlTreeSuite.scala b/modules/sql-core/src/test/scala/SqlTreeSuite.scala index 30d79679..ce3a526f 100644 --- a/modules/sql-core/src/test/scala/SqlTreeSuite.scala +++ b/modules/sql-core/src/test/scala/SqlTreeSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlTreeSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlUnionSuite.scala b/modules/sql-core/src/test/scala/SqlUnionSuite.scala index 08f53989..b4df4c38 100644 --- a/modules/sql-core/src/test/scala/SqlUnionSuite.scala +++ b/modules/sql-core/src/test/scala/SqlUnionSuite.scala @@ -20,7 +20,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.assertWeaklyEqualIO trait SqlUnionSuite extends CatsEffectSuite { diff --git a/modules/sql-core/src/test/scala/SqlUnionsMapping.scala b/modules/sql-core/src/test/scala/SqlUnionsMapping.scala index e5e8d740..d02e3e41 100644 --- a/modules/sql-core/src/test/scala/SqlUnionsMapping.scala +++ b/modules/sql-core/src/test/scala/SqlUnionsMapping.scala @@ -16,8 +16,8 @@ package grackle.sql.test import grackle._ +import grackle.Predicate._ import grackle.syntax._ -import Predicate._ trait SqlUnionsMapping[F[_]] extends SqlTestMapping[F] { @@ -51,35 +51,31 @@ trait SqlUnionsMapping[F[_]] extends SqlTestMapping[F] { List( ObjectMapping( tpe = QueryType, - fieldMappings = - List( - SqlObject("collection") - ) + fieldMappings = List( + SqlObject("collection") + ) ), SqlUnionMapping( tpe = ItemType, discriminator = itemTypeDiscriminator, - fieldMappings = - List( - SqlField("id", collections.id, key = true, hidden = true), - SqlField("itemType", collections.itemType, discriminator = true, hidden = true) - ) + fieldMappings = List( + SqlField("id", collections.id, key = true, hidden = true), + SqlField("itemType", collections.itemType, discriminator = true, hidden = true) + ) ), ObjectMapping( tpe = ItemAType, - fieldMappings = - List( - SqlField("id", collections.id, key = true, hidden = true), - SqlField("itema", collections.itemA) - ) + fieldMappings = List( + SqlField("id", collections.id, key = true, hidden = true), + SqlField("itema", collections.itemA) + ) ), ObjectMapping( tpe = ItemBType, - fieldMappings = - List( - SqlField("id", collections.id, key = true, hidden = true), - SqlField("itemb", collections.itemB) - ) + fieldMappings = List( + SqlField("id", collections.id, key = true, hidden = true), + SqlField("itemb", collections.itemB) + ) ) ) diff --git a/modules/sql-core/src/test/scala/SqlWorldCompilerSuite.scala b/modules/sql-core/src/test/scala/SqlWorldCompilerSuite.scala index 35eb0537..6a316a61 100644 --- a/modules/sql-core/src/test/scala/SqlWorldCompilerSuite.scala +++ b/modules/sql-core/src/test/scala/SqlWorldCompilerSuite.scala @@ -21,21 +21,27 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ -import Predicate._, Query._ -import sql.{Like, SqlStatsMonitor} - +import grackle.Predicate._ +import grackle.Query._ +import grackle.sql.{Like, SqlStatsMonitor} import grackle.test.GraphQLResponseTests.assertWeaklyEqual -/** Tests that confirm the compiler is writing the queries we want. */ +/** + * Tests that confirm the compiler is writing the queries we want. + */ trait SqlWorldCompilerSuite extends CatsEffectSuite { type Fragment def mapping: IO[(Mapping[IO], SqlStatsMonitor[IO, Fragment])] - /** Expected SQL string for the simple restricted query test. */ + /** + * Expected SQL string for the simple restricted query test. + */ def simpleRestrictedQuerySql: String - /** Expected SQL string for the simple filtered query test. */ + /** + * Expected SQL string for the simple filtered query test. + */ def simpleFilteredQuerySql: String def filterArg: String @@ -66,26 +72,31 @@ trait SqlWorldCompilerSuite extends CatsEffectSuite { val prog: IO[(Json, List[SqlStatsMonitor.SqlStats], Schema)] = for { - mm <- mapping + mm <- mapping (map, mon) = mm res <- map.compileAndRun(query) - ss <- mon.take + ss <- mon.take } yield (res, ss.map(_.normalize), map.schema) - prog.map { case (res, stats, schema) => - assertWeaklyEqual(res, expected) - - assertEquals(stats, - List( - SqlStatsMonitor.SqlStats( - Select("country", Unique(Filter(Eql(schema.ref("Country") / "code",Const("GBR")), Select("name")))), - simpleRestrictedQuerySql, - List(encodeArg("GBR")), - 1, - 2 + prog.map { + case (res, stats, schema) => + assertWeaklyEqual(res, expected) + + assertEquals( + stats, + List( + SqlStatsMonitor.SqlStats( + Select( + "country", + Unique( + Filter(Eql(schema.ref("Country") / "code", Const("GBR")), Select("name")))), + simpleRestrictedQuerySql, + List(encodeArg("GBR")), + 1, + 2 + ) ) ) - ) } } @@ -121,26 +132,30 @@ trait SqlWorldCompilerSuite extends CatsEffectSuite { val prog: IO[(Json, List[SqlStatsMonitor.SqlStats], Schema)] = for { - mm <- mapping + mm <- mapping (map, mon) = mm res <- map.compileAndRun(query) - ss <- mon.take + ss <- mon.take } yield (res, ss.map(_.normalize), map.schema) - prog.map { case (res, stats, schema) => - assertWeaklyEqual(res, expected) - - assertEquals(stats, - List( - SqlStatsMonitor.SqlStats( - Select("cities", Filter(Like(schema.ref("City") / "name","Linh%",true), Select("name"))), - simpleFilteredQuerySql, - List(encodeArg(filterArg)), - 3, - 2 + prog.map { + case (res, stats, schema) => + assertWeaklyEqual(res, expected) + + assertEquals( + stats, + List( + SqlStatsMonitor.SqlStats( + Select( + "cities", + Filter(Like(schema.ref("City") / "name", "Linh%", true), Select("name"))), + simpleFilteredQuerySql, + List(encodeArg(filterArg)), + 3, + 2 + ) ) ) - ) } } } diff --git a/modules/sql-core/src/test/scala/SqlWorldMapping.scala b/modules/sql-core/src/test/scala/SqlWorldMapping.scala index 85f827d2..6409abbb 100644 --- a/modules/sql-core/src/test/scala/SqlWorldMapping.scala +++ b/modules/sql-core/src/test/scala/SqlWorldMapping.scala @@ -18,40 +18,42 @@ package grackle.sql.test import cats.implicits._ import grackle._ -import sql.Like -import syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.sql.Like +import grackle.syntax._ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { object root extends RootDef { val numCountries = col("num_countries", int8) } object country extends TableDef("country") { - val code = col("code", bpchar(3)) - val name = col("name", nvarchar) - val continent = col("continent", nvarchar) - val region = col("region", nvarchar) - val surfacearea = col("surfacearea", float4) - val indepyear = col("indepyear", nullable(int2)) - val population = col("population", int4) + val code = col("code", bpchar(3)) + val name = col("name", nvarchar) + val continent = col("continent", nvarchar) + val region = col("region", nvarchar) + val surfacearea = col("surfacearea", float4) + val indepyear = col("indepyear", nullable(int2)) + val population = col("population", int4) val lifeexpectancy = col("lifeexpectancy", nullable(float4)) - val gnp = col("gnp", nullable(numeric(10, 2))) - val gnpold = col("gnpold", nullable(numeric(10, 2))) - val localname = col("localname", nvarchar) + val gnp = col("gnp", nullable(numeric(10, 2))) + val gnpold = col("gnpold", nullable(numeric(10, 2))) + val localname = col("localname", nvarchar) val governmentform = col("governmentform", nvarchar) - val headofstate = col("headofstate", nullable(nvarchar)) - val capitalId = col("capital", nullable(int4)) - val numCities = col("num_cities", int8) - val code2 = col("code2", bpchar(2)) + val headofstate = col("headofstate", nullable(nvarchar)) + val capitalId = col("capital", nullable(int4)) + val numCities = col("num_cities", int8) + val code2 = col("code2", bpchar(2)) } object city extends TableDef("city") { - val id = col("id", int4) + val id = col("id", int4) val countrycode = col("countrycode", bpchar(3)) - val name = col("name", nvarchar) - val district = col("district", nvarchar) - val population = col("population", int4) + val name = col("name", nvarchar) + val district = col("district", nvarchar) + val population = col("population", int4) } object countrylanguage extends TableDef("countrylanguage") { @@ -108,9 +110,9 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { } """ - val QueryType = schema.ref("Query") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val QueryType = schema.ref("Query") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") val LanguageType = schema.ref("Language") val typeMappings = @@ -132,26 +134,26 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { ObjectMapping( tpe = CountryType, fieldMappings = List( - SqlField("code", country.code, key = true, hidden = true), - SqlField("name", country.name), - SqlField("continent", country.continent), - SqlField("region", country.region), - SqlField("surfacearea", country.surfacearea), - SqlField("indepyear", country.indepyear), - SqlField("population", country.population), + SqlField("code", country.code, key = true, hidden = true), + SqlField("name", country.name), + SqlField("continent", country.continent), + SqlField("region", country.region), + SqlField("surfacearea", country.surfacearea), + SqlField("indepyear", country.indepyear), + SqlField("population", country.population), SqlField("lifeexpectancy", country.lifeexpectancy), - SqlField("gnp", country.gnp), - SqlField("gnpold", country.gnpold), - SqlField("localname", country.localname), + SqlField("gnp", country.gnp), + SqlField("gnpold", country.gnpold), + SqlField("localname", country.localname), SqlField("governmentform", country.governmentform), - SqlField("headofstate", country.headofstate), - SqlField("capitalId", country.capitalId), - SqlField("code2", country.code2), - SqlField("numCities", country.numCities), - SqlObject("cities", Join(country.code, city.countrycode)), - SqlObject("city", Join(country.code, city.countrycode)), - SqlObject("languages", Join(country.code, countrylanguage.countrycode)) - ), + SqlField("headofstate", country.headofstate), + SqlField("capitalId", country.capitalId), + SqlField("code2", country.code2), + SqlField("numCities", country.numCities), + SqlObject("cities", Join(country.code, city.countrycode)), + SqlObject("city", Join(country.code, city.countrycode)), + SqlObject("languages", Join(country.code, countrylanguage.countrycode)) + ) ), ObjectMapping( tpe = CityType, @@ -161,7 +163,7 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { SqlField("name", city.name), SqlField("district", city.district), SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlObject("country", Join(city.countrycode, country.code)) ) ), ObjectMapping( @@ -179,22 +181,31 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { object StringListValue { def unapply(value: Value): Option[List[String]] = value match { - case ListValue(l) => l.traverse { - case StringValue(s) => Some(s) - case _ => None - } + case ListValue(l) => + l.traverse { + case StringValue(s) => Some(s) + case _ => None + } case _ => None } } override val selectElaborator = SelectElaborator { case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CountryType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CountryType / "code", Const(code)), child))) case (QueryType, "city", List(Binding("id", IntValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(CityType / "id", Const(id)), child))) - case (QueryType, "countries", List(Binding("limit", IntValue(num)), Binding("offset", IntValue(off)), Binding("minPopulation", IntValue(min)), Binding("byPopulation", BooleanValue(byPop)))) => + case ( + QueryType, + "countries", + List( + Binding("limit", IntValue(num)), + Binding("offset", IntValue(off)), + Binding("minPopulation", IntValue(min)), + Binding("byPopulation", BooleanValue(byPop)))) => def limit(query: Query): Query = if (num < 1) query else Limit(num, query) @@ -224,12 +235,18 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { Elab.transformChild(child => Filter(Like(CityType / "name", namePattern, true), child)) case (QueryType, "language", List(Binding("language", StringValue(language)))) => - Elab.transformChild(child => Unique(Filter(Eql(LanguageType / "language", Const(language)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(LanguageType / "language", Const(language)), child))) case (QueryType, "languages", List(Binding("languages", StringListValue(languages)))) => Elab.transformChild(child => Filter(In(CityType / "language", languages), child)) - case (QueryType, "search", List(Binding("minPopulation", IntValue(min)), Binding("indepSince", IntValue(year)))) => + case ( + QueryType, + "search", + List( + Binding("minPopulation", IntValue(min)), + Binding("indepSince", IntValue(year)))) => Elab.transformChild(child => Filter( And( @@ -237,11 +254,14 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { Not(Lt(CountryType / "indepyear", Const(Option(year)))) ), child - ) - ) + )) - case (QueryType, "search2", List(Binding("indep", BooleanValue(indep)), Binding("limit", IntValue(num)))) => - Elab.transformChild(child => Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))) + case ( + QueryType, + "search2", + List(Binding("indep", BooleanValue(indep)), Binding("limit", IntValue(num)))) => + Elab.transformChild(child => + Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))) case (QueryType, "numCountries", Nil) => Elab.transformChild(_ => Count(Select("countries", Select("code2")))) @@ -250,7 +270,9 @@ trait SqlWorldMapping[F[_]] extends SqlTestMapping[F] { Elab.transformChild(_ => Count(Select("cities", Select("name")))) case (CountryType, "numCities", List(Binding("namePattern", StringValue(namePattern)))) => - Elab.transformChild(_ => Count(Select("cities", Filter(Like(CityType / "name", namePattern, true), Select("name"))))) + Elab.transformChild(_ => + Count( + Select("cities", Filter(Like(CityType / "name", namePattern, true), Select("name"))))) case (CountryType, "city", List(Binding("id", IntValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(CityType / "id", Const(id)), child))) diff --git a/modules/sql-core/src/test/scala/SqlWorldSuite.scala b/modules/sql-core/src/test/scala/SqlWorldSuite.scala index 6895cce3..85c5eaef 100644 --- a/modules/sql-core/src/test/scala/SqlWorldSuite.scala +++ b/modules/sql-core/src/test/scala/SqlWorldSuite.scala @@ -21,7 +21,6 @@ import io.circe.literal._ import munit.CatsEffectSuite import grackle._ - import grackle.test.GraphQLResponseTests.{assertNoErrorsIO, assertWeaklyEqualIO} trait SqlWorldSuite extends CatsEffectSuite { @@ -42,10 +41,7 @@ trait SqlWorldSuite extends CatsEffectSuite { val res = mapping.compileAndRun(query) val resSize = - res.map (_.hcursor - .downField("data") - .downField("countries") - .values.map(_.size)) + res.map(_.hcursor.downField("data").downField("countries").values.map(_.size)) assertIO(resSize, Some(expected)) } @@ -864,17 +860,18 @@ trait SqlWorldSuite extends CatsEffectSuite { mapping.compileAndRun(query).map { json => val countries = - json - .hcursor - .downField("data") - .downField("countries") - .values - .map(_.toVector) - .get - - val map = countries.map(j => j.hcursor.downField("name").as[String].toOption.get -> j.hcursor.downField("cities").values.map(_.size).get).toMap - - assert(map("Kazakstan") == 21) + json.hcursor.downField("data").downField("countries").values.map(_.toVector).get + + val map = countries + .map(j => + j.hcursor + .downField("name") + .as[String] + .toOption + .get -> j.hcursor.downField("cities").values.map(_.size).get) + .toMap + + assert(map("Kazakstan") == 21) assert(map("Antarctica") == 0) } } @@ -1512,10 +1509,13 @@ trait SqlWorldSuite extends CatsEffectSuite { val expected = 4079 val resTotal = - res.map(_.hcursor - .downField("data") - .downField("countries") - .values.flatMap(_.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) + res.map( + _.hcursor + .downField("data") + .downField("countries") + .values + .flatMap( + _.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) ) assertNoErrorsIO(res) *> @@ -1536,10 +1536,13 @@ trait SqlWorldSuite extends CatsEffectSuite { val expected = 4079 val resTotal = - res.map(_.hcursor - .downField("data") - .downField("countries") - .values.flatMap(_.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) + res.map( + _.hcursor + .downField("data") + .downField("countries") + .values + .flatMap( + _.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) ) assertNoErrorsIO(res) *> @@ -1560,10 +1563,13 @@ trait SqlWorldSuite extends CatsEffectSuite { val expected = 255 val resTotal = - res.map(_.hcursor - .downField("data") - .downField("countries") - .values.flatMap(_.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) + res.map( + _.hcursor + .downField("data") + .downField("countries") + .values + .flatMap( + _.toSeq.traverse(_.hcursor.downField("numCities").as[Int]).map(_.sum).toOption) ) assertNoErrorsIO(res) *> @@ -1596,8 +1602,9 @@ trait SqlWorldSuite extends CatsEffectSuite { } """ - val expected = if (isJS) - json""" + val expected = + if (isJS) + json""" { "data" : { "country" : { @@ -1620,8 +1627,8 @@ trait SqlWorldSuite extends CatsEffectSuite { } } """ - else - json""" + else + json""" { "data" : { "country" : { @@ -1689,8 +1696,9 @@ trait SqlWorldSuite extends CatsEffectSuite { } """ - val expected = if (isJS) - json""" + val expected = + if (isJS) + json""" { "data" : { "language" : { @@ -1701,8 +1709,8 @@ trait SqlWorldSuite extends CatsEffectSuite { } } """ - else - json""" + else + json""" { "data" : { "language" : { diff --git a/modules/sql-pg/js-jvm/src/test/scala/SqlPgDatabaseSuite.scala b/modules/sql-pg/js-jvm/src/test/scala/SqlPgDatabaseSuite.scala index ee6674e0..a1c4965d 100644 --- a/modules/sql-pg/js-jvm/src/test/scala/SqlPgDatabaseSuite.scala +++ b/modules/sql-pg/js-jvm/src/test/scala/SqlPgDatabaseSuite.scala @@ -13,8 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle.sqlpg -package test +package grackle.sqlpg.test import munit.CatsEffectSuite diff --git a/modules/sql-pg/shared/src/main/scala/SqlPgMapping.scala b/modules/sql-pg/shared/src/main/scala/SqlPgMapping.scala index 337f9852..4234aa18 100644 --- a/modules/sql-pg/shared/src/main/scala/SqlPgMapping.scala +++ b/modules/sql-pg/shared/src/main/scala/SqlPgMapping.scala @@ -13,16 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package grackle -package sqlpg +package grackle.sqlpg import cats.MonadThrow import cats.syntax.all._ +import grackle._ +import grackle.Query.OrderSelection import grackle.sql.SqlMappingLike -import Query.OrderSelection -abstract class SqlPgMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with SqlPgMappingLike[F] +abstract class SqlPgMapping[F[_]](implicit val M: MonadThrow[F]) + extends Mapping[F] + with SqlPgMappingLike[F] trait SqlPgMappingLike[F[_]] extends SqlMappingLike[F] { import SqlQuery.SqlSelect @@ -41,7 +43,7 @@ trait SqlPgMappingLike[F[_]] extends SqlMappingLike[F] { Fragments.const(" LIMIT ") |+| limit def likeToFragment(expr: Fragment, pattern: String, caseInsensitive: Boolean): Fragment = { - val op = if(caseInsensitive) "ILIKE" else "LIKE" + val op = if (caseInsensitive) "ILIKE" else "LIKE" expr |+| Fragments.const(s" $op ") |+| Fragments.bind(stringEncoder, pattern) } @@ -54,9 +56,14 @@ trait SqlPgMappingLike[F[_]] extends SqlMappingLike[F] { def collateSelected: Boolean = true def distinctOnToFragment(dcols: List[Fragment]): Fragment = - Fragments.const("DISTINCT ON ") |+| Fragments.parentheses(dcols.intercalate(Fragments.const(", "))) + Fragments.const("DISTINCT ON ") |+| Fragments.parentheses( + dcols.intercalate(Fragments.const(", "))) - def distinctOrderColumn(owner: ColumnOwner, col: SqlColumn, predCols: List[SqlColumn], orders: List[OrderSelection[_]]): SqlColumn = col + def distinctOrderColumn( + owner: ColumnOwner, + col: SqlColumn, + predCols: List[SqlColumn], + orders: List[OrderSelection[_]]): SqlColumn = col def encapsulateUnionBranch(s: SqlSelect): SqlSelect = s def mkLateral(inner: Boolean): Laterality = Laterality.Lateral @@ -64,11 +71,11 @@ trait SqlPgMappingLike[F[_]] extends SqlMappingLike[F] { def defaultOffsetForLimit(limit: Option[Int]): Option[Int] = None def orderToFragment(col: Fragment, ascending: Boolean, nullsLast: Boolean): Fragment = { - val dir = if(ascending) Fragments.empty else Fragments.const(" DESC") + val dir = if (ascending) Fragments.empty else Fragments.const(" DESC") val nulls = - if(!nullsLast && ascending) + if (!nullsLast && ascending) Fragments.const(" NULLS FIRST ") - else if(nullsLast && !ascending) + else if (nullsLast && !ascending) Fragments.const(" NULLS LAST ") else Fragments.empty diff --git a/profile/src/main/scala/Bench.scala b/profile/src/main/scala/Bench.scala index b54a1551..b0aefe54 100644 --- a/profile/src/main/scala/Bench.scala +++ b/profile/src/main/scala/Bench.scala @@ -19,45 +19,46 @@ import scala.concurrent.duration._ import cats.effect.{ExitCode, IO, IOApp, Sync} import cats.implicits._ - -import _root_.doobie.util.meta.Meta -import _root_.doobie.util.transactor.Transactor +import doobie.util.meta.Meta +import doobie.util.transactor.Transactor import grackle._ -import doobie.{DoobieMappingCompanion, DoobieMonitor} -import doobie.postgres.DoobiePgMapping -import sql.Like -import syntax._ -import Query._, Predicate._, Value._ -import QueryCompiler._ +import grackle.Predicate._ +import grackle.Query._ +import grackle.QueryCompiler._ +import grackle.Value._ +import grackle.doobie.{DoobieMappingCompanion, DoobieMonitor} +import grackle.doobie.postgres.DoobiePgMapping +import grackle.sql.Like +import grackle.syntax._ trait WorldPostgresSchema[F[_]] extends DoobiePgMapping[F] { object country extends TableDef("country") { - val code = col("code", Meta[String]) - val name = col("name", Meta[String]) - val continent = col("continent", Meta[String]) - val region = col("region", Meta[String]) - val surfacearea = col("surfacearea", Meta[Float]) - val indepyear = col("indepyear", Meta[Int], true) - val population = col("population", Meta[Int]) + val code = col("code", Meta[String]) + val name = col("name", Meta[String]) + val continent = col("continent", Meta[String]) + val region = col("region", Meta[String]) + val surfacearea = col("surfacearea", Meta[Float]) + val indepyear = col("indepyear", Meta[Int], true) + val population = col("population", Meta[Int]) val lifeexpectancy = col("lifeexpectancy", Meta[Float], true) - val gnp = col("gnp", Meta[BigDecimal], true) - val gnpold = col("gnpold", Meta[BigDecimal], true) - val localname = col("localname", Meta[String]) + val gnp = col("gnp", Meta[BigDecimal], true) + val gnpold = col("gnpold", Meta[BigDecimal], true) + val localname = col("localname", Meta[String]) val governmentform = col("governmentform", Meta[String]) - val headofstate = col("headofstate", Meta[String], true) - val capitalId = col("capitalId", Meta[Int], true) - val numCities = col("num_cities", Meta[Int], false) - val code2 = col("code2", Meta[String]) + val headofstate = col("headofstate", Meta[String], true) + val capitalId = col("capitalId", Meta[Int], true) + val numCities = col("num_cities", Meta[Int], false) + val code2 = col("code2", Meta[String]) } object city extends TableDef("city") { - val id = col("id", Meta[Int]) + val id = col("id", Meta[Int]) val countrycode = col("countrycode", Meta[String]) - val name = col("name", Meta[String]) - val district = col("district", Meta[String]) - val population = col("population", Meta[Int]) + val name = col("name", Meta[String]) + val district = col("district", Meta[String]) + val population = col("population", Meta[Int]) } object countrylanguage extends TableDef("countrylanguage") { @@ -115,9 +116,9 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { } """ - val QueryType = schema.ref("Query") - val CountryType = schema.ref("Country") - val CityType = schema.ref("City") + val QueryType = schema.ref("Query") + val CountryType = schema.ref("Country") + val CityType = schema.ref("City") val LanguageType = schema.ref("Language") val typeMappings = @@ -132,24 +133,24 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { SqlObject("search2") ), ObjectMapping(CountryType)( - SqlField("code", country.code, key = true, hidden = true), - SqlField("name", country.name), - SqlField("continent", country.continent), - SqlField("region", country.region), - SqlField("surfacearea", country.surfacearea), - SqlField("indepyear", country.indepyear), - SqlField("population", country.population), + SqlField("code", country.code, key = true, hidden = true), + SqlField("name", country.name), + SqlField("continent", country.continent), + SqlField("region", country.region), + SqlField("surfacearea", country.surfacearea), + SqlField("indepyear", country.indepyear), + SqlField("population", country.population), SqlField("lifeexpectancy", country.lifeexpectancy), - SqlField("gnp", country.gnp), - SqlField("gnpold", country.gnpold), - SqlField("localname", country.localname), + SqlField("gnp", country.gnp), + SqlField("gnpold", country.gnpold), + SqlField("localname", country.localname), SqlField("governmentform", country.governmentform), - SqlField("headofstate", country.headofstate), - SqlField("capitalId", country.capitalId), - SqlField("code2", country.code2), - SqlField("numCities", country.numCities), - SqlObject("cities", Join(country.code, city.countrycode)), - SqlObject("languages", Join(country.code, countrylanguage.countrycode)) + SqlField("headofstate", country.headofstate), + SqlField("capitalId", country.capitalId), + SqlField("code2", country.code2), + SqlField("numCities", country.numCities), + SqlObject("cities", Join(country.code, city.countrycode)), + SqlObject("languages", Join(country.code, countrylanguage.countrycode)) ), ObjectMapping(CityType)( SqlField("id", city.id, key = true, hidden = true), @@ -157,7 +158,7 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { SqlField("name", city.name), SqlField("district", city.district), SqlField("population", city.population), - SqlObject("country", Join(city.countrycode, country.code)), + SqlObject("country", Join(city.countrycode, country.code)) ), ObjectMapping(LanguageType)( SqlField("language", countrylanguage.language, key = true, associative = true), @@ -170,12 +171,20 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { override val selectElaborator = SelectElaborator { case (QueryType, "country", List(Binding("code", StringValue(code)))) => - Elab.transformChild(child => Unique(Filter(Eql(CountryType / "code", Const(code)), child))) + Elab.transformChild(child => + Unique(Filter(Eql(CountryType / "code", Const(code)), child))) case (QueryType, "city", List(Binding("id", IntValue(id)))) => Elab.transformChild(child => Unique(Filter(Eql(CityType / "id", Const(id)), child))) - case (QueryType, "countries", List(Binding("limit", IntValue(num)), Binding("offset", IntValue(off)), Binding("minPopulation", IntValue(min)), Binding("byPopulation", BooleanValue(byPop)))) => + case ( + QueryType, + "countries", + List( + Binding("limit", IntValue(num)), + Binding("offset", IntValue(off)), + Binding("minPopulation", IntValue(min)), + Binding("byPopulation", BooleanValue(byPop)))) => def limit(query: Query): Query = if (num < 1) query else Limit(num, query) @@ -205,9 +214,15 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { Elab.transformChild(child => Filter(Like(CityType / "name", namePattern, true), child)) case (QueryType, "language", List(Binding("language", StringValue(language)))) => - Elab.transformChild(child => Unique(Filter(Eql(LanguageType / "language", Const(language)), child))) - - case (QueryType, "search", List(Binding("minPopulation", IntValue(min)), Binding("indepSince", IntValue(year)))) => + Elab.transformChild(child => + Unique(Filter(Eql(LanguageType / "language", Const(language)), child))) + + case ( + QueryType, + "search", + List( + Binding("minPopulation", IntValue(min)), + Binding("indepSince", IntValue(year)))) => Elab.transformChild(child => Filter( And( @@ -215,17 +230,22 @@ trait WorldMapping[F[_]] extends WorldPostgresSchema[F] { Not(Lt(CountryType / "indepyear", Const(Option(year)))) ), child - ) - ) + )) - case (QueryType, "search2", List(Binding("indep", BooleanValue(indep)), Binding("limit", IntValue(num)))) => - Elab.transformChild(child => Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))) + case ( + QueryType, + "search2", + List(Binding("indep", BooleanValue(indep)), Binding("limit", IntValue(num)))) => + Elab.transformChild(child => + Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))) case (CountryType, "numCities", List(Binding("namePattern", AbsentValue))) => Elab.transformChild(_ => Count(Select("cities", Select("name")))) case (CountryType, "numCities", List(Binding("namePattern", StringValue(namePattern)))) => - Elab.transformChild(_ => Count(Select("cities", Filter(Like(CityType / "name", namePattern, true), Select("name"))))) + Elab.transformChild(_ => + Count( + Select("cities", Filter(Like(CityType / "name", namePattern, true), Select("name"))))) } } @@ -262,22 +282,22 @@ object Bench extends IOApp { def runQuery(show: Boolean, time: Boolean): IO[Unit] = { for { - st <- if(time) IO.realTime else IO.pure(0.millis) + st <- if (time) IO.realTime else IO.pure(0.millis) res <- mapping.compileAndRun(query) - et <- if(time) IO.realTime else IO.pure(0.millis) - _ <- IO.println(s"Execution time: ${et-st}").whenA(time) - _ <- IO.println(res).whenA(show) + et <- if (time) IO.realTime else IO.pure(0.millis) + _ <- IO.println(s"Execution time: ${et - st}").whenA(time) + _ <- IO.println(res).whenA(show) } yield () } def run(args: List[String]) = { for { - _ <- IO.println("-----------------") - _ <- IO.println("Warmup ...") - _ <- runQuery(false, true).replicateA(100) - _ <- IO.println("-----------------") - _ <- IO.println("Executing query ...") - _ <- runQuery(false, true).replicateA(1000) + _ <- IO.println("-----------------") + _ <- IO.println("Warmup ...") + _ <- runQuery(false, true).replicateA(100) + _ <- IO.println("-----------------") + _ <- IO.println("Executing query ...") + _ <- runQuery(false, true).replicateA(1000) } yield ExitCode.Success } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b9d84e7..90c6758d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,11 +1,11 @@ -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.6") -addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.6") -addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") -addSbtPlugin("nl.zolotko.sbt" % "sbt-jfr" % "0.0.1") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4") -addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.1") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.6") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.6") +addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") +addSbtPlugin("nl.zolotko.sbt" % "sbt-jfr" % "0.0.1") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4") +addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.1") diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt index f8629497..2e5f8761 100644 --- a/project/project/plugins.sbt +++ b/project/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.3") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.3")