Skip to content

Commit 82f1f92

Browse files
authored
Merge pull request #471 from 1e16miin/feat/custom-directives-introspection-with-repeatable
Add custom directive introspection and repeatable directive validation
2 parents f369cdd + 4ff5fe7 commit 82f1f92

File tree

6 files changed

+359
-97
lines changed

6 files changed

+359
-97
lines changed

resources/com/walmartlabs/lacinia/introspection.edn

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@
6161
{:name {:type (non-null String)}
6262
:description {:type String}
6363
:locations {:type (non-null (list :__DirectiveLocation))}
64-
:args {:type (non-null (list :__InputValue))}}}}
64+
:args {:type (non-null (list :__InputValue))}
65+
:isRepeatable {:type (non-null Boolean)
66+
:resolve :is-repeatable}}}}
6567

6668
:enums
6769
{:__TypeKind
@@ -83,7 +85,19 @@
8385
"FIELD"
8486
"FRAGMENT_DEFINITION"
8587
"FRAGMENT_SPREAD"
86-
"INLINE_FRAGMENT"]}}
88+
"INLINE_FRAGMENT"
89+
"VARIABLE_DEFINITION"
90+
"SCHEMA"
91+
"SCALAR"
92+
"OBJECT"
93+
"FIELD_DEFINITION"
94+
"ARGUMENT_DEFINITION"
95+
"INTERFACE"
96+
"UNION"
97+
"ENUM"
98+
"ENUM_VALUE"
99+
"INPUT_OBJECT"
100+
"INPUT_FIELD_DEFINITION"]}}
87101

88102
:queries
89103
{:__type {:type :__Type

src/com/walmartlabs/lacinia/introspection.clj

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
(ns ^:no-doc com.walmartlabs.lacinia.introspection
1616
"Uses the compiled schema to expose introspection."
1717
(:require
18-
[clojure.edn :as edn]
19-
[clojure.java.io :as io]
20-
[com.walmartlabs.lacinia.util :as util]
21-
[com.walmartlabs.lacinia.internal-utils :refer [remove-keys is-internal-type-name? cond-let
22-
get-nested keepv]]
23-
[com.walmartlabs.lacinia.constants :as constants]
24-
[clojure.string :as str]
25-
[clojure.data.json :as json]
26-
[clojure.spec.alpha :as s]))
18+
[clojure.edn :as edn]
19+
[clojure.java.io :as io]
20+
[com.walmartlabs.lacinia.util :as util]
21+
[com.walmartlabs.lacinia.internal-utils :refer [remove-keys is-internal-type-name? cond-let
22+
get-nested keepv]]
23+
[com.walmartlabs.lacinia.constants :as constants]
24+
[clojure.string :as str]
25+
[clojure.data.json :as json]
26+
[clojure.spec.alpha :as s]))
2727

2828
(def ^:private category->kind
2929
{:scalar :SCALAR
@@ -103,6 +103,68 @@
103103
(-> % :deprecated is-deprecated? not)))
104104
(sort-by :field-name))))))
105105

