parse.lua 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. local lpeg = require 'lpeg'
  2. local P, R, S, V, C, Ct, Cmt, Cg, Cc, Cf, Cmt = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.C, lpeg.Ct, lpeg.Cmt, lpeg.Cg, lpeg.Cc, lpeg.Cf, lpeg.Cmt
  3. local line
  4. local lastLinePos
  5. local function pack(...)
  6. return { n = select('#', ...), ... }
  7. end
  8. -- Utility
  9. local ws = Cmt(S(' \t\r\n') ^ 0, function(str, pos)
  10. str = str:sub(lastLinePos, pos)
  11. while str:find('\n') do
  12. line = line + 1
  13. lastLinePos = pos
  14. str = str:sub(str:find('\n') + 1)
  15. end
  16. return true
  17. end)
  18. local comma = P(',') ^ 0
  19. local _ = V
  20. local function maybe(pattern)
  21. if type(pattern) == 'string' then pattern = V(pattern) end
  22. return pattern ^ -1
  23. end
  24. local function list(pattern, min)
  25. if type(pattern) == 'string' then pattern = V(pattern) end
  26. min = min or 0
  27. return Ct((pattern * comma * ws) ^ min)
  28. end
  29. -- Formatters
  30. local function simpleValue(key)
  31. return function(value)
  32. return {
  33. kind = key,
  34. value = value
  35. }
  36. end
  37. end
  38. local cName = simpleValue('name')
  39. local cInt = simpleValue('int')
  40. local cFloat = simpleValue('float')
  41. local cBoolean = simpleValue('boolean')
  42. local cString = simpleValue('string')
  43. local cEnum = simpleValue('enum')
  44. local function cList(value)
  45. return {
  46. kind = 'list',
  47. values = value
  48. }
  49. end
  50. local function cObjectField(name, value)
  51. return {
  52. name = name,
  53. value = value
  54. }
  55. end
  56. local function cObject(fields)
  57. return {
  58. kind = 'inputObject',
  59. values = fields
  60. }
  61. end
  62. local function cAlias(name)
  63. return {
  64. kind = 'alias',
  65. name = name
  66. }
  67. end
  68. local function cArgument(name, value)
  69. return {
  70. kind = 'argument',
  71. name = name,
  72. value = value
  73. }
  74. end
  75. local function cField(...)
  76. local tokens = pack(...)
  77. local field = { kind = 'field' }
  78. for i = 1, #tokens do
  79. local key = tokens[i].kind
  80. if not key then
  81. if tokens[i][1].kind == 'argument' then
  82. key = 'arguments'
  83. elseif tokens[i][1].kind == 'directive' then
  84. key = 'directives'
  85. end
  86. end
  87. field[key] = tokens[i]
  88. end
  89. return field
  90. end
  91. local function cSelectionSet(selections)
  92. return {
  93. kind = 'selectionSet',
  94. selections = selections
  95. }
  96. end
  97. local function cFragmentSpread(name)
  98. return {
  99. kind = 'fragmentSpread',
  100. name = name
  101. }
  102. end
  103. local function cOperation(...)
  104. local args = pack(...)
  105. if args[1].kind == 'selectionSet' then
  106. return {
  107. kind = 'operation',
  108. operation = 'query',
  109. selectionSet = args[1]
  110. }
  111. else
  112. local result = {
  113. kind = 'operation',
  114. operation = args[1]
  115. }
  116. for i = 2, #args do
  117. local key = args[i].kind
  118. if not key then
  119. if args[i][1].kind == 'variableDefinition' then
  120. key = 'variableDefinitions'
  121. elseif args[i][1].kind == 'directive' then
  122. key = 'directives'
  123. end
  124. end
  125. result[key] = args[i]
  126. end
  127. return result
  128. end
  129. end
  130. local function cDocument(definitions)
  131. return {
  132. kind = 'document',
  133. definitions = definitions
  134. }
  135. end
  136. local function cFragmentDefinition(name, typeCondition, selectionSet)
  137. return {
  138. kind = 'fragmentDefinition',
  139. name = name,
  140. typeCondition = typeCondition,
  141. selectionSet = selectionSet
  142. }
  143. end
  144. local function cNamedType(name)
  145. return {
  146. kind = 'namedType',
  147. name = name
  148. }
  149. end
  150. local function cListType(type)
  151. return {
  152. kind = 'listType',
  153. type = type
  154. }
  155. end
  156. local function cNonNullType(type)
  157. return {
  158. kind = 'nonNullType',
  159. type = type
  160. }
  161. end
  162. local function cInlineFragment(...)
  163. local args = pack(...)
  164. if #args == 2 then
  165. return {
  166. kind = 'inlineFragment',
  167. typeCondition = args[1],
  168. selectionSet = args[2]
  169. }
  170. elseif #args == 1 then
  171. return {
  172. kind = 'inlineFragment',
  173. selectionSet = args[1]
  174. }
  175. end
  176. end
  177. local function cVariable(name)
  178. return {
  179. kind = 'variable',
  180. name = name
  181. }
  182. end
  183. local function cVariableDefinition(variable, type, defaultValue)
  184. return {
  185. kind = 'variableDefinition',
  186. variable = variable,
  187. type = type,
  188. defaultValue = defaultValue
  189. }
  190. end
  191. local function cDirective(name, arguments)
  192. return {
  193. kind = 'directive',
  194. name = name,
  195. arguments = arguments
  196. }
  197. end
  198. -- Simple types
  199. local rawName = R('az', 'AZ') * (P'_' + R'09' + R('az', 'AZ')) ^ 0
  200. local name = rawName / cName
  201. local fragmentName = (rawName - 'on') / cName
  202. local alias = ws * name * P':' * ws / cAlias
  203. local integerPart = P'-' ^ -1 * ('0' + R'19' * R'09' ^ 0)
  204. local intValue = integerPart / cInt
  205. local fractionalPart = '.' * R'09' ^ 1
  206. local exponentialPart = S'Ee' * S'+-' ^ -1 * R'09' ^ 1
  207. local floatValue = integerPart * (fractionalPart + exponentialPart + (fractionalPart * exponentialPart)) / cFloat
  208. local booleanValue = (P'true' + P'false') / cBoolean
  209. local stringValue = P'"' * C((P'\\"' + 1 - S'"\n') ^ 0) * P'"' / cString
  210. local enumValue = (rawName - 'true' - 'false' - 'null') / cEnum
  211. local variable = ws * '$' * name / cVariable
  212. -- Grammar
  213. local graphQL = P {
  214. 'document',
  215. document = ws * list('definition') / cDocument * -1,
  216. definition = _'operation' + _'fragmentDefinition',
  217. operationType = C(P'query' + P'mutation'),
  218. operation = (_'operationType' * ws * maybe(name) * maybe('variableDefinitions') * maybe('directives') * _'selectionSet' + _'selectionSet') / cOperation,
  219. fragmentDefinition = 'fragment' * ws * fragmentName * ws * _'typeCondition' * ws * _'selectionSet' / cFragmentDefinition,
  220. selectionSet = ws * '{' * ws * list('selection') * ws * '}' / cSelectionSet,
  221. selection = ws * (_'field' + _'fragmentSpread' + _'inlineFragment'),
  222. field = ws * maybe(alias) * name * maybe('arguments') * maybe('directives') * maybe('selectionSet') / cField,
  223. fragmentSpread = ws * '...' * fragmentName / cFragmentSpread,
  224. inlineFragment = ws * '...' * ws * maybe('typeCondition') * _'selectionSet' / cInlineFragment,
  225. typeCondition = 'on' * ws * _'namedType',
  226. argument = ws * name * ':' * _'value' / cArgument,
  227. arguments = '(' * list('argument', 1) * ')',
  228. directive = '@' * name * maybe('arguments') / cDirective,
  229. directives = ws * list('directive', 1) * ws,
  230. variableDefinition = ws * variable * ws * ':' * ws * _'type' * (ws * '=' * _'value') ^ -1 * comma * ws / cVariableDefinition,
  231. variableDefinitions = '(' * list('variableDefinition', 1) * ')',
  232. value = ws * (variable + _'objectValue' + _'listValue' + enumValue + stringValue + booleanValue + floatValue + intValue),
  233. listValue = '[' * list('value') * ']' / cList,
  234. objectFieldValue = ws * C(rawName) * ws * ':' * ws * _'value' * comma / cObjectField,
  235. objectValue = '{' * ws * list('objectFieldValue') * ws * '}' / cObject,
  236. type = _'nonNullType' + _'listType' + _'namedType',
  237. namedType = name / cNamedType,
  238. listType = '[' * ws * _'type' * ws * ']' / cListType,
  239. nonNullType = (_'namedType' + _'listType') * '!' / cNonNullType
  240. }
  241. -- TODO doesn't handle quotes that immediately follow escaped backslashes.
  242. local function stripComments(str)
  243. return (str .. '\n'):gsub('(.-\n)', function(line)
  244. local index = 1
  245. while line:find('#', index) do
  246. local pos = line:find('#', index) - 1
  247. local chunk = line:sub(1, pos)
  248. local _, quotes = chunk:gsub('([^\\]")', '')
  249. if quotes % 2 == 0 then
  250. return chunk .. '\n'
  251. else
  252. index = pos + 2
  253. end
  254. end
  255. return line
  256. end)
  257. end
  258. return function(str)
  259. assert(type(str) == 'string', 'parser expects a string')
  260. str = stripComments(str)
  261. line, lastLinePos = 1, 1
  262. return graphQL:match(str) or error('Syntax error near line ' .. line, 2)
  263. end