diff --git a/CHANGES.md b/CHANGES.md index 3137c394..8892d29b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,12 @@ Changes have started to bring Lacinia into compliance with the [June 2018 version of the GraphQL specification](https://github.com/facebook/graphql/releases/tag/June2018). Lacinia now supports block strings (via `"""`) in query and schema documents. -However, we have not yet implemented using block strings as documentation in schemas. -The error maps inside the `:error` key are now structured according to the spec; +In addition, descriptions are now supported inside schema documents; +a string (or block string) before an element in the schema becomes the +documentation for that element. + +The error maps inside the `:error` key are now structured according to the June 2018 spec; the top level keys are `:message`, `:locations`, and `:path`, and `:extensions` (which contains any other keys in the error map supplied by the field resolver). @@ -25,9 +28,10 @@ A change to how GraphQL schema documentation is attached. Previously, arguments were refered to as `:MyType.my_field/arg_name` but with this release, we've changed it to `:MyType/my_field.arg_name`. -It is now possible, when parsing a schema from SDL, to document interfaces, enums, -scalars, and unions. -Previously, only objects and input objects could be documented. +It is now possible, when parsing a schema from SDL via +`com.walmartlabs.lacinia.parser.schema/parse-schema`, to +attach descriptions to interfaces, enums, scalars, and unions. +Previously, only objects and input objects could have descriptions attached. New function `com.walmartlabs.lacinia.util/inject-resolvers` is an alternate way to attach resolvers to a schema. diff --git a/dev-resources/documented-schema.sdl b/dev-resources/documented-schema.sdl new file mode 100644 index 00000000..b3f11d3c --- /dev/null +++ b/dev-resources/documented-schema.sdl @@ -0,0 +1,60 @@ +{ + """ + Things that have a name. + """ + interface Named { + "The unique name for the Named thing." + name: String + } + + """ + File node type. + """ + enum FileNodeType { + "A standard file-system file." + FILE + + "A directory that may contain other files and directories." + DIR + + "A special file, such as a device." + SPECIAL + } + + type DirectoryListing implements Named { + name: String + node_type: FileNodeType + } + + """ + String that identifies permissions on a file or directory. + """ + scalar Permissions + + """ + Directory type. + """ + type Directory implements Named { + name : String + permissions: Permissions + contents( + """ + Wildcard used for matching. + """ + match : String) : [DirectoryListing] + } + + type File implements Named { + name : String + } + + "Stuff that can appear on the file system" + union FileSystemEntry = File | Directory + + type Query { + file(path : String) : FileSystemEntry + } + +} + + diff --git a/docs/schema/parsing.rst b/docs/schema/parsing.rst index 353e3a80..953cc47b 100644 --- a/docs/schema/parsing.rst +++ b/docs/schema/parsing.rst @@ -1,20 +1,13 @@ GraphQL SDL Parsing =================== -.. important:: - The GraphQL Schema Definition Language is not yet a formal specification - and is still under development. As such, this part of Lacinia will continue - to evolve to keep up with new developments, and it's possible that breaking - changes will occur. +.. sidebar:: GraphQL Spec -.. sidebar:: GraphQL SDL - - See the `RFC PR `_ for details - on the GraphQL Schema Definition Language. + Read about :spec:`the Schema Definition Language `. As noted in the :doc:`overview <../overview>`, Lacinia schemas are represented as Clojure data. However, Lacinia also contains a facility to transform schemas -written in the GraphQL Interface Definition Language into the form usable by Lacinia. +written in the GraphQL Schema Definition Language into the form usable by Lacinia. This is exposed by the function ``com.walmartlabs.lacinia.parser.schema/parse-schema``. The Lacinia schema definition includes things which are not available in the SDL, such as @@ -60,6 +53,14 @@ The ``:documentation`` key uses a naming convention on the keys which become pat ``:Query/find_all_in_episode.episode`` applies to the ``episode`` argument, inside the ``find_all_in_episode`` field of the ``Query`` object. +.. tip:: + + Attaching documentation this way is less necessary since release 0.29.0, which added support for + embedded :spec:`schema documentation `. + + Alternately, the documentation map can be parsed from a Markdown file using + ``com.walmartlabs.lacinia.parser.docs/parse-docs``. + The same key structure can be used to document input objects and interfaces. Unions may be documented, but do not contain fields. @@ -71,3 +72,7 @@ are defined on ordinary schema objects, and the ``schema`` element identifies wh which purposes. The ``:roots`` map inside the Lacinia schema is equivalent to the ``schema`` element in the SDL. + +.. warning:: + + :spec:`Schema extensions ` are defined in the GraphQL specification, but not yet implemented. diff --git a/resources/com/walmartlabs/lacinia/schema.g4 b/resources/com/walmartlabs/lacinia/schema.g4 index 85304286..6586da45 100644 --- a/resources/com/walmartlabs/lacinia/schema.g4 +++ b/resources/com/walmartlabs/lacinia/schema.g4 @@ -4,6 +4,11 @@ graphqlSchema : '{' (schemaDef|typeDef|inputTypeDef|unionDef|enumDef|interfaceDef|scalarDef)* '}' ; +description + : StringValue + | BlockStringValue + ; + schemaDef : 'schema' '{' operationTypeDef+ '}' ; @@ -27,7 +32,7 @@ subscriptionOperationDef ; typeDef - : 'type' typeName implementationDef? fieldDefs + : description? 'type' typeName implementationDef? fieldDefs ; fieldDefs @@ -40,19 +45,19 @@ implementationDef ; inputTypeDef - : 'input' typeName fieldDefs + : description? 'input' typeName fieldDefs ; interfaceDef - : 'interface' typeName fieldDefs + : description? 'interface' typeName fieldDefs ; scalarDef - : 'scalar' typeName + : description? 'scalar' typeName ; unionDef - : 'union' typeName '=' unionTypes + : description? 'union' typeName '=' unionTypes ; unionTypes @@ -60,7 +65,15 @@ unionTypes ; enumDef - : 'enum' typeName '{' scalarName+ '}' + : description? 'enum' typeName enumValueDefs + ; + +enumValueDefs + : '{' enumValueDef+ '}' + ; + +enumValueDef + : description? scalarName ; scalarName @@ -68,7 +81,7 @@ scalarName ; fieldDef - : fieldName fieldArgs? ':' typeSpec + : description? fieldName fieldArgs? ':' typeSpec ; fieldArgs @@ -80,7 +93,7 @@ fieldName ; argument - : Name ':' typeSpec defaultValue? + : description? Name ':' typeSpec defaultValue? ; typeSpec diff --git a/src/com/walmartlabs/lacinia/parser/schema.clj b/src/com/walmartlabs/lacinia/parser/schema.clj index c7ebe9b9..d4e12f98 100644 --- a/src/com/walmartlabs/lacinia/parser/schema.clj +++ b/src/com/walmartlabs/lacinia/parser/schema.clj @@ -106,6 +106,11 @@ (-> prod second xform)) +(defn ^:private apply-description + [parsed descripion-prod] + (cond-> parsed + descripion-prod (assoc :description (xform descripion-prod)))) + (defmethod xform :schemaDef [prod] [[:roots] (checked-map "schema entry" (map xform (drop 2 prod)))]) @@ -142,12 +147,18 @@ [prod] (-> prod second keyword)) +(defmethod xform :description + [prod] + (xform-second prod)) + (defmethod xform :typeDef [prod] - (let [{:keys [typeName implementationDef fieldDefs]} (tag prod)] + (let [{:keys [typeName implementationDef fieldDefs description]} (tag prod)] [[:objects (xform typeName)] - (cond-> (common/copy-meta {:fields (xform fieldDefs)} typeName) - implementationDef (assoc :implements (xform implementationDef)))])) + (-> {:fields (xform fieldDefs)} + (common/copy-meta typeName) + (apply-description description) + (cond-> implementationDef (assoc :implements (xform implementationDef))))])) (defmethod xform :fieldDefs [prod] @@ -155,10 +166,12 @@ (defmethod xform :fieldDef [prod] - (let [{:keys [fieldName typeSpec fieldArgs]} (tag prod)] + (let [{:keys [fieldName typeSpec fieldArgs description]} (tag prod)] [(xform fieldName) - (cond-> (common/copy-meta {:type (xform typeSpec)} fieldName) - fieldArgs (assoc :args (xform fieldArgs)))])) + (-> {:type (xform typeSpec)} + (common/copy-meta fieldName) + (apply-description description) + (cond-> fieldArgs (assoc :args (xform fieldArgs))))])) (defmethod xform :fieldArgs [prod] @@ -166,10 +179,12 @@ (defmethod xform :argument [prod] - (let [{:keys [name typeSpec defaultValue]} (tag prod)] + (let [{:keys [name typeSpec defaultValue description]} (tag prod)] [(xform name) - (cond-> (common/copy-meta {:type (xform typeSpec)} name) - defaultValue (assoc :default-value (xform-second defaultValue)))])) + (-> {:type (xform typeSpec)} + (common/copy-meta name) + (apply-description description) + (cond-> defaultValue (assoc :default-value (xform-second defaultValue))))])) (defmethod xform :value [prod] @@ -214,15 +229,19 @@ (defmethod xform :interfaceDef [prod] - (let [[_ _ type fieldDefs] prod] - [[:interfaces (xform type)] - (common/copy-meta {:fields (xform fieldDefs)} type)])) + (let [{:keys [typeName fieldDefs description]} (tag prod)] + [[:interfaces (xform typeName)] + (-> {:fields (xform fieldDefs)} + (common/copy-meta typeName) + (apply-description description))])) (defmethod xform :unionDef [prod] - (let [[_ _ type unionTypes] prod] - [[:unions (xform type)] - (common/copy-meta {:members (xform unionTypes)} type)])) + (let [{:keys [description typeName unionTypes]} (tag prod)] + [[:unions (xform typeName)] + (-> {:members (xform unionTypes)} + (common/copy-meta typeName) + (apply-description description))])) (defmethod xform :unionTypes [prod] @@ -233,11 +252,22 @@ (defmethod xform :enumDef [prod] - (let [[_ _ type & enumValues] prod] - [[:enums (xform type)] - {:values (mapv (fn [prod] - (common/copy-meta {:enum-value (xform prod)} prod)) - enumValues)}])) + (let [{:keys [description typeName enumValueDefs]} (tag prod)] + [[:enums (xform typeName)] + (-> {:values (xform enumValueDefs)} + (common/copy-meta typeName) + (apply-description description))])) + +(defmethod xform :enumValueDefs + [prod] + (mapv xform (rest prod))) + +(defmethod xform :enumValueDef + [prod] + (let [{:keys [description scalarName]} (tag prod)] + (-> {:enum-value (xform scalarName)} + (common/copy-meta scalarName) + (apply-description description)))) (defmethod xform :scalarName [prod] @@ -245,19 +275,23 @@ (defmethod xform :inputTypeDef [prod] - (let [{:keys [typeName fieldDefs]} (tag prod)] + (let [{:keys [typeName fieldDefs description]} (tag prod)] [[:input-objects (xform typeName)] - (common/copy-meta {:fields (xform fieldDefs)} typeName)])) + (-> {:fields (xform fieldDefs)} + (common/copy-meta typeName) + (apply-description description))])) (defmethod xform :scalarDef [prod] - (let [[_ _ typeName] prod] + (let [{:keys [typeName description]} (tag prod)] [[:scalars (xform typeName)] - (common/copy-meta {} typeName)])) + (-> {} + (common/copy-meta typeName) + (apply-description description))])) (defmethod xform :objectValue [prod] - (-> (map xform (rest prod)) + (-> (mapv xform (rest prod)) (as-> % (checked-map "object key" %)) (common/copy-meta prod))) diff --git a/test/com/walmartlabs/lacinia/parser/schema_test.clj b/test/com/walmartlabs/lacinia/parser/schema_test.clj index 2eadeee7..e2707c9b 100644 --- a/test/com/walmartlabs/lacinia/parser/schema_test.clj +++ b/test/com/walmartlabs/lacinia/parser/schema_test.clj @@ -242,3 +242,38 @@ (is (some? input-arg)) (is (= "line 1\n\nline 3\n indented line 4\nline 5" (:default-value input-arg))))) + +(deftest embedded-docs + (let [schema (-> "documented-schema.sdl" + resource + slurp + (parser/parse-schema {}))] + (is (= {:enums {:FileNodeType {:description "File node type." + :values [{:description "A standard file-system file." + :enum-value :FILE} + {:description "A directory that may contain other files and directories." + :enum-value :DIR} + {:description "A special file, such as a device." + :enum-value :SPECIAL}]}} + :interfaces {:Named {:description "Things that have a name." + :fields {:name {:description "The unique name for the Named thing." + :type 'String}}}} + :objects {:Directory {:description "Directory type." + :fields {:contents {:args {:match {:description "Wildcard used for matching." + :type 'String}} + :type '(list :DirectoryListing)} + :name {:type 'String} + :permissions {:type :Permissions}} + :implements [:Named]} + :DirectoryListing {:fields {:name {:type 'String} + :node_type {:type :FileNodeType}} + :implements [:Named]} + :File {:fields {:name {:type 'String}} + :implements [:Named]} + :Query {:fields {:file {:args {:path {:type 'String}} + :type :FileSystemEntry}}}} + :scalars {:Permissions {:description "String that identifies permissions on a file or directory."}} + :unions {:FileSystemEntry {:description "Stuff that can appear on the file system" + :members [:File + :Directory]}}} + schema))))