1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
|
;;; graphql.el --- GraphQL utilities -*- lexical-binding: t; -*-
;; Copyright (C) 2017 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
;; Keywords: hypermedia, tools, lisp
;; Homepage: https://github.com/vermiculus/graphql.el
;; Package-Version: 0.1.1
;; Package-Requires: ((emacs "25"))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; GraphQL.el provides a generally-applicable domain-specific language
;; for creating and executing GraphQL queries against your favorite
;; web services.
;;; Code:
(require 'pcase)
(defun graphql--encode-object (obj)
"Encode OBJ as a GraphQL string."
(cond
((stringp obj)
obj)
((symbolp obj)
(symbol-name obj))
((numberp obj)
(number-to-string obj))
((and (consp obj)
(not (consp (cdr obj))))
(symbol-name (car obj)))))
(defun graphql--encode-argument-spec (spec)
"Encode an argument spec SPEC.
SPEC is of the form..."
(graphql--encode-argument (car spec) (cdr spec)))
(defun graphql--encode-argument (key value)
"Encode an argument KEY with value VALUE."
(format "%s:%s" key (graphql--encode-argument-value value)))
(defun graphql--encode-argument-value (value)
"Encode an argument value VALUE.
VALUE is expected to be one of the following:
* a symbol
* a 'variable', i.e. \\='($ variableName)
* an object (as a list)
* a string
* a vector of values (e.g., symbols)
* a number
* something encode-able by `graphql-encode'."
(cond
((symbolp value)
(symbol-name value))
((eq '$ (car-safe value))
(format "$%s" (cadr value)))
((listp value)
(format "{%s}" (mapconcat #'graphql--encode-argument-spec value ",")))
((stringp value)
(format "\"%s\"" value))
((vectorp value)
(format "[%s]" (mapconcat #'graphql-encode value ",")))
((numberp value)
(number-to-string value))
(t
(graphql-encode value))))
(defun graphql--encode-parameter-spec (spec)
"Encode a parameter SPEC.
SPEC is expected to be of the following form:
(NAME TYPE [REQUIRED] . [DEFAULT])
NAME is the name of the parameter.
TYPE is the parameter's type.
A non-nil value for REQUIRED will indicate the parameter is
required. A value of `!' is recommended.
A non-nil value for DEFAULT will provide a default value for the
parameter."
;; Unfortunately can't use `pcase' here because the first DEFAULT
;; value (in the case of a complex value) might be misunderstood as
;; the value for REQUIRED. We need to know if the third cons is the
;; very last one; not just that the list has at least three
;; elements.
(if (eq (last spec) (nthcdr 2 spec))
(graphql--encode-parameter (nth 0 spec)
(nth 1 spec)
(car (last spec))
(cdr (last spec)))
(graphql--encode-parameter (nth 0 spec)
(nth 1 spec)
nil
(nthcdr 2 spec))))
(defun graphql--encode-parameter (name type &optional required default)
"Encode a GraphQL parameter with a NAME and TYPE.
If REQUIRED is non-nil, mark the parameter as required.
If DEFAULT is non-nil, is the default value of the parameter."
(format "$%s:%s%s%s"
(symbol-name name)
(symbol-name type)
(if required "!" "")
(if default
(concat "=" (graphql--encode-argument-value default))
"")))
(defun graphql--get-keys (g)
"Get the keyword arguments from a graph G.
Returns a list where the first element is a plist of arguments
and the second is a 'clean' copy of G."
(or (and (not (consp g))
(list nil g))
(let (graph keys)
(while g
(if (keywordp (car g))
(let* ((param (pop g))
(value (pop g)))
(push (cons param value) keys))
(push (pop g) graph)))
(list keys (nreverse graph)))))
(defun graphql-encode (g)
"Encode graph G as a GraphQL string."
(pcase (graphql--get-keys g)
(`(,keys ,graph)
(let ((object (or (car-safe graph) graph))
(name (alist-get :op-name keys))
(params (alist-get :op-params keys))
(arguments (alist-get :arguments keys))
(fields (cdr-safe graph)))
(concat
(graphql--encode-object object)
(when name
(format " %S" name))
(when arguments
;; Format arguments "key:value,key:value,..."
(format "(%s)"
(mapconcat #'graphql--encode-argument-spec arguments ",")))
(when params
(format "(%s)"
(mapconcat #'graphql--encode-parameter-spec params ",")))
(when fields
(format "{%s}"
(mapconcat #'graphql-encode fields " "))))))))
(defun graphql-simplify-response-edges (data)
"Simplify DATA to collapse edges into their nodes."
(pcase data
;; When we encounter a collection of edges, simplify those edges
;; into their nodes
(`(,object (edges . ,edges))
(cons object (mapcar #'graphql-simplify-response-edges
(mapcar (lambda (edge) (alist-get 'node edge))
edges))))
;; When we encounter a plain cons cell (not a list), let it pass
(`(,(and key (guard (not (consp key)))) . ,(and value (guard (not (consp value)))))
(cons key value))
;; symbols should pass unaltered
(`,(and symbol (guard (symbolp symbol)))
symbol)
;; everything else should be mapped
(_ (mapcar #'graphql-simplify-response-edges data))))
(defun graphql--genform-operation (args kind)
"Generate the Lisp form for an operation.
ARGS is is a list ([NAME [PARAMETERS]] GRAPH) where NAME is the
name of the operation, PARAMETERS are its parameters, and GRAPH
is the form of the actual operation.
KIND can be `query' or `mutation'."
(pcase args
(`(,name ,parameters ,graph)
`(graphql-encode '(,kind :op-name ,name
:op-params ,parameters
,@graph)))
(`(,name ,graph)
`(graphql-encode '(,kind :op-name ,name
,@graph)))
(`(,graph)
`(graphql-encode '(,kind ,@graph)))
(_ (error "Bad form"))))
(defmacro graphql-query (&rest args)
"Construct a Query object.
ARGS is a listof the form described by `graphql--genform-operation'.
\(fn [NAME] [(PARAMETER-SPEC...)] GRAPH)"
(graphql--genform-operation args 'query))
(defmacro graphql-mutation (&rest args)
"Construct a Mutation object.
ARGS is a listof the form described by `graphql--genform-operation'.
\(fn [NAME] [(PARAMETER-SPEC...)] GRAPH)"
(graphql--genform-operation args 'mutation))
(provide 'graphql)
;;; graphql.el ends here
|