about summary refs log tree commit diff
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))