Skip to content

Commit

Permalink
Merge pull request #378 from gemini-hlsw/topic/internal-errors
Browse files Browse the repository at this point in the history
Reworked error handling
  • Loading branch information
milessabin authored May 9, 2023
2 parents 6aff2e7 + cdefefb commit 3c4aa60
Show file tree
Hide file tree
Showing 77 changed files with 2,401 additions and 2,000 deletions.
4 changes: 0 additions & 4 deletions demo/src/main/scala/demo/GraphQLService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package demo

import cats.effect.Concurrent
import cats.Id
import cats.implicits._
import edu.gemini.grackle.Mapping
import io.circe.{Json, ParsingFailure, parser}
Expand All @@ -22,9 +21,6 @@ object GraphQLService {
def fromMapping[F[_]: Concurrent](mapping: Mapping[F]): GraphQLService[F] =
(op: Option[String], vars: Option[Json], query: String) => mapping.compileAndRun(query, op, vars)

def fromGenericIdMapping[F[_]: Concurrent](mapping: Mapping[Id]): GraphQLService[F] =
(op: Option[String], vars: Option[Json], query: String) => mapping.compileAndRun(query, op, vars).pure[F]

def routes[F[_]: Concurrent](prefix: String, service: GraphQLService[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F]{}
import dsl._
Expand Down
6 changes: 3 additions & 3 deletions demo/src/main/scala/demo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package demo
import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.implicits._
import com.dimafeng.testcontainers.PostgreSQLContainer
import demo.starwars.StarWarsMapping
import demo.starwars.{StarWarsData, StarWarsMapping}
import demo.world.WorldMapping
import doobie.hikari.HikariTransactor
import org.flywaydb.core.Flyway
Expand All @@ -30,7 +30,7 @@ object Main extends IOApp {
)
val starWarsGraphQLRoutes = GraphQLService.routes[IO](
"starwars",
GraphQLService.fromGenericIdMapping(StarWarsMapping)
GraphQLService.fromMapping(new StarWarsMapping[IO] with StarWarsData[IO])
)
DemoServer.stream[IO](worldGraphQLRoutes <+> starWarsGraphQLRoutes).compile.drain
}
Expand Down Expand Up @@ -72,4 +72,4 @@ object Main extends IOApp {
flyway.load().migrate()
}.void
}
// #main
// #main
172 changes: 83 additions & 89 deletions demo/src/main/scala/demo/starwars/StarWarsMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,97 @@

package demo.starwars

import cats._
import cats.implicits._
import edu.gemini.grackle.Predicate._
import edu.gemini.grackle.Query._
import edu.gemini.grackle.QueryCompiler._
import edu.gemini.grackle.QueryInterpreter.{mkErrorResult, mkOneError}
import edu.gemini.grackle.Value._
import edu.gemini.grackle._
import edu.gemini.grackle.generic._
import edu.gemini.grackle.syntax._

trait StarWarsMapping[F[_]] extends GenericMapping[F] { self: StarWarsData[F] =>
// #schema
val schema =
schema"""
type Query {
hero(episode: Episode!): Character!
character(id: ID!): Character
human(id: ID!): Human
droid(id: ID!): Droid
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
interface Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
}
type Human implements Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
homePlanet: String
}
type Droid implements Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
primaryFunction: String
}
"""
// #schema

val QueryType = schema.ref("Query")
val EpisodeType = schema.ref("Episode")
val CharacterType = schema.ref("Character")
val HumanType = schema.ref("Human")
val DroidType = schema.ref("Droid")

val typeMappings =
List(
// #root
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 })
)
)
// #root
)

// #elaborator
override val selectElaborator = new SelectElaborator(Map(
QueryType -> {
// The hero selector takes an Episode argument and yields a single value. We use the
// Unique operator to pick out the target using the FieldEquals predicate.
case Select("hero", List(Binding("episode", TypedEnumValue(e))), child) =>
Episode.values.find(_.toString == e.name).map { episode =>
Select("hero", Nil, Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))).success
}.getOrElse(Result.failure(s"Unknown episode '${e.name}'"))

// The character, human and droid selectors all take a single ID argument and yield a
// single value (if any) or null. We use the Unique operator to pick out the target
// using the FieldEquals predicate.
case Select(f@("character" | "human" | "droid"), List(Binding("id", IDValue(id))), child) =>
Select(f, Nil, Unique(Filter(Eql(CharacterType / "id", Const(id)), child))).success
}
))
// #elaborator
}

// The types and values for the in-memory Star Wars example.
object StarWarsData {
import StarWarsMapping._
trait StarWarsData[F[_]] extends GenericMapping[F] { self: StarWarsMapping[F] =>
import semiauto._

// #model_types
Expand Down Expand Up @@ -66,9 +143,9 @@ object StarWarsData {

def resolveFriends(c: Character): Result[Option[List[Character]]] =
c.friends match {
case None => None.rightIor
case None => None.success
case Some(ids) =>
ids.traverse(id => characters.find(_.id == id).toRightIor(mkOneError(s"Bad id '$id'"))).map(_.some)
ids.traverse(id => characters.find(_.id == id).toResultOrError(s"Bad id '$id'")).map(_.some)
}

// #model_types
Expand Down Expand Up @@ -139,87 +216,4 @@ object StarWarsData {
EMPIRE -> lukeSkywalker,
JEDI -> r2d2
)

}

object StarWarsMapping extends GenericMapping[Id] {
import StarWarsData._

// #schema
val schema =
schema"""
type Query {
hero(episode: Episode!): Character!
character(id: ID!): Character
human(id: ID!): Human
droid(id: ID!): Droid
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
interface Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
}
type Human implements Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
homePlanet: String
}
type Droid implements Character {
id: String!
name: String
friends: [Character!]
appearsIn: [Episode!]
primaryFunction: String
}
"""
// #schema

val QueryType = schema.ref("Query")
val EpisodeType = schema.ref("Episode")
val CharacterType = schema.ref("Character")
val HumanType = schema.ref("Human")
val DroidType = schema.ref("Droid")

val typeMappings =
List(
// #root
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 })
)
)
// #root
)

