aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.org3
-rw-r--r--kolam/graphql.scm190
-rw-r--r--kolam/parse.scm568
3 files changed, 761 insertions, 0 deletions
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..326ec08
--- /dev/null
+++ b/README.org
@@ -0,0 +1,3 @@
+#+TITLE: kolam
+
+kolam is a [[https://graphql.org/][GraphQL]] implementation for Scheme.
diff --git a/kolam/graphql.scm b/kolam/graphql.scm
new file mode 100644
index 0000000..676caa4
--- /dev/null
+++ b/kolam/graphql.scm
@@ -0,0 +1,190 @@
+;;; kolam --- GraphQL implementation
+;;; Copyright © 2021 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of kolam.
+;;;
+;;; kolam is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU Affero General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; kolam 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
+;;; Affero General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Affero General Public
+;;; License along with kolam. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (kolam graphql)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-19)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-71)
+ #:use-module (ice-9 match)
+ #:export (graphql-evaluator
+ graphql-schema
+ define-object-type
+ define-scalar-type
+ define-enum-type
+ non-nullable-type
+ list-type
+ <integer>
+ <float>
+ <string>
+ <boolean>
+ <id>
+ <date>))
+
+(define-record-type <schema>
+ (make-schema query mutation)
+ schema?
+ (query schema-query)
+ (mutation schema-mutation))
+
+(define* (graphql-schema #:key query mutation)
+ (make-schema query mutation))
+
+(define-record-type <field>
+ (make-field name type resolver)
+ field?
+ (name field-name)
+ (type delayed-field-type)
+ (resolver field-resolver))
+
+(define field-type (compose force delayed-field-type))
+
+(define-record-type <object-type>
+ (make-object-type identifier fields)
+ object-type?
+ (identifier object-type-identifier)
+ (fields object-type-fields))
+
+(define-syntax-rule (define-object-type identifier (name type resolver) ...)
+ (define identifier
+ (make-object-type 'identifier
+ (list (make-field 'name (delay type) resolver) ...))))
+
+(define-record-type <scalar-type>
+ (make-scalar-type identifier predicate serializer)
+ scalar-type?
+ (identifier scalar-type-identifier)
+ (predicate scalar-type-predicate)
+ (serializer scalar-type-serializer))
+
+(define-syntax-rule (define-scalar-type identifier predicate serializer)
+ (define identifier
+ (make-scalar-type 'identifier predicate serializer)))
+
+(define-scalar-type <integer> integer? identity)
+(define-scalar-type <float>
+ (lambda (x)
+ (and (real? x)
+ (not (rational? x))))
+ identity)
+(define-scalar-type <string> string? identity)
+(define-scalar-type <boolean> boolean? identity)
+(define-scalar-type <id> string? identity)
+(define-scalar-type <date> date? (cut date->string <> "~4"))
+
+(define-record-type <non-nullable-type>
+ (non-nullable-type subtype)
+ non-nullable-type?
+ (subtype non-nullable-type-subtype))
+
+(define-record-type <list-type>
+ (list-type subtype)
+ list-type?
+ (subtype list-type-subtype))
+
+(define-record-type <enum-type>
+ (make-enum-type enumerators)
+ enum-type?
+ (enumerators enum-type-enumerators))
+
+(define-syntax-rule (define-enum-type identifier enumerators ...)
+ (define identifier
+ (make-enum-type (list enumerators ...))))
+
+(define (find-field type field)
+ "Find field named FIELD in TYPE."
+ (find (lambda (root-type-field)
+ (eq? (field-name root-type-field)
+ field))
+ (object-type-fields type)))
+
+(define (correct-type? value type)
+ "Return non-#f if VALUE is of GraphQL TYPE. Else, return #f."
+ (cond
+ ((scalar-type? type)
+ ((scalar-type-predicate type) value))
+ ((enum-type? type)
+ (member value
+ (enum-type-enumerators type)))
+ ((non-nullable-type? type)
+ (and (not (eq? value 'null))
+ (correct-type? value (non-nullable-type-subtype type))))
+ ((list-type? type)
+ (and (list? value)
+ (every (cut correct-type? <> (list-type-subtype type))
+ value)))
+ ((object-type? type) #t)
+ (else (error "Unknown type:" type))))
+
+(define (graphql-evaluator schema)
+ (match-lambda
+ (('query tree)
+ (eval-graphql tree (schema-query schema)))
+ (operation (error "Invalid GraphQL operation:" operation))))
+
+(define* (eval-graphql tree parent-type #:optional parent)
+ (let* ((root args children (match tree
+ ((#(root args ...) children ...) (values root args children))
+ ((root children ...) (values root '() children))
+ (#(leaf args ...) (values leaf args #f))
+ (leaf (values leaf '() #f))))
+ ;; TODO: Check if required args are present.
+ (root-field (or (find-field parent-type root)
+ (error "Unknown field:" root)))
+ (root-type (field-type root-field))
+ (underlying-type (if (non-nullable-type? root-type)
+ (non-nullable-type-subtype root-type)
+ root-type))
+ (next-parent (apply (field-resolver root-field)
+ parent #f #f args)))
+ (unless (correct-type? next-parent root-type)
+ (error "Return value of resolver is of unexpected GraphQL type:"
+ next-parent root-type))
+ (when (and (not children)
+ (not (or (scalar-type? underlying-type)
+ (enum-type? underlying-type))))
+ (error "Leaf node must be scalar or enum type:" root-field underlying-type))
+ (cons root
+ (cond
+ ((eq? next-parent 'null) 'null)
+ ;; List of non-object types
+ ((and (list-type? underlying-type)
+ (not (object-type? (list-type-subtype underlying-type))))
+ (list->vector next-parent))
+ ;; List of object types
+ ((list-type? underlying-type)
+ (list->vector
+ (map (lambda (next-parent-element)
+ (map (cut eval-graphql
+ <>
+ (list-type-subtype underlying-type)
+ next-parent-element)
+ children))
+ next-parent)))
+ ;; Non-leaf node
+ (children
+ (map (cut eval-graphql <> underlying-type next-parent)
+ children))
+ ;; Leaf node of a scalar type
+ ((scalar-type? underlying-type)
+ ((scalar-type-serializer underlying-type) next-parent))
+ ;; Leaf node of enum type
+ ((enum-type? underlying-type) next-parent)
+ (else (error "eval-graphql bug: Unknown underlying-type:" underlying-type))))))
diff --git a/kolam/parse.scm b/kolam/parse.scm
new file mode 100644
index 0000000..fa05148
--- /dev/null
+++ b/kolam/parse.scm
@@ -0,0 +1,568 @@
+;;; kolam --- GraphQL implementation
+;;; Copyright © 2021 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of kolam.
+;;;
+;;; kolam is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU Affero General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; kolam 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
+;;; Affero General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Affero General Public
+;;; License along with kolam. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (kolam parse)
+ #:use-module (rnrs io ports)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-71)
+ #:use-module (ice-9 match)
+ #:use-module ((ice-9 textual-ports) #:select (unget-string))
+ #:export (graphql->scm
+ graphql-string->scm))
+
+
+;;; Lexer
+
+;; FIXME: Error out all readers on unexpected end of input.
+(define (read-name port)
+ (call-with-output-string
+ (lambda (out)
+ (put-char out (get-char port))
+ (let loop ()
+ (let ((c (lookahead-char port)))
+ (when (and (not (eof-object? c))
+ (char-set-contains? (char-set-union
+ (char-set #\_)
+ (char-set-intersection char-set:letter+digit
+ char-set:ascii))
+ c))
+ (put-char out (get-char port))
+ (loop)))))))
+
+(define (read-digit-sequence port)
+ (call-with-output-string
+ (lambda (out)
+ (let loop ()
+ (let ((c (lookahead-char port)))
+ (when (and (not (eof-object? c))
+ (char-set-contains? (char-set-intersection char-set:digit
+ char-set:ascii)
+ c))
+ (put-char out (get-char port))
+ (loop)))))))
+
+(define (read-integer-part port)
+ ;; FIXME: 00 is apparently not a valid int-value.
+ (string-append (string (get-char port))
+ (read-digit-sequence port)))
+
+(define (read-escaped-character port)
+ "Read an escaped character from PORT. The port position should be
+after the backslash character that initiated the escaped character."
+ (let ((c (get-char port)))
+ (string-ref
+ (call-with-input-string
+ (string-append (string #\")
+ (case c
+ ;; The forward slash needs to be escaped in
+ ;; GraphQL, but not in Scheme.
+ ((#\/) (string #\/))
+ ;; Usual escape sequences
+ ((#\" #\\ #\b #\f #\n #\r #\t)
+ (string #\\ c))
+ ;; Unicode character
+ ((#\u)
+ (string-append (string #\\ #\u)
+ (get-string-n port 4)))
+ (else (error "Invalid escape sequence character" c)))
+ (string #\"))
+ read)
+ 0)))
+
+(define (lookahead-string-n port count)
+ (let ((str (get-string-n port count)))
+ (unless (eof-object? str)
+ (unget-string port str))
+ str))
+
+(define (read-string port)
+ (get-char port)
+ (call-with-output-string
+ (lambda (out)
+ (let loop ()
+ (let ((c (get-char port)))
+ (unless (eof-object? c)
+ (cond
+ ((char-set-contains? (char-set #\newline #\return)
+ c)
+ (error "Unexpected line terminator in string"))
+ ((char=? c #\\)
+ (put-char out (read-escaped-character port))
+ (loop))
+ ((not (char=? c #\"))
+ (put-char out c)
+ (loop)))))))))
+
+(define (string-blank? str)
+ "Return non-#f if STR contains only whitespace characters. Else,
+return #f."
+ (string-every char-set:whitespace str))
+
+(define (drop-right-while pred lst)
+ "Drop the longest suffix of LST whose elements all satisfy PRED."
+ (reverse (drop-while pred (reverse lst))))
+
+(define (block-string-value str)
+ (let* ((trimmed-lines (drop-right-while string-blank?
+ (drop-while string-blank?
+ (string-split str #\newline))))
+ (common-indent
+ (match trimmed-lines
+ ((first-line other-lines ...)
+ (fold (lambda (line result)
+ (if (string-blank? line)
+ result
+ (min result (string-index line
+ (char-set-complement char-set:whitespace)))))
+ (string-index first-line (char-set-complement char-set:whitespace))
+ other-lines)))))
+ (string-join (map (lambda (line)
+ (if (string-blank? line)
+ line
+ (substring line common-indent)))
+ trimmed-lines)
+ "\n")))
+
+(define (read-block-string port)
+ (get-string-n port 3)
+ (call-with-output-string
+ (lambda (out)
+ (let loop ()
+ (cond
+ ;; End of blockstring
+ ((or (eof-object? (lookahead-string-n port 3))
+ (string=? (lookahead-string-n port 3)
+ "\"\"\""))
+ (get-string-n port 3))
+ ;; Escaped triple quote
+ ((string=? (lookahead-string-n port 4)
+ "\\\"\"\"")
+ (get-string-n port 4)
+ (put-string out "\"\"\"")
+ (loop))
+ (else
+ (put-char out (get-char port))
+ (loop)))))))
+
+(define (make-lexer port)
+ (lambda ()
+ (let loop ()
+ (let ((c (lookahead-char port)))
+ (cond
+ ((eof-object? c) c)
+ ;; Ignored
+ ((char-set-contains? (char-set (integer->char #xFEFF)
+ #\tab #\space #\newline #\return #\,)
+ c)
+ (get-char port)
+ (loop))
+ ;; Comments
+ ((char=? c #\#)
+ (get-line port)
+ (when (eq? (lookahead-char port) #\return)
+ (get-char port))
+ (loop))
+ ;; Punctuator
+ ((char-set-contains? (char-set #\! #\$ #\& #\( #\)
+ #\… #\: #\= #\@ #\[ #\]
+ #\{ #\| #\})
+ c)
+ (cons 'punctuator (get-char port)))
+ ;; Name
+ ((char-set-contains? (char-set-union
+ (char-set #\_)
+ (char-set-intersection char-set:letter
+ char-set:ascii))
+ c)
+ (cons 'name (read-name port)))
+ ;; Numbers
+ ((char-set-contains? (char-set-union
+ (char-set #\-)
+ (char-set-intersection char-set:digit
+ char-set:ascii))
+ c)
+ (let ((integer-part (read-integer-part port))
+ (fractional-part (case (lookahead-char port)
+ ((#\.)
+ (get-char port)
+ (read-digit-sequence port))
+ (else #f)))
+ (exponential-part (case (lookahead-char port)
+ ((#\e #\E)
+ (get-char port)
+ (read-integer-part port))
+ (else #f))))
+ (cond
+ ((and (not fractional-part)
+ (not exponential-part))
+ (cons 'int-value integer-part))
+ (else
+ (cons 'float-value
+ (string-append integer-part
+ (if fractional-part
+ (string-append "." fractional-part)
+ "")
+ (if exponential-part
+ (string-append "e" exponential-part)
+ "")))))))
+ ;; String value
+ ((eq? c #\")
+ (cons 'string-value
+ (if (string=? (lookahead-string-n port 3)
+ "\"\"\"")
+ (block-string-value
+ (read-block-string port))
+ (read-string port))))
+ (else (error "kolam lexer bug, unidentified token at character:" c)))))))
+
+
+;;; Parser
+;;;
+;;; Every parser function (*-pattern functions) is a procedure that
+;;; takes a list of tokens as an argument and returns the result of the
+;;; match and the remaining tokens. The result of the match must be #f
+;;; or a pair.
+;;;
+;;; If the match was unsuccessful, the result of the match must be
+;;; #f. Else, it must be a pair, with the first element being the
+;;; symbol identifying the matched object, and the second element being
+;;; the object itself.
+
+(define-record-type <optional>
+ (make-optional)
+ optional?)
+
+(define %optional (make-optional))
+
+(define-syntax pattern-optional
+ (syntax-rules ()
+ ((_ proc pattern)
+ (pattern-optional-function (delay pattern) proc))
+ ((_ pattern)
+ (pattern-optional-function (delay pattern)))))
+
+(define* (pattern-optional-function pattern #:optional proc)
+ (lambda (tokens)
+ (let ((matched remaining-tokens ((force pattern) tokens)))
+ (if matched
+ (values (if proc
+ (proc (strip-token-id matched))
+ matched)
+ remaining-tokens)
+ (values (cons 'optional %optional)
+ tokens)))))
+
+(define* (many-collect pattern tokens #:optional (result '()))
+ (if (null? tokens)
+ (values (reverse result) tokens)
+ (let ((matched remaining-tokens (pattern tokens)))
+ (if matched
+ (many-collect pattern remaining-tokens (cons matched result))
+ (values (reverse result) tokens)))))
+
+(define strip-token-id
+ (match-lambda
+ ((_ . token-value) token-value)
+ (token (error "Invalid token:" token))))
+
+(define-syntax-rule (pattern-many proc pattern)
+ (pattern-many-function proc (delay pattern)))
+
+(define* (pattern-many-function proc pattern)
+ (lambda (tokens)
+ (let ((matched remaining-tokens (many-collect (force pattern) tokens)))
+ (values (proc (map strip-token-id matched))
+ remaining-tokens))))
+
+(define* (and-collect patterns tokens #:optional (result '()))
+ (match patterns
+ (() (values (reverse result) tokens))
+ ((pattern remaining-patterns ...)
+ (let ((matched remaining-tokens (pattern tokens)))
+ (if matched
+ (and-collect remaining-patterns remaining-tokens (cons matched result))
+ (values #f tokens))))))
+
+(define-syntax-rule (pattern-and proc patterns ...)
+ (pattern-and-function proc (delay patterns) ...))
+
+(define (pattern-and-function proc . patterns)
+ (lambda (tokens)
+ (let ((matched remaining-tokens (and-collect (map force patterns)
+ tokens)))
+ (if matched
+ (values (apply proc (map strip-token-id matched))
+ remaining-tokens)
+ (values #f tokens)))))
+
+(define* (or-collect patterns tokens #:optional (result '()))
+ (match patterns
+ (() (values #f tokens))
+ ((pattern remaining-patterns ...)
+ (let ((matched remaining-tokens (pattern tokens)))
+ (if matched
+ (values matched remaining-tokens)
+ (or-collect remaining-patterns tokens (cons matched result)))))))
+
+(define-syntax-rule (pattern-or proc patterns ...)
+ (pattern-or-function proc (delay patterns) ...))
+
+(define (pattern-or-function proc . patterns)
+ (lambda (tokens)
+ (let ((matched remaining-tokens (or-collect (map force patterns) tokens)))
+ (if matched
+ (values (proc matched)
+ remaining-tokens)
+ (values #f tokens)))))
+
+(define-syntax-rule (pattern-not pattern)
+ (pattern-not-function (delay pattern)))
+
+(define (pattern-not-function pattern)
+ (lambda (tokens)
+ (let ((matched remaining-tokens ((force pattern) tokens)))
+ (values (and (not matched)
+ (cons 'negation 'null))
+ tokens))))
+
+(define (token-pattern token)
+ (lambda (tokens)
+ (match tokens
+ (((head-id . head-value) tail ...)
+ (if (eq? head-id token)
+ (values (cons head-id head-value)
+ tail)
+ (values #f tokens)))
+ (() (values #f tokens)))))
+
+(define (token-value-pattern token-id token-value)
+ (lambda (tokens)
+ (match tokens
+ ((head tail ...)
+ (if (equal? head (cons token-id token-value))
+ (values head tail)
+ (values #f tokens)))
+ (() (values #f tokens)))))
+
+(define variable-pattern
+ (pattern-and (lambda (dollar name)
+ (cons 'variable name))
+ (token-value-pattern 'punctuator #\$)
+ (token-pattern 'name)))
+
+(define alias-pattern
+ (pattern-and (lambda (name colon)
+ (cons 'alias (string->symbol name)))
+ (token-pattern 'name)
+ (token-value-pattern 'punctuator #\:)))
+
+(define operation-type-pattern
+ (pattern-or (match-lambda
+ (('name . value)
+ (cons 'operation-type (string->symbol value))))
+ (token-value-pattern 'name "query")
+ (token-value-pattern 'name "mutation")
+ (token-value-pattern 'name "subscription")))
+
+(define selection-set-pattern
+ (pattern-and (lambda (open selections close)
+ (cons 'selection-set selections))
+ (token-value-pattern 'punctuator #\{)
+ (pattern-many (cut cons 'selections <>) selection-pattern)
+ (token-value-pattern 'punctuator #\})))
+
+(define selection-pattern
+ (pattern-or (match-lambda
+ (('field . field)
+ (cons 'selection field)))
+ field-pattern
+ ;; TODO: Implement fragments.
+ ;; fragment-spread-pattern
+ ;; inline-fragment-pattern
+ ))
+
+(define field-pattern
+ (pattern-and (lambda (alias name arguments selection-set)
+ (let* ((name (string->symbol name))
+ (arguments (if (optional? arguments)
+ (list)
+ arguments))
+ (main-node
+ (match (if (optional? alias)
+ arguments
+ (cons alias arguments))
+ (() name)
+ (alias-and-arguments (apply vector name alias-and-arguments)))))
+ (cons 'field
+ (if (optional? selection-set)
+ main-node
+ (cons main-node selection-set)))))
+ (pattern-optional alias-pattern)
+ (token-pattern 'name)
+ (pattern-optional arguments-pattern)
+ ;; TODO: Implement directives.
+ ;; (pattern-optional directives-pattern)
+ (pattern-optional selection-set-pattern)))
+
+(define arguments-pattern
+ (pattern-and (lambda (open arguments close)
+ (cons 'arguments (apply append arguments)))
+ (token-value-pattern 'punctuator #\()
+ (pattern-many (cut cons 'arguments <>) argument-pattern)
+ (token-value-pattern 'punctuator #\))))
+
+(define argument-pattern
+ (pattern-and (lambda (name colon value)
+ (cons 'argument (list (symbol->keyword (string->symbol name))
+ value)))
+ (token-pattern 'name)
+ (token-value-pattern 'punctuator #\:)
+ value-pattern))
+
+(define boolean-value-pattern
+ (pattern-or (match-lambda
+ (('name . value)
+ (cons 'boolean-value value)))
+ (token-value-pattern 'name "true")
+ (token-value-pattern 'name "false")))
+
+(define null-value-pattern
+ (pattern-or (match-lambda
+ (('name . "null")
+ (cons 'null-value "null")))
+ (token-value-pattern 'name "null")))
+
+(define enum-value-pattern
+ (pattern-and (lambda (not1 not2 name)
+ (cons 'enum-value name))
+ (pattern-not boolean-value-pattern)
+ (pattern-not null-value-pattern)
+ (token-pattern 'name)))
+
+(define list-value-pattern
+ (pattern-or identity
+ (pattern-and (lambda _
+ (cons 'list-value (list)))
+ (token-value-pattern 'punctuator #\[)
+ (token-value-pattern 'punctuator #\]))
+ (pattern-and (lambda (open values close)
+ (cons 'list-value values))
+ (token-value-pattern 'punctuator #\[)
+ (pattern-many (cut cons 'values <>) value-pattern)
+ (token-value-pattern 'punctuator #\]))))
+
+(define object-value-pattern
+ (pattern-or identity
+ (pattern-and (lambda _
+ (cons 'object-value (list)))
+ (token-value-pattern 'punctuator #\{)
+ (token-value-pattern 'punctuator #\}))
+ (pattern-and (lambda (open object-fields close)
+ (cons 'object-value object-fields))
+ (token-value-pattern 'punctuator #\{)
+ (pattern-many (cut cons 'object-fields <>) object-field-pattern)
+ (token-value-pattern 'punctuator #\}))))
+
+(define object-field-pattern
+ (pattern-and (lambda (name colon value)
+ (list 'object-field name value))
+ (token-pattern 'name)
+ (token-value-pattern 'punctuator #\:)
+ (token-pattern 'value)))
+
+(define value-pattern
+ (pattern-or (match-lambda
+ (((or 'int-value 'float-value) . value)
+ (cons 'value (string->number value)))
+ ((_ . value)
+ (cons 'value value)))
+ variable-pattern
+ (token-pattern 'int-value)
+ (token-pattern 'float-value)
+ (token-pattern 'string-value)
+ boolean-value-pattern
+ null-value-pattern
+ enum-value-pattern
+ list-value-pattern
+ object-value-pattern))
+
+(define operation-definition-pattern
+ (pattern-or (match-lambda
+ (('operation-definition . operation-definition)
+ (cons 'operation-definition operation-definition))
+ (('selection-set . selection-set)
+ (cons 'operation-definition
+ (cons 'query selection-set))))
+ (pattern-and (lambda (operation-type name selection-set)
+ ;; TODO: Use name.
+ (cons 'operation-definition
+ (cons operation-type selection-set)))
+ operation-type-pattern
+ (pattern-optional (token-pattern 'name))
+ ;; TODO: Implement variables and directives.
+ ;; (pattern-optional variable-definitions-pattern)
+ ;; (pattern-optional directives-pattern)
+ selection-set-pattern)
+ selection-set-pattern))
+
+(define executable-definition-pattern
+ (pattern-or (match-lambda
+ (('operation-definition . operation-definition)
+ (cons 'executable-definition operation-definition)))
+ operation-definition-pattern
+ ;; TODO: Implement fragments.
+ ;; fragment-definition-pattern
+ ))
+
+(define executable-document-pattern
+ (pattern-many (lambda (executable-definitions)
+ (cons 'executable-document executable-definitions))
+ executable-definition-pattern))
+
+(define definition-pattern
+ (pattern-or (match-lambda
+ (('executable-definition . executable-definition)
+ (cons 'definition executable-definition)))
+ executable-definition-pattern))
+
+(define document-pattern
+ (pattern-many (lambda (definitions)
+ (cons 'document definitions))
+ definition-pattern))
+
+
+;;; Public interface and other functions
+
+(define (drain-lexer lexer)
+ (let ((token (lexer)))
+ (if (eof-object? token)
+ '()
+ (cons token (drain-lexer lexer)))))
+
+(define (graphql->scm port)
+ (let ((matched remaining-tokens (document-pattern (drain-lexer (make-lexer port)))))
+ (if (null? remaining-tokens)
+ matched
+ (error "Expected end of GraphQL document"))))
+
+(define (graphql-string->scm str)
+ (call-with-input-string str
+ graphql->scm))