parse.lua 7.5 KB

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