Skip to content

Commit 4a85dcf

Browse files
committed
general query validation, subquery types in annotation
1 parent 99cc91f commit 4a85dcf

70 files changed

Lines changed: 1547 additions & 323 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,57 @@ They must extend `GraphQLOperation[S]`, defining the following members:
9999
}
100100
```
101101

102+
#### Validating operations against the schema
103+
104+
Hand-written operations and subqueries (those defined manually, without the code generator) are
105+
validated against the schema by the `GraphQLValidate` rule, which reuses the same checks as the
106+
generator — no code generation involved. Any definition extending `GraphQLOperation[S]` /
107+
`GraphQLOperation.Typed[S, ...]` (checked via its `document`) or `GraphQLSubquery[S]` /
108+
`GraphQLSubquery.Typed[S, ...]` (checked via its `subquery`) is validated; fields, arguments,
109+
variables and deprecations that don't typecheck against schema `S` are reported as scalafix
110+
diagnostics. Operations that *are* generated (annotated with `@GraphQL`) are validated during
111+
generation.
112+
113+
A subquery declares the GraphQL root type(s) its selection applies to with a
114+
`@clue.annotation.GraphQLType` annotation:
115+
116+
```scala
117+
@GraphQLType("Character")
118+
abstract class FriendFields extends GraphQLSubquery.Typed[StarWars, Json] {
119+
val subquery = "{ id name }"
120+
}
121+
```
122+
123+
Hand-written subqueries may declare **multiple** types (the selection is validated against each:
124+
`@GraphQLType("Human", "Droid")`). A subquery processed by the generator (`@GraphQL`) must declare
125+
**exactly one** type. `@GraphQLType` is only valid on a subquery, not on a `GraphQLOperation`.
126+
127+
The schema location is configured (once) in `.scalafix.conf`:
128+
129+
```hocon
130+
Clue.schemaDirs = ["path/to/schemas"]
131+
```
132+
133+
**With `sbt-clue`:** validation runs automatically **on every compile** — the plugin enables
134+
scalafix's on-compile hook for the validation rule, so an invalid hand-written operation/subquery
135+
fails the build. `<project>/clueCheck` runs the same check on demand (e.g. in CI). The generator
136+
separately validates the annotated sources under `src/clue/scala` during generation. A
137+
`@GraphQL`/`@GraphQLSchema`/`@GraphQLStub` annotation found outside the clue source directory is
138+
reported as a **warning** (those are only processed by the generator, so the annotation has no effect
139+
there). (Don't add `rules = [GraphQLGen]` to `.scalafix.conf` — a plain `scalafixAll` would then
140+
expand the annotated generator inputs in place.)
141+
142+
**Without the plugin** (running scalafix directly), use the validation-only `GraphQLValidate` rule
143+
(it reads the same `Clue` config) and run `scalafixAll` / `scalafixCheckAll`:
144+
145+
```hocon
146+
rules = [GraphQLValidate]
147+
Clue.schemaDirs = ["path/to/schemas"]
148+
```
149+
150+
A subquery with no `@GraphQLType` annotation has no declared root type, so it can't be validated —
151+
this is reported as a warning rather than silently skipped.
152+
102153
### 3) Invoke operations
103154

104155
#### Example

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
lazy val V = _root_.scalafix.sbt.BuildInfo
22

3-
ThisBuild / tlBaseVersion := "0.54"
3+
ThisBuild / tlBaseVersion := "0.55"
44
ThisBuild / tlJdkRelease := Some(17)
55
ThisBuild / githubWorkflowJavaVersions := Seq("25", "17").map(JavaSpec.temurin(_))
66
ThisBuild / scalaVersion := "3.8.4"

clue-plugin-integration.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
3. **Schema Reference Configuration**:
1919
- Create or update `.scalafix.conf` to reference GraphQL schemas:
2020
```
21-
GraphQLGen.schemaDirs=["/lucuma/schemas"] # /lucuma/schemas is in lucuma-schemas
21+
Clue.schemaDirs=["/lucuma/schemas"] # /lucuma/schemas is in lucuma-schemas
2222
```
2323
In this example the graphql schema is in a library
2424
Multiple directories can be specified:
@@ -36,6 +36,21 @@
3636
2. These generated sources are compiled along with regular code
3737
3. Generated classes can be imported, like `import [package].SomeQueriesGQL`
3838

39+
## Validating hand-written operations
40+
41+
The generator validates the annotated operations/subqueries under `src/clue/scala`. Hand-written ones
42+
elsewhere in the project (e.g. `@GraphQLType("Type") object Foo extends GraphQLSubquery.Typed[Schema, T]`
43+
in `src/main`) are validated **automatically on every compile**: the plugin enables scalafix's
44+
on-compile hook for the `GraphQLValidate` rule (via a generated config that includes your
45+
`.scalafix.conf` and adds `triggered.rules = [GraphQLValidate]`), so an invalid query fails the
46+
build. `<project>/clueCheck` runs the same validation on demand (e.g. in CI).
47+
48+
A misplaced `@GraphQL`/`@GraphQLSchema`/`@GraphQLStub` annotation outside `src/clue/scala` is reported
49+
as a warning (those are only processed by the generator). Do not add `rules = [GraphQLGen]` to
50+
`.scalafix.conf`: a plain `scalafixAll` would then expand the annotated generator inputs in place.
51+
52+
> Note: on-compile validation assumes your config is the standard `.scalafix.conf` at the build root.
53+
3954
## Common Issues
4055

4156
- Missing schema error: "No schema [SchemaName.graphql] found in paths []"

core/src/main/scala/clue/GraphQLSubquery.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package clue
66
import io.circe.Decoder
77

88
/*
9-
* A subquery must extend this trait.
9+
* A subquery must extend this trait. The GraphQL root type(s) the subquery applies to are declared
10+
* via the `@clue.annotation.GraphQLType` annotation (used for schema validation), not constructor
11+
* arguments.
1012
*/
11-
abstract class GraphQLSubquery[S](val rootType: String) {
13+
abstract class GraphQLSubquery[S] {
1214
type Data
1315

1416
val dataDecoder: Decoder[Data]
@@ -23,7 +25,7 @@ abstract class GraphQLSubquery[S](val rootType: String) {
2325
}
2426

2527
object GraphQLSubquery {
26-
abstract class Typed[S, T: Decoder](rootType: String) extends GraphQLSubquery[S](rootType) {
28+
abstract class Typed[S, T: Decoder] extends GraphQLSubquery[S] {
2729
override type Data = T
2830
override val dataDecoder = summon[Decoder[T]]
2931
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA)
2+
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause
3+
4+
package clue.annotation
5+
6+
import scala.annotation.StaticAnnotation
7+
8+
/**
9+
* Declares the GraphQL root type(s) a `GraphQLSubquery`'s selection set applies to, so it can be
10+
* validated against the schema. Multiple types are allowed for hand-written subqueries (the
11+
* selection is validated against each); a subquery processed by the generator (`@GraphQL`) must
12+
* declare exactly one. Not valid on a `GraphQLOperation`.
13+
*/
14+
class GraphQLType(types: String*) extends StaticAnnotation

flake.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
typelevelShell = {
2121
nodejs.enable = true;
2222
nodejs.package = pkgs.nodejs_24;
23-
jdk.package = pkgs.jdk17;
23+
jdk.package = pkgs.jdk25;
2424
};
2525
};
2626
}

gen/input/src/main/scala/test/LucumaODBSchema.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// format: off
55
/*
66
rules = [GraphQLGen]
7-
GraphQLGen.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
7+
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
88
*/
99
package test
1010

gen/input/src/main/scala/test/LucumaQuery.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// format: off
55
/*
66
rules = [GraphQLGen]
7-
GraphQLGen.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
7+
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
88
*/
99
package test
1010

gen/input/src/main/scala/test/LucumaQuery2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// format: off
55
/*
66
rules = [GraphQLGen]
7-
GraphQLGen.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
7+
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
88
*/
99
package test
1010

0 commit comments

Comments
 (0)