From 921dca059936c64484e23ff404bb6bf37a64455c Mon Sep 17 00:00:00 2001
From: Mayank Agarwal <mayank1791989@gmail.com>
Date: Sat, 3 Jun 2017 11:43:00 +0530
Subject: [PATCH] feat(QueryParser): add support for interpolation at document
 level.

apollo client uses interpolation to include fragments definitions
see http://dev.apollodata.com/react/fragments.html
---
 src/query/_shared/Parsers/QueryParser.js      | 40 ++++++++++---------
 .../getTokenAtPosition.test.js.snap           | 18 ++++-----
 .../parseQuery_toQueryDocument.test.js.snap   | 14 +++++++
 .../parseQuery_toQueryDocument.test.js        | 23 +++++++++++
 src/query/_shared/parseQuery.js               |  2 +-
 5 files changed, 69 insertions(+), 28 deletions(-)

diff --git a/src/query/_shared/Parsers/QueryParser.js b/src/query/_shared/Parsers/QueryParser.js
index 76ce508..e4111ed 100644
--- a/src/query/_shared/Parsers/QueryParser.js
+++ b/src/query/_shared/Parsers/QueryParser.js
@@ -8,34 +8,36 @@ import {
 import { type Stream, type TokenState } from '../../../shared/types';
 import invariant from 'invariant';
 
-function JSInlineFragment() {
+type InterpolationState = { count: number, style: string };
+
+function Interpolation(style: string) {
   return {
-    style: '',
+    style: '', // NOTE: should be empty
     match(token) {
       return token.value === '${';
     },
     update(state) {
-      state.jsInlineFragment = { count: 1 }; // count is number of open curly braces
+      state.interpolation = { count: 1, style }; // count is number of open curly braces
     },
   };
 }
 
-function eatJSInlineFragment(stream, state) {
-  const { jsInlineFragment: frag } = state;
-  invariant(frag, 'missing JSInlineFragment');
+function eatInterpolation(stream, state) {
+  const { interpolation } = state;
+  invariant(interpolation, 'missing interpolation field in state');
   stream.eatWhile((ch) => {
-    if (frag.count === 0) {
-      state.jsInlineFragment = null;
+    if (interpolation.count === 0) {
+      state.interpolation = null;
       return false;
     }
     if (!ch) {
       return false;
     } // eol
     if (ch === '}') {
-      frag.count -= 1;
+      interpolation.count -= 1;
     }
     if (ch === '{') {
-      frag.count += 1;
+      interpolation.count += 1;
     }
     return true;
   });
@@ -51,9 +53,6 @@ const parserOptions = {
   parseRules: {
     ...ParseRules,
 
-    // relay only one definition per Relay.QL
-    // Document: ['Definition'],
-
     // only query, mutation and fragment possible in Relay.QL
     Definition(token) {
       switch (token.value) {
@@ -65,6 +64,8 @@ const parserOptions = {
           return 'Subscription';
         case 'fragment':
           return 'FragmentDefinition';
+        case '${':
+          return 'DocumentInterpolation';
         default:
           return null;
       }
@@ -77,7 +78,8 @@ const parserOptions = {
       return ParseRules.Selection(token, stream);
     },
 
-    JSInlineFragment: [JSInlineFragment()],
+    JSInlineFragment: [Interpolation('js-frag')],
+    DocumentInterpolation: [Interpolation('ws-2')],
   },
 };
 
@@ -92,11 +94,13 @@ export default class QueryParser {
     return this._parser.startState();
   }
 
-  token(stream: Stream, state: TokenState) {
-    if (state.jsInlineFragment) {
-      eatJSInlineFragment(stream, state);
+  token(stream: Stream, state: TokenState & { interpolation: ?InterpolationState }) {
+    if (state.interpolation) {
+      const { style } = state.interpolation;
+      // NOTE: eatInterpolation mutate both stream and state
+      eatInterpolation(stream, state);
       stream._start -= 2; // to include '${' in token
-      return 'js-frag';
+      return style;
     }
 
     return this._parser.token(stream, state);
diff --git a/src/query/_shared/__tests__/__snapshots__/getTokenAtPosition.test.js.snap b/src/query/_shared/__tests__/__snapshots__/getTokenAtPosition.test.js.snap
index c9d7140..f0969fd 100644
--- a/src/query/_shared/__tests__/__snapshots__/getTokenAtPosition.test.js.snap
+++ b/src/query/_shared/__tests__/__snapshots__/getTokenAtPosition.test.js.snap
@@ -118,7 +118,7 @@ Object {
   "prevChar": "a",
   "start": 78,
   "state": Object {
-    "jsInlineFragment": null,
+    "interpolation": null,
     "kind": "Field",
     "level": 0,
     "levels": Array [
@@ -128,7 +128,7 @@ Object {
     "needsAdvance": true,
     "needsSeperator": false,
     "prevState": Object {
-      "jsInlineFragment": null,
+      "interpolation": null,
       "kind": "Selection",
       "level": 0,
       "levels": Array [
@@ -138,7 +138,7 @@ Object {
       "needsAdvance": false,
       "needsSeperator": false,
       "prevState": Object {
-        "jsInlineFragment": null,
+        "interpolation": null,
         "kind": "SelectionSet",
         "level": 0,
         "levels": Array [
@@ -267,7 +267,7 @@ Object {
   "prevChar": "a",
   "start": 62,
   "state": Object {
-    "jsInlineFragment": null,
+    "interpolation": null,
     "kind": "Field",
     "level": 0,
     "levels": Array [
@@ -277,7 +277,7 @@ Object {
     "needsAdvance": true,
     "needsSeperator": false,
     "prevState": Object {
-      "jsInlineFragment": null,
+      "interpolation": null,
       "kind": "Selection",
       "level": 0,
       "levels": Array [
@@ -287,7 +287,7 @@ Object {
       "needsAdvance": false,
       "needsSeperator": false,
       "prevState": Object {
-        "jsInlineFragment": null,
+        "interpolation": null,
         "kind": "SelectionSet",
         "level": 0,
         "levels": Array [
@@ -416,7 +416,7 @@ Object {
   "prevChar": "a",
   "start": 111,
   "state": Object {
-    "jsInlineFragment": null,
+    "interpolation": null,
     "kind": "Field",
     "level": 0,
     "levels": Array [
@@ -426,7 +426,7 @@ Object {
     "needsAdvance": true,
     "needsSeperator": false,
     "prevState": Object {
-      "jsInlineFragment": null,
+      "interpolation": null,
       "kind": "Selection",
       "level": 0,
       "levels": Array [
@@ -436,7 +436,7 @@ Object {
       "needsAdvance": false,
       "needsSeperator": false,
       "prevState": Object {
-        "jsInlineFragment": null,
+        "interpolation": null,
         "kind": "SelectionSet",
         "level": 0,
         "levels": Array [
diff --git a/src/query/_shared/__tests__/__snapshots__/parseQuery_toQueryDocument.test.js.snap b/src/query/_shared/__tests__/__snapshots__/parseQuery_toQueryDocument.test.js.snap
index eaa046e..be450c9 100644
--- a/src/query/_shared/__tests__/__snapshots__/parseQuery_toQueryDocument.test.js.snap
+++ b/src/query/_shared/__tests__/__snapshots__/parseQuery_toQueryDocument.test.js.snap
@@ -25,6 +25,20 @@ exports[`add dummy fragment name if missing (relay) and \`on\` in next line 1`]
   "
 `;
 
+exports[`allow template string interpolation at document level 1`] = `
+"
+        
+      fragment FeedEntry on Entry {
+        commentCount
+        ...VoteButtons
+        ...RepoInfo
+      }
+                                    
+                                 
+     
+  "
+`;
+
 exports[`extract embedded queries 1`] = `
 "
                 
diff --git a/src/query/_shared/__tests__/parseQuery_toQueryDocument.test.js b/src/query/_shared/__tests__/parseQuery_toQueryDocument.test.js
index d09fb75..885b770 100644
--- a/src/query/_shared/__tests__/parseQuery_toQueryDocument.test.js
+++ b/src/query/_shared/__tests__/parseQuery_toQueryDocument.test.js
@@ -212,3 +212,26 @@ test('replace all irregular whitespace with space', () => {
 
   expect(qd).toMatchSnapshot();
 });
+
+test('allow template string interpolation at document level', () => {
+  // for apollo client which uses interpolation to include child components fragments
+  // see http://dev.apollodata.com/react/fragments.html
+  const text = `
+    gql\`
+      fragment FeedEntry on Entry {
+        commentCount
+        ...VoteButtons
+        ...RepoInfo
+      }
+      \${VoteButtons.fragments.entry}
+      \${RepoInfo.fragments.entry}
+    \`
+  `;
+  const qd = toQueryDocument(
+    new Source(text, 'query.js'),
+    { parser: ['EmbeddedQueryParser', { startTag: 'gql`', endTag: '`' }] },
+  );
+
+  expect(qd).toMatchSnapshot();
+});
+
diff --git a/src/query/_shared/parseQuery.js b/src/query/_shared/parseQuery.js
index 6d58941..bf4b9a9 100644
--- a/src/query/_shared/parseQuery.js
+++ b/src/query/_shared/parseQuery.js
@@ -42,7 +42,7 @@ export function toQueryDocument(source: Source, config: Config): string {
     condition: () => stream.getCurrentPosition() < source.body.length,
     call: () => {
       const style = parser.token(stream, state);
-      // console.log('current', stream.current(), style);
+      // console.log('current', `[${stream.current()}]`, style);
       if ( // add fragment name is missing
         config.isRelay &&
         state.kind === 'TypeCondition' &&