// #elaborator
override val selectElaborator = new SelectElaborator(Map(
QueryType -> {
// The hero selector takes an Episode argument and yields a single value. We use the
// Unique operator to pick out the target using the FieldEquals predicate.
case Select("hero", List(Binding("episode", TypedEnumValue(e))), child) =>
Episode.values.find(_.toString == e.name).map { episode =>
Select("hero", Nil, Unique(Filter(Eql(CharacterType / "id", Const(hero(episode).id)), child))).rightIor
}.getOrElse(mkErrorResult(s"Unknown episode '${e.name}'"))

// The character, human and droid selectors all take a single ID argument and yield a
// single value (if any) or null. We use the Unique operator to pick out the target
// using the FieldEquals predicate.
case Select(f@("character" | "human" | "droid"), List(Binding("id", IDValue(id))), child) =>
Select(f, Nil, Unique(Filter(Eql(CharacterType / "id", Const(id)), child))).rightIor
}
))
// #elaborator
}
20 changes: 10 additions & 10 deletions demo/src/main/scala/demo/world/WorldMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ trait WorldMapping[F[_]] extends DoobieMapping[F] {
QueryType -> {

case Select("country", List(Binding("code", StringValue(code))), child) =>
Select("country", Nil, Unique(Filter(Eql(CountryType / "code", Const(code)), child))).rightIor
Select("country", Nil, Unique(Filter(Eql(CountryType / "code", Const(code)), child))).success

case Select("city", List(Binding("id", IntValue(id))), child) =>
Select("city", Nil, Unique(Filter(Eql(CityType / "id", Const(id)), child))).rightIor
Select("city", Nil, Unique(Filter(Eql(CityType / "id", Const(id)), child))).success

case Select("countries", List(Binding("limit", IntValue(num)), Binding("offset", IntValue(off)), Binding("minPopulation", IntValue(min)), Binding("byPopulation", BooleanValue(byPop))), child) =>
def limit(query: Query): Query =
Expand All @@ -202,16 +202,16 @@ trait WorldMapping[F[_]] extends DoobieMapping[F] {
if (min == 0) query
else Filter(GtEql(CountryType / "population", Const(min)), query)

Select("countries", Nil, limit(offset(order(filter(child))))).rightIor
Select("countries", Nil, limit(offset(order(filter(child))))).success

case Select("cities", List(Binding("namePattern", StringValue(namePattern))), child) =>
if (namePattern == "%")
Select("cities", Nil, child).rightIor
Select("cities", Nil, child).success
else
Select("cities", Nil, Filter(Like(CityType / "name", namePattern, true), child)).rightIor
Select("cities", Nil, Filter(Like(CityType / "name", namePattern, true), child)).success

case Select("language", List(Binding("language", StringValue(language))), child) =>
Select("language", Nil, Unique(Filter(Eql(LanguageType / "language", Const(language)), child))).rightIor
Select("language", Nil, Unique(Filter(Eql(LanguageType / "language", Const(language)), child))).success

case Select("search", List(Binding("minPopulation", IntValue(min)), Binding("indepSince", IntValue(year))), child) =>
Select("search", Nil,
Expand All @@ -222,17 +222,17 @@ trait WorldMapping[F[_]] extends DoobieMapping[F] {
),
child
)
).rightIor
).success

case Select("search2", List(Binding("indep", BooleanValue(indep)), Binding("limit", IntValue(num))), child) =>
Select("search2", Nil, Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))).rightIor
Select("search2", Nil, Limit(num, Filter(IsNull[Int](CountryType / "indepyear", isNull = !indep), child))).success
},
CountryType -> {
case Select("numCities", List(Binding("namePattern", AbsentValue)), Empty) =>
Count("numCities", Select("cities", Nil, Select("name", Nil, Empty))).rightIor
Count("numCities", Select("cities", Nil, Select("name", Nil, Empty))).success

case Select("numCities", List(Binding("namePattern", StringValue(namePattern))), Empty) =>
Count("numCities", Select("cities", Nil, Filter(Like(CityType / "name", namePattern, true), Select("name", Nil, Empty)))).rightIor
Count("numCities", Select("cities", Nil, Filter(Like(CityType / "name", namePattern, true), Select("name", Nil, Empty)))).success
}
))
// #elaborator
Expand Down
2 changes: 1 addition & 1 deletion docs/src/main/paradox/tutorial/in-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Extracting out the case for the `character` selector,

```scala
case Select(f@("character"), List(Binding("id", IDValue(id))), child) =>
Select(f, Nil, Unique(Filter(Eql(CharacterType / "id", Const(id)), child))).rightIor
Select(f, Nil, Unique(Filter(Eql(CharacterType / "id", Const(id)), child))).success

```

Expand Down
Loading

0 comments on commit 3c4aa60

Please sign in to comment.