106+
(defn ^:private convert-location
107+
"Converts a directive location keyword to its GraphQL enum value format"
108+
[location]
109+
(-> location
110+
name
111+
str/upper-case
112+
(str/replace "-" "_")
113+
keyword))
114+
115+
(defn ^:private convert-directive-arg
116+
"Converts a directive argument definition to introspection format"
117+
[arg-name arg-def]
118+
(let [{:keys [kind type]} (:type arg-def)]
119+
{:name (name arg-name)
120+
:description (:description arg-def)
121+
::type-map {:kind kind :type type}
122+
::default-value (:default-value arg-def)}))
123+
124+
(defn ^:private convert-directive
125+
"Converts a directive definition to introspection format"
126+
[[directive-name directive-def]]
127+
{:name (name directive-name)
128+
:description (:description directive-def)
129+
:locations (vec (map convert-location (:locations directive-def)))
130+
:repeatable (get directive-def :repeatable false)
131+
:args (vec (map (fn [[arg-name arg-def]]
132+
(convert-directive-arg arg-name arg-def))
133+
(:args directive-def)))})
134+
135+
(defn ^:private get-builtin-directives
136+
"Returns the built-in GraphQL directives for introspection.
137+
138+
TODO: This duplicates some logic from parser.clj's builtin-directives.
139+
Future improvement: Consolidate all builtin directive definitions into
140+
a single source of truth shared between parser and schema compilation."
141+
[]
142+
(let [not-null-boolean {:kind :non-null
143+
:type {:kind :root
144+
:type :Boolean}}]
145+
[{:name "skip"
146+
:description "Skip the selection only when the `if` argument is true."
147+
:locations [:INLINE_FRAGMENT :FIELD :FRAGMENT_SPREAD]
148+
:repeatable false
149+
:args [{:name "if"
150+
:description "Triggering argument for skip directive."
151+
::type-map not-null-boolean}]}
152+
{:name "include"
153+
:description "Include the selection only when the `if` argument is true."
154+
:locations [:INLINE_FRAGMENT :FIELD :FRAGMENT_SPREAD]
155+
:repeatable false
156+
:args [{:name "if"
157+
:description "Triggering argument for include directive."
158+
::type-map not-null-boolean}]}]))
159+
160+
(defn ^:private get-all-directives
161+
"Returns all directives (built-in and custom) for the schema."
162+
[schema]
163+
(let [directive-defs (:com.walmartlabs.lacinia.schema/directive-defs schema)
164+
custom-directives (->> directive-defs
165+
(map convert-directive))]
166+
(vec (concat (get-builtin-directives) custom-directives))))
167+
106168
(defn ^:private resolve-root-schema
107169
[context _ _]
108170
(let [schema (get context constants/schema-key)
@@ -116,22 +178,8 @@
116178
omit-subs (-> subs-root :fields empty?)
117179
type-names' (cond-> (set type-names)
118180
omit-mutations (disj (:mutation root))
119-
omit-subs (disj (:subscription root)))
120-
not-null-boolean {:kind :non-null
121-
:type {:kind :root
122-
:type :Boolean}}]
123-
(cond-> {:directives [{:name "skip"
124-
:description "Skip the selection only when the `if` argument is true."
125-
:locations [:INLINE_FRAGMENT :FIELD :FRAGMENT_SPREAD]
126-
:args [{:name "if"
127-
:description "Triggering argument for skip directive."
128-
::type-map not-null-boolean}]}
129-
{:name "include"
130-
:description "Include the selection only when the `if` argument is true."
131-
:locations [:INLINE_FRAGMENT :FIELD :FRAGMENT_SPREAD]
132-
:args [{:name "if"
133-
:description "Triggering argument for include directive."
134-
::type-map not-null-boolean}]}]
181+
omit-subs (disj (:subscription root)))]
182+
(cond-> {:directives (get-all-directives schema)
135183
:types (->> type-names'
136184
sort
137185
(map #(type-name->schema-type schema %)))
@@ -143,7 +191,6 @@
143191
(not omit-subs)
144192
(assoc :subscriptionType (schema-type schema subs-root)))))
145193

146-
147194
(defn ^:private resolve-root-type
148195
[context args _]
149196
(let [schema (get context constants/schema-key)
@@ -210,6 +257,11 @@
210257
(when (::type-map value)
211258
(resolve-nested-type context nil value)))
212259

260+
(defn ^:private resolve-is-repeatable
261+
"Resolves isRepeatable field for a directive"
262+
[_ _ directive]
263+
(:repeatable directive))
264+
213265
(defmulti emit-default-value
214266
(fn [schema type-map value]
215267
(cond-let
@@ -296,4 +348,5 @@
296348
:interfaces resolve-interfaces
297349
:of-type resolve-of-type
298350
:possible-types resolve-possible-types
299-
:default-value default-value})))
351+
:default-value default-value
352+
:is-repeatable resolve-is-repeatable})))

src/com/walmartlabs/lacinia/parser.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
;; At some point, this will move to the schema when we work out how to do extensible
5959
;; directives. A directive effector is invoked during the prepare phase to modify
6060
;; a node based on the directive arguments.
61+
;;
62+
;; TODO: This creates inconsistency with schema.clj's compile-directive-defs which
63+
;; defines @deprecated. Future improvement: consolidate all
64+
;; builtin directive definitions into schema directive-defs for consistency.
6165
(def ^:private builtin-directives
6266
(let [if-arg {:if {:type {:kind :non-null
6367
:type {:kind :root

0 commit comments

Comments
 (0)