graphql 1.7.6 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/function_generator.rb +1 -1
  3. data/lib/generators/graphql/install_generator.rb +14 -8
  4. data/lib/generators/graphql/loader_generator.rb +1 -1
  5. data/lib/generators/graphql/mutation_generator.rb +6 -1
  6. data/lib/generators/graphql/templates/function.erb +2 -2
  7. data/lib/generators/graphql/templates/loader.erb +2 -2
  8. data/lib/generators/graphql/templates/schema.erb +1 -1
  9. data/lib/graphql/argument.rb +25 -19
  10. data/lib/graphql/backtrace/tracer.rb +16 -22
  11. data/lib/graphql/backtrace.rb +1 -1
  12. data/lib/graphql/backwards_compatibility.rb +2 -3
  13. data/lib/graphql/base_type.rb +31 -31
  14. data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +14 -0
  15. data/lib/graphql/compatibility/query_parser_specification.rb +117 -0
  16. data/lib/graphql/define/assign_object_field.rb +5 -12
  17. data/lib/graphql/deprecated_dsl.rb +42 -0
  18. data/lib/graphql/directive.rb +1 -0
  19. data/lib/graphql/enum_type.rb +3 -1
  20. data/lib/graphql/execution/execute.rb +21 -13
  21. data/lib/graphql/execution/instrumentation.rb +82 -0
  22. data/lib/graphql/execution/lazy/lazy_method_map.rb +1 -1
  23. data/lib/graphql/execution/lazy/resolve.rb +1 -3
  24. data/lib/graphql/execution/multiplex.rb +12 -29
  25. data/lib/graphql/execution.rb +1 -0
  26. data/lib/graphql/field.rb +21 -4
  27. data/lib/graphql/function.rb +14 -0
  28. data/lib/graphql/input_object_type.rb +3 -1
  29. data/lib/graphql/interface_type.rb +5 -3
  30. data/lib/graphql/internal_representation/node.rb +26 -14
  31. data/lib/graphql/internal_representation/visit.rb +3 -6
  32. data/lib/graphql/introspection/base_object.rb +16 -0
  33. data/lib/graphql/introspection/directive_location_enum.rb +11 -7
  34. data/lib/graphql/introspection/directive_type.rb +23 -16
  35. data/lib/graphql/introspection/dynamic_fields.rb +11 -0
  36. data/lib/graphql/introspection/entry_points.rb +29 -0
  37. data/lib/graphql/introspection/enum_value_type.rb +16 -11
  38. data/lib/graphql/introspection/field_type.rb +21 -12
  39. data/lib/graphql/introspection/input_value_type.rb +26 -23
  40. data/lib/graphql/introspection/schema_field.rb +7 -2
  41. data/lib/graphql/introspection/schema_type.rb +36 -22
  42. data/lib/graphql/introspection/type_by_name_field.rb +10 -2
  43. data/lib/graphql/introspection/type_kind_enum.rb +10 -6
  44. data/lib/graphql/introspection/type_type.rb +85 -23
  45. data/lib/graphql/introspection/typename_field.rb +1 -0
  46. data/lib/graphql/introspection.rb +3 -10
  47. data/lib/graphql/language/block_string.rb +47 -0
  48. data/lib/graphql/language/document_from_schema_definition.rb +280 -0
  49. data/lib/graphql/language/generation.rb +3 -182
  50. data/lib/graphql/language/lexer.rb +144 -69
  51. data/lib/graphql/language/lexer.rl +15 -4
  52. data/lib/graphql/language/nodes.rb +141 -78
  53. data/lib/graphql/language/parser.rb +677 -630
  54. data/lib/graphql/language/parser.y +18 -12
  55. data/lib/graphql/language/printer.rb +361 -0
  56. data/lib/graphql/language/token.rb +10 -3
  57. data/lib/graphql/language.rb +3 -0
  58. data/lib/graphql/non_null_type.rb +1 -1
  59. data/lib/graphql/object_type.rb +1 -6
  60. data/lib/graphql/query/arguments.rb +63 -32
  61. data/lib/graphql/query/context.rb +32 -2
  62. data/lib/graphql/query/literal_input.rb +4 -1
  63. data/lib/graphql/query/null_context.rb +1 -1
  64. data/lib/graphql/query/result.rb +1 -1
  65. data/lib/graphql/query/variables.rb +21 -3
  66. data/lib/graphql/query.rb +19 -6
  67. data/lib/graphql/railtie.rb +109 -0
  68. data/lib/graphql/relay/connection_resolve.rb +3 -0
  69. data/lib/graphql/relay/connection_type.rb +5 -3
  70. data/lib/graphql/relay/edge_type.rb +2 -1
  71. data/lib/graphql/relay/global_id_resolve.rb +5 -1
  72. data/lib/graphql/relay/mongo_relation_connection.rb +40 -0
  73. data/lib/graphql/relay/mutation/instrumentation.rb +1 -1
  74. data/lib/graphql/relay/mutation/resolve.rb +5 -1
  75. data/lib/graphql/relay/relation_connection.rb +14 -19
  76. data/lib/graphql/relay/type_extensions.rb +30 -0
  77. data/lib/graphql/relay.rb +2 -0
  78. data/lib/graphql/scalar_type.rb +14 -2
  79. data/lib/graphql/schema/argument.rb +92 -0
  80. data/lib/graphql/schema/build_from_definition.rb +64 -18
  81. data/lib/graphql/schema/enum.rb +85 -0
  82. data/lib/graphql/schema/enum_value.rb +74 -0
  83. data/lib/graphql/schema/field.rb +372 -0
  84. data/lib/graphql/schema/finder.rb +153 -0
  85. data/lib/graphql/schema/input_object.rb +87 -0
  86. data/lib/graphql/schema/interface.rb +105 -0
  87. data/lib/graphql/schema/introspection_system.rb +93 -0
  88. data/lib/graphql/schema/late_bound_type.rb +32 -0
  89. data/lib/graphql/schema/list.rb +32 -0
  90. data/lib/graphql/schema/loader.rb +2 -2
  91. data/lib/graphql/schema/member/accepts_definition.rb +152 -0
  92. data/lib/graphql/schema/member/base_dsl_methods.rb +100 -0
  93. data/lib/graphql/schema/member/build_type.rb +137 -0
  94. data/lib/graphql/schema/member/cached_graphql_definition.rb +26 -0
  95. data/lib/graphql/schema/member/graphql_type_names.rb +21 -0
  96. data/lib/graphql/schema/member/has_arguments.rb +50 -0
  97. data/lib/graphql/schema/member/has_fields.rb +130 -0
  98. data/lib/graphql/schema/member/instrumentation.rb +115 -0
  99. data/lib/graphql/schema/member/type_system_helpers.rb +34 -0
  100. data/lib/graphql/schema/member.rb +28 -0
  101. data/lib/graphql/schema/middleware_chain.rb +5 -1
  102. data/lib/graphql/schema/mutation.rb +138 -0
  103. data/lib/graphql/schema/non_null.rb +38 -0
  104. data/lib/graphql/schema/object.rb +81 -0
  105. data/lib/graphql/schema/printer.rb +33 -266
  106. data/lib/graphql/schema/relay_classic_mutation.rb +87 -0
  107. data/lib/graphql/schema/rescue_middleware.rb +8 -7
  108. data/lib/graphql/schema/resolver.rb +122 -0
  109. data/lib/graphql/schema/scalar.rb +35 -0
  110. data/lib/graphql/schema/traversal.rb +102 -22
  111. data/lib/graphql/schema/union.rb +36 -0
  112. data/lib/graphql/schema/validation.rb +3 -2
  113. data/lib/graphql/schema.rb +381 -12
  114. data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
  115. data/lib/graphql/static_validation/literal_validator.rb +16 -4
  116. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +6 -6
  117. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -1
  118. data/lib/graphql/static_validation/rules/fields_will_merge.rb +15 -8
  119. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +11 -1
  120. data/lib/graphql/static_validation/validation_context.rb +1 -1
  121. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +7 -5
  122. data/lib/graphql/subscriptions/instrumentation.rb +5 -1
  123. data/lib/graphql/subscriptions/serialize.rb +2 -0
  124. data/lib/graphql/subscriptions.rb +90 -16
  125. data/lib/graphql/tracing/data_dog_tracing.rb +49 -0
  126. data/lib/graphql/tracing/new_relic_tracing.rb +26 -0
  127. data/lib/graphql/tracing/platform_tracing.rb +20 -7
  128. data/lib/graphql/tracing/scout_tracing.rb +2 -2
  129. data/lib/graphql/tracing.rb +1 -0
  130. data/lib/graphql/unresolved_type_error.rb +3 -2
  131. data/lib/graphql/upgrader/member.rb +894 -0
  132. data/lib/graphql/upgrader/schema.rb +37 -0
  133. data/lib/graphql/version.rb +1 -1
  134. data/lib/graphql.rb +5 -25
  135. data/readme.md +2 -2
  136. data/spec/dummy/app/channels/graphql_channel.rb +23 -2
  137. data/spec/dummy/log/development.log +239 -0
  138. data/spec/dummy/log/test.log +410 -0
  139. data/spec/dummy/test/system/action_cable_subscription_test.rb +4 -0
  140. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/-x/-xYZjAnuuzgR79fcznLTQtSdh6AARxu8FcQ_J6p7L3U.cache +0 -0
  141. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/13/13HiV12xyoQvT-1L39ZzLwMZxjyaGMiENmfw7f-QTIc.cache +0 -0
  142. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/3W/3Wtf5pCWdqq0AB-iB0Y9uUNrTkruRxIEf1XFn_BETU0.cache +1 -0
  143. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/5i/5iguGafb4hOn8262Kn8Q37ogNN9MxxQKGKNzHAzUcvI.cache +1 -0
  144. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/8m/8mj2T6yy847Mc2Z7k3Xzh8O91hhVJt3NrPe8ASNDlIA.cache +1 -0
  145. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/DT/DTQyMpr4ABZYQetsdRJ5A7S4jf1r3ie4FGOR7GZBNSs.cache +3 -0
  146. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Dq/DqJ5_yJPrP5iLlOQyTQsjAVI5FE5LCVDkED0f7GgsSo.cache +3 -0
  147. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/F8/F8MUNRzORGFgr329fNM0xLaoWCXdv3BIalT7dsvLfjs.cache +0 -0
  148. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/KB/KB07ZaKNC5uXJ7TjLi-WqnY6g7dq8wWp_8N3HNjBNxg.cache +0 -0
  149. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Rw/RwDuCV-XpnCtjNkvhpJfBuxXMk0b5AD3L9eR6M-wcy0.cache +3 -0
  150. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/UL/ULdjhhb0bRuqmaG7XSZlFYzGYCXTDnqZuJBTWRlzqgw.cache +0 -0
  151. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Up/UpPNgh0yYoUsyMDh5zWqe_U6qJIyTC6-dxMMAs1vvlM.cache +1 -0
  152. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Wg/Wguh-szFGTI1gaL6npYwPekMXflugRei7F_mOyRucXg.cache +0 -0
  153. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/X-/X-khLYMA9mqFRPg3zAi86mREDxpKl4bdKYp3uF6WHos.cache +0 -0
  154. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/bi/BIkdhfxsezxM4q-HZ4oCNTq97WEJTigcq0tpX2cDvbY.cache +0 -0
  155. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ff/FfxmA4CMHQZT7exx0G7NS1Wpcnny0vzp-Jhc2H36bp8.cache +1 -0
  156. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/gE/gEiiG4GZNy_djEjK2pHm_NgA-gyhLZhdQvo0Yt96GqE.cache +0 -0
  157. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/gn/gnA9ZSqpjccNL2m8pe_jBvY6SinXlCzXDWyop83Od8s.cache +1 -0
  158. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/lO/lOAan3cMwCE_Hli6gsDML88xFNfn0nxPmvrSkW7eEOw.cache +1 -0
  159. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/m1/M1pv8MJEPLXGLvS8QxVh3DSO9cI4mRt5FHFWdrvUj6o.cache +2 -0
  160. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/m7/m77qH7ZqH0_0SmwJbiKGDd-aLau1Dav847DC6ge46zY.cache +1 -0
  161. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/sj/sjRjnjRB37lH2vrgtkdJ8Cz84__IJ978IuKTM7HcztI.cache +0 -0
  162. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/um/um1JrirR4hJhK-1rE-HywlyCi5ibgxHVrReiujZBWJM.cache +1 -0
  163. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/v4/v4fwVytD7ITcE0_GDbslZEYud8a5Okm85fV1o7SDl6g.cache +0 -0
  164. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/v_/v_0PAQt0iipQjFP5zjgkkk9Stnpf4VzvnMv67d1Keuw.cache +1 -0
  165. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/wd/wdT9U4MKxe1PyqNjVuCKMpCl3dxGCIRJIlwUTfh2DQU.cache +1 -0
  166. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/xI/xIaxut_fEIhKBDqljTNwYaADK9kj3gG0ESrfHs-5_og.cache +3 -0
  167. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/y0/y0SJOqIx2fn1SKqOkAihsQow0trRJrSIyAswufVuoA8.cache +0 -0
  168. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/zg/zgpzeaX-KZErHyGJ1aBH3ZusweNXMneVZule88XsIJI.cache +1 -0
  169. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/zy/zYFltDy-8VC-uKq2BVEiJJyYXNFvVzAKuMlR3ZIYZsk.cache +0 -0
  170. data/spec/dummy/tmp/screenshots/failures_test_it_handles_subscriptions.png +0 -0
  171. data/spec/fixtures/upgrader/account.original.rb +19 -0
  172. data/spec/fixtures/upgrader/account.transformed.rb +20 -0
  173. data/spec/fixtures/upgrader/blame_range.original.rb +43 -0
  174. data/spec/fixtures/upgrader/blame_range.transformed.rb +30 -0
  175. data/spec/fixtures/upgrader/date_time.original.rb +24 -0
  176. data/spec/fixtures/upgrader/date_time.transformed.rb +23 -0
  177. data/spec/fixtures/upgrader/delete_project.original.rb +28 -0
  178. data/spec/fixtures/upgrader/delete_project.transformed.rb +27 -0
  179. data/spec/fixtures/upgrader/gist_order_field.original.rb +14 -0
  180. data/spec/fixtures/upgrader/gist_order_field.transformed.rb +13 -0
  181. data/spec/fixtures/upgrader/increment_count.original.rb +59 -0
  182. data/spec/fixtures/upgrader/increment_count.transformed.rb +50 -0
  183. data/spec/fixtures/upgrader/photo.original.rb +10 -0
  184. data/spec/fixtures/upgrader/photo.transformed.rb +12 -0
  185. data/spec/fixtures/upgrader/release_order.original.rb +15 -0
  186. data/spec/fixtures/upgrader/release_order.transformed.rb +14 -0
  187. data/spec/fixtures/upgrader/starrable.original.rb +49 -0
  188. data/spec/fixtures/upgrader/starrable.transformed.rb +46 -0
  189. data/spec/fixtures/upgrader/subscribable.original.rb +55 -0
  190. data/spec/fixtures/upgrader/subscribable.transformed.rb +51 -0
  191. data/spec/fixtures/upgrader/type_x.original.rb +65 -0
  192. data/spec/fixtures/upgrader/type_x.transformed.rb +56 -0
  193. data/spec/generators/graphql/function_generator_spec.rb +26 -0
  194. data/spec/generators/graphql/install_generator_spec.rb +1 -1
  195. data/spec/generators/graphql/loader_generator_spec.rb +24 -0
  196. data/spec/graphql/analysis/max_query_complexity_spec.rb +3 -3
  197. data/spec/graphql/analysis/max_query_depth_spec.rb +3 -3
  198. data/spec/graphql/argument_spec.rb +21 -0
  199. data/spec/graphql/backtrace_spec.rb +10 -0
  200. data/spec/graphql/base_type_spec.rb +42 -0
  201. data/spec/graphql/boolean_type_spec.rb +3 -3
  202. data/spec/graphql/directive_spec.rb +3 -1
  203. data/spec/graphql/enum_type_spec.rb +18 -5
  204. data/spec/graphql/execution/execute_spec.rb +4 -4
  205. data/spec/graphql/execution/instrumentation_spec.rb +165 -0
  206. data/spec/graphql/execution/multiplex_spec.rb +2 -2
  207. data/spec/graphql/execution_error_spec.rb +18 -0
  208. data/spec/graphql/float_type_spec.rb +2 -2
  209. data/spec/graphql/id_type_spec.rb +1 -1
  210. data/spec/graphql/input_object_type_spec.rb +15 -2
  211. data/spec/graphql/int_type_spec.rb +2 -2
  212. data/spec/graphql/interface_type_spec.rb +12 -0
  213. data/spec/graphql/internal_representation/rewrite_spec.rb +2 -2
  214. data/spec/graphql/introspection/schema_type_spec.rb +2 -0
  215. data/spec/graphql/language/block_string_spec.rb +70 -0
  216. data/spec/graphql/language/document_from_schema_definition_spec.rb +770 -0
  217. data/spec/graphql/language/generation_spec.rb +21 -186
  218. data/spec/graphql/language/lexer_spec.rb +21 -1
  219. data/spec/graphql/language/nodes_spec.rb +21 -12
  220. data/spec/graphql/language/parser_spec.rb +1 -1
  221. data/spec/graphql/language/printer_spec.rb +203 -0
  222. data/spec/graphql/object_type_spec.rb +22 -0
  223. data/spec/graphql/query/arguments_spec.rb +25 -15
  224. data/spec/graphql/query/context_spec.rb +18 -0
  225. data/spec/graphql/query/executor_spec.rb +2 -1
  226. data/spec/graphql/query/serial_execution/value_resolution_spec.rb +2 -8
  227. data/spec/graphql/query/variables_spec.rb +42 -1
  228. data/spec/graphql/query_spec.rb +31 -5
  229. data/spec/graphql/rake_task_spec.rb +3 -1
  230. data/spec/graphql/relay/base_connection_spec.rb +1 -1
  231. data/spec/graphql/relay/connection_instrumentation_spec.rb +2 -2
  232. data/spec/graphql/relay/connection_resolve_spec.rb +1 -1
  233. data/spec/graphql/relay/connection_type_spec.rb +1 -1
  234. data/spec/graphql/relay/mongo_relation_connection_spec.rb +474 -0
  235. data/spec/graphql/relay/mutation_spec.rb +9 -7
  236. data/spec/graphql/relay/range_add_spec.rb +5 -1
  237. data/spec/graphql/relay/relation_connection_spec.rb +65 -1
  238. data/spec/graphql/schema/argument_spec.rb +87 -0
  239. data/spec/graphql/schema/build_from_definition_spec.rb +89 -5
  240. data/spec/graphql/schema/enum_spec.rb +74 -0
  241. data/spec/graphql/schema/field_spec.rb +225 -0
  242. data/spec/graphql/schema/finder_spec.rb +135 -0
  243. data/spec/graphql/schema/input_object_spec.rb +111 -0
  244. data/spec/graphql/schema/instrumentation_spec.rb +40 -0
  245. data/spec/graphql/schema/interface_spec.rb +185 -0
  246. data/spec/graphql/schema/introspection_system_spec.rb +39 -0
  247. data/spec/graphql/schema/member/accepts_definition_spec.rb +111 -0
  248. data/spec/graphql/schema/member/build_type_spec.rb +17 -0
  249. data/spec/graphql/schema/member/has_fields_spec.rb +129 -0
  250. data/spec/graphql/schema/member/type_system_helpers_spec.rb +63 -0
  251. data/spec/graphql/schema/mutation_spec.rb +148 -0
  252. data/spec/graphql/schema/object_spec.rb +175 -0
  253. data/spec/graphql/schema/printer_spec.rb +111 -15
  254. data/spec/graphql/schema/relay_classic_mutation_spec.rb +38 -0
  255. data/spec/graphql/schema/rescue_middleware_spec.rb +11 -0
  256. data/spec/graphql/schema/resolver_spec.rb +131 -0
  257. data/spec/graphql/schema/scalar_spec.rb +95 -0
  258. data/spec/graphql/schema/traversal_spec.rb +31 -0
  259. data/spec/graphql/schema/union_spec.rb +65 -0
  260. data/spec/graphql/schema/validation_spec.rb +1 -1
  261. data/spec/graphql/schema/warden_spec.rb +11 -11
  262. data/spec/graphql/schema_spec.rb +55 -12
  263. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +10 -2
  264. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
  265. data/spec/graphql/string_type_spec.rb +3 -3
  266. data/spec/graphql/subscriptions_spec.rb +273 -184
  267. data/spec/graphql/tracing/active_support_notifications_tracing_spec.rb +1 -1
  268. data/spec/graphql/tracing/new_relic_tracing_spec.rb +47 -0
  269. data/spec/graphql/tracing/platform_tracing_spec.rb +60 -1
  270. data/spec/graphql/union_type_spec.rb +1 -1
  271. data/spec/graphql/upgrader/member_spec.rb +516 -0
  272. data/spec/graphql/upgrader/schema_spec.rb +82 -0
  273. data/spec/spec_helper.rb +8 -0
  274. data/spec/support/dummy/schema.rb +53 -24
  275. data/spec/support/jazz.rb +544 -0
  276. data/spec/support/lazy_helpers.rb +21 -23
  277. data/spec/support/new_relic.rb +24 -0
  278. data/spec/support/star_trek/data.rb +109 -0
  279. data/spec/support/star_trek/schema.rb +388 -0
  280. data/spec/support/star_wars/data.rb +6 -7
  281. data/spec/support/star_wars/schema.rb +127 -171
  282. metadata +233 -11
  283. data/lib/graphql/introspection/arguments_field.rb +0 -7
  284. data/lib/graphql/introspection/enum_values_field.rb +0 -18
  285. data/lib/graphql/introspection/fields_field.rb +0 -13
  286. data/lib/graphql/introspection/input_fields_field.rb +0 -12
  287. data/lib/graphql/introspection/interfaces_field.rb +0 -11
  288. data/lib/graphql/introspection/of_type_field.rb +0 -6
  289. data/lib/graphql/introspection/possible_types_field.rb +0 -11
@@ -0,0 +1,894 @@
1
+ # frozen_string_literal: true
2
+ begin
3
+ require 'parser/current'
4
+ rescue LoadError
5
+ raise LoadError, "GraphQL::Upgrader requires the 'parser' gem, please install it and/or add it to your Gemfile"
6
+ end
7
+
8
+ module GraphQL
9
+ module Upgrader
10
+ GRAPHQL_TYPES = '(Object|InputObject|Interface|Enum|Scalar|Union)'
11
+
12
+ class Transform
13
+ # @param input_text [String] Untransformed GraphQL-Ruby code
14
+ # @return [String] The input text, with a transformation applied if necessary
15
+ def apply(input_text)
16
+ raise NotImplementedError, "Return transformed text here"
17
+ end
18
+
19
+ # Recursively transform a `.define`-DSL-based type expression into a class-ready expression, for example:
20
+ #
21
+ # - `types[X]` -> `[X, null: true]`
22
+ # - `Int` -> `Integer`
23
+ # - `X!` -> `X`
24
+ #
25
+ # Notice that `!` is removed sometimes, because it doesn't exist in the class API.
26
+ #
27
+ # @param type_expr [String] A `.define`-ready expression of a return type or input type
28
+ # @return [String] A class-ready expression of the same type`
29
+ def normalize_type_expression(type_expr, preserve_bang: false)
30
+ case type_expr
31
+ when /\A!/
32
+ # Handle the bang, normalize the inside
33
+ "#{preserve_bang ? "!" : ""}#{normalize_type_expression(type_expr[1..-1], preserve_bang: preserve_bang)}"
34
+ when /\Atypes\[.*\]\Z/
35
+ # Unwrap the brackets, normalize, then re-wrap
36
+ inner_type = type_expr[6..-2]
37
+ if inner_type.start_with?("!")
38
+ nullable = false
39
+ inner_type = inner_type[1..-1]
40
+ else
41
+ nullable = true
42
+ end
43
+
44
+ "[#{normalize_type_expression(inner_type, preserve_bang: preserve_bang)}#{nullable ? ", null: true" : ""}]"
45
+ when /\Atypes\./
46
+ # Remove the prefix
47
+ normalize_type_expression(type_expr[6..-1], preserve_bang: preserve_bang)
48
+ when /\A->/
49
+ # Remove the proc wrapper, don't re-apply it
50
+ # because stabby is not supported in class-based definition
51
+ # (and shouldn't ever be necessary)
52
+ unwrapped = type_expr
53
+ .sub(/\A->\s?\{\s*/, "")
54
+ .sub(/\s*\}/, "")
55
+ normalize_type_expression(unwrapped, preserve_bang: preserve_bang)
56
+ when "Int"
57
+ "Integer"
58
+ else
59
+ type_expr
60
+ end
61
+ end
62
+
63
+ def underscorize(str)
64
+ str
65
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
66
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2') # someThing -> some_Thing
67
+ .downcase
68
+ end
69
+
70
+ def apply_processor(input_text, processor)
71
+ ruby_ast = Parser::CurrentRuby.parse(input_text)
72
+ processor.process(ruby_ast)
73
+ processor
74
+ rescue Parser::SyntaxError
75
+ puts "Error text:"
76
+ puts input_text
77
+ raise
78
+ end
79
+
80
+ def reindent_lines(input_text, from_indent:, to_indent:)
81
+ prev_indent = " " * from_indent
82
+ next_indent = " " * to_indent
83
+ # For each line, remove the previous indent, then add the new indent
84
+ lines = input_text.split("\n").map do |line|
85
+ line = line.sub(prev_indent, "")
86
+ "#{next_indent}#{line}".rstrip
87
+ end
88
+ lines.join("\n")
89
+ end
90
+
91
+ # Remove trailing whitespace
92
+ def trim_lines(input_text)
93
+ input_text.gsub(/ +$/, "")
94
+ end
95
+ end
96
+
97
+ # Turns `{X} = GraphQL::{Y}Type.define do` into `class {X} < Types::Base{Y}`.
98
+ class TypeDefineToClassTransform < Transform
99
+ # @param base_class_pattern [String] Replacement pattern for the base class name. Use this if your base classes have nonstandard names.
100
+ def initialize(base_class_pattern: "Types::Base\\3")
101
+ @find_pattern = /( *)([a-zA-Z_0-9:]*) = GraphQL::#{GRAPHQL_TYPES}Type\.define do/
102
+ @replace_pattern = "\\1class \\2 < #{base_class_pattern}"
103
+ @interface_replace_pattern = "\\1module \\2\n\\1 include #{base_class_pattern}"
104
+ end
105
+
106
+ def apply(input_text)
107
+ if input_text.include?("GraphQL::InterfaceType.define")
108
+ input_text.sub(@find_pattern, @interface_replace_pattern)
109
+ else
110
+ input_text.sub(@find_pattern, @replace_pattern)
111
+ end
112
+ end
113
+ end
114
+
115
+ # Turns `{X} = GraphQL::Relay::Mutation.define do` into `class {X} < Mutations::BaseMutation`
116
+ class MutationDefineToClassTransform < Transform
117
+ # @param base_class_name [String] Replacement pattern for the base class name. Use this if your Mutation base class has a nonstandard name.
118
+ def initialize(base_class_name: "Mutations::BaseMutation")
119
+ @find_pattern = /([a-zA-Z_0-9:]*) = GraphQL::Relay::Mutation.define do/
120
+ @replace_pattern = "class \\1 < #{base_class_name}"
121
+ end
122
+
123
+ def apply(input_text)
124
+ input_text.sub(@find_pattern, @replace_pattern)
125
+ end
126
+ end
127
+
128
+ # Remove `name "Something"` if it is redundant with the class name.
129
+ # Or, if it is not redundant, move it to `graphql_name "Something"`.
130
+ class NameTransform < Transform
131
+ def apply(transformable)
132
+ last_type_defn = transformable
133
+ .split("\n")
134
+ .select { |line| line.include?("class ") || line.include?("module ")}
135
+ .last
136
+
137
+ if last_type_defn && (matches = last_type_defn.match(/(class|module) (?<type_name>[a-zA-Z_0-9:]*)( <|$)/))
138
+ type_name = matches[:type_name]
139
+ # Get the name without any prefixes or suffixes
140
+ type_name_without_the_type_part = type_name.split('::').last.gsub(/Type$/, '')
141
+ # Find an overridden name value
142
+ if matches = transformable.match(/ name ('|")(?<overridden_name>.*)('|")/)
143
+ name = matches[:overridden_name]
144
+ if type_name_without_the_type_part != name
145
+ # If the overridden name is still required, use `graphql_name` for it
146
+ transformable = transformable.sub(/ name (.*)/, ' graphql_name \1')
147
+ else
148
+ # Otherwise, remove it altogether
149
+ transformable = transformable.sub(/\s+name ('|").*('|")/, '')
150
+ end
151
+ end
152
+ end
153
+
154
+ transformable
155
+ end
156
+ end
157
+
158
+ # Remove newlines -- normalize the text for processing
159
+ class RemoveNewlinesTransform
160
+ def apply(input_text)
161
+ keep_looking = true
162
+ while keep_looking do
163
+ keep_looking = false
164
+ # Find the `field` call (or other method), and an open paren, but not a close paren, or a comma between arguments
165
+ input_text = input_text.gsub(/(?<field>(?:field|input_field|return_field|connection|argument)(?:\([^)]*|.*,))\n\s*(?<next_line>.+)/) do
166
+ keep_looking = true
167
+ field = $~[:field].chomp
168
+ next_line = $~[:next_line]
169
+
170
+ "#{field} #{next_line}"
171
+ end
172
+ end
173
+ input_text
174
+ end
175
+ end
176
+
177
+ # Remove parens from method call - normalize for processing
178
+ class RemoveMethodParensTransform < Transform
179
+ def apply(input_text)
180
+ input_text.sub(
181
+ /(field|input_field|return_field|connection|argument)\( *(.*?) *\)( *)/,
182
+ '\1 \2\3'
183
+ )
184
+ end
185
+ end
186
+
187
+ # Move `type X` to be the second positional argument to `field ...`
188
+ class PositionalTypeArgTransform < Transform
189
+ def apply(input_text)
190
+ input_text.gsub(
191
+ /(?<field>(?:field|input_field|return_field|connection|argument) :(?:[a-zA-Z_0-9]*)) do(?<block_contents>.*?)[ ]*type (?<return_type>.*?)\n/m
192
+ ) do
193
+ field = $~[:field]
194
+ block_contents = $~[:block_contents]
195
+ return_type = normalize_type_expression($~[:return_type], preserve_bang: true)
196
+
197
+ "#{field}, #{return_type} do#{block_contents}"
198
+ end
199
+ end
200
+ end
201
+
202
+ # Find a configuration in the block and move it to a kwarg,
203
+ # for example
204
+ # ```
205
+ # do
206
+ # property :thing
207
+ # end
208
+ # ```
209
+ # becomes:
210
+ # ```
211
+ # property: thing
212
+ # ```
213
+ class ConfigurationToKwargTransform < Transform
214
+ def initialize(kwarg:)
215
+ @kwarg = kwarg
216
+ end
217
+
218
+ def apply(input_text)
219
+ input_text.gsub(
220
+ /(?<field>(?:field|return_field|input_field|connection|argument).*) do(?<block_contents>.*?)[ ]*#{@kwarg} (?<kwarg_value>.*?)\n/m
221
+ ) do
222
+ field = $~[:field]
223
+ block_contents = $~[:block_contents]
224
+ kwarg_value = $~[:kwarg_value].strip
225
+
226
+ "#{field}, #{@kwarg}: #{kwarg_value} do#{block_contents}"
227
+ end
228
+ end
229
+ end
230
+
231
+ # Transform `property:` kwarg to `method:` kwarg
232
+ class PropertyToMethodTransform < Transform
233
+ def apply(input_text)
234
+ input_text.gsub /property:/, 'method:'
235
+ end
236
+ end
237
+
238
+ # Find a keyword whose value is a string or symbol,
239
+ # and if the value is equivalent to the field name,
240
+ # remove the keyword altogether.
241
+ class RemoveRedundantKwargTransform < Transform
242
+ def initialize(kwarg:)
243
+ @kwarg = kwarg
244
+ @finder_pattern = /(field|return_field|input_field|connection|argument) :(?<name>[a-zA-Z_0-9]*).*#{@kwarg}: ['":](?<kwarg_value>[a-zA-Z_0-9?!]+)['"]?/
245
+ end
246
+
247
+ def apply(input_text)
248
+ if input_text =~ @finder_pattern
249
+ field_name = $~[:name]
250
+ kwarg_value = $~[:kwarg_value]
251
+ if field_name == kwarg_value
252
+ # It's redundant, remove it
253
+ input_text = input_text.sub(/, #{@kwarg}: ['":]#{kwarg_value}['"]?/, "")
254
+ end
255
+ end
256
+ input_text
257
+ end
258
+ end
259
+
260
+ # Take camelized field names and convert them to underscore case.
261
+ # (They'll be automatically camelized later.)
262
+ class UnderscoreizeFieldNameTransform < Transform
263
+ def apply(input_text)
264
+ input_text.gsub /(?<field_type>input_field|return_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/ do
265
+ field_type = $~[:field_type]
266
+ camelized_name = $~[:name]
267
+ underscored_name = underscorize(camelized_name)
268
+ "#{field_type} :#{underscored_name}"
269
+ end
270
+ end
271
+ end
272
+
273
+ class ProcToClassMethodTransform < Transform
274
+ # @param proc_name [String] The name of the proc to be moved to `def self.#{proc_name}`
275
+ def initialize(proc_name)
276
+ @proc_name = proc_name
277
+ # This will tell us whether to operate on the input or not
278
+ @proc_check_pattern = /#{proc_name}\s?->/
279
+ end
280
+
281
+ def apply(input_text)
282
+ if input_text =~ @proc_check_pattern
283
+ processor = apply_processor(input_text, NamedProcProcessor.new(@proc_name))
284
+ proc_body = input_text[processor.proc_body_start..processor.proc_body_end]
285
+ method_defn_indent = " " * processor.proc_defn_indent
286
+ method_defn = "def self.#{@proc_name}(#{processor.proc_arg_names.join(", ")})\n#{method_defn_indent} #{proc_body}\n#{method_defn_indent}end\n"
287
+ method_defn = trim_lines(method_defn)
288
+ # replace the proc with the new method
289
+ input_text[processor.proc_defn_start..processor.proc_defn_end] = method_defn
290
+ end
291
+ input_text
292
+ end
293
+
294
+ class NamedProcProcessor < Parser::AST::Processor
295
+ attr_reader :proc_arg_names, :proc_defn_start, :proc_defn_end, :proc_defn_indent, :proc_body_start, :proc_body_end
296
+ def initialize(proc_name)
297
+ @proc_name_sym = proc_name.to_sym
298
+ @proc_arg_names = nil
299
+ # Beginning of the `#{proc_name} -> {...}` call
300
+ @proc_defn_start = nil
301
+ # End of the last `end/}`
302
+ @proc_defn_end = nil
303
+ # Amount of whitespace to insert to the rewritten body
304
+ @proc_defn_indent = nil
305
+ # First statement of the proc
306
+ @proc_body_start = nil
307
+ # End of last statement in the proc
308
+ @proc_body_end = nil
309
+ # Used for identifying the proper block
310
+ @inside_proc = false
311
+ end
312
+
313
+ def on_send(node)
314
+ receiver, method_name, _args = *node
315
+ if method_name == @proc_name_sym && receiver.nil?
316
+ source_exp = node.loc.expression
317
+ @proc_defn_start = source_exp.begin.begin_pos
318
+ @proc_defn_end = source_exp.end.end_pos
319
+ @proc_defn_indent = source_exp.column
320
+ @inside_proc = true
321
+ end
322
+ res = super(node)
323
+ @inside_proc = false
324
+ res
325
+ end
326
+
327
+ def on_block(node)
328
+ send_node, args_node, body_node = node.children
329
+ _receiver, method_name, _send_args_node = *send_node
330
+ if method_name == :lambda && @inside_proc
331
+ source_exp = body_node.loc.expression
332
+ @proc_arg_names = args_node.children.map { |arg_node| arg_node.children[0].to_s }
333
+ @proc_body_start = source_exp.begin.begin_pos
334
+ @proc_body_end = source_exp.end.end_pos
335
+ end
336
+ super(node)
337
+ end
338
+ end
339
+ end
340
+
341
+ class MutationResolveProcToMethodTransform < Transform
342
+ # @param proc_name [String] The name of the proc to be moved to `def self.#{proc_name}`
343
+ def initialize(proc_name: "resolve")
344
+ @proc_name = proc_name
345
+ end
346
+
347
+ # TODO dedup with ResolveProcToMethodTransform
348
+ def apply(input_text)
349
+ if input_text =~ /GraphQL::Relay::Mutation\.define/
350
+ named_proc_processor = apply_processor(input_text, ProcToClassMethodTransform::NamedProcProcessor.new(@proc_name))
351
+ resolve_proc_processor = apply_processor(input_text, ResolveProcToMethodTransform::ResolveProcProcessor.new)
352
+ proc_body = input_text[named_proc_processor.proc_body_start..named_proc_processor.proc_body_end]
353
+ method_defn_indent = " " * named_proc_processor.proc_defn_indent
354
+
355
+ obj_arg_name, args_arg_name, ctx_arg_name = resolve_proc_processor.proc_arg_names
356
+ # This is not good, it will hit false positives
357
+ # Should use AST to make this substitution
358
+ if obj_arg_name != "_"
359
+ proc_body.gsub!(/([^\w:.]|^)#{obj_arg_name}([^\w:]|$)/, '\1@object\2')
360
+ end
361
+ if ctx_arg_name != "_"
362
+ proc_body.gsub!(/([^\w:.]|^)#{ctx_arg_name}([^\w:]|$)/, '\1@context\2')
363
+ end
364
+
365
+ method_defn = "def #{@proc_name}(**#{args_arg_name})\n#{method_defn_indent} #{proc_body}\n#{method_defn_indent}end\n"
366
+ method_defn = trim_lines(method_defn)
367
+ # Update usage of args keys
368
+ method_defn = method_defn.gsub(/#{args_arg_name}(?<method_begin>\.key\?\(?|\[)["':](?<arg_name>[a-zA-Z0-9_]+)["']?(?<method_end>\]|\))?/) do
369
+ method_begin = $~[:method_begin]
370
+ arg_name = underscorize($~[:arg_name])
371
+ method_end = $~[:method_end]
372
+ "#{args_arg_name}#{method_begin}:#{arg_name}#{method_end}"
373
+ end
374
+ # replace the proc with the new method
375
+ input_text[named_proc_processor.proc_defn_start..named_proc_processor.proc_defn_end] = method_defn
376
+ end
377
+ input_text
378
+ end
379
+ end
380
+
381
+ # Find hash literals which are returned from mutation resolves,
382
+ # and convert their keys to underscores. This catches a lot of cases but misses
383
+ # hashes which are initialized anywhere except in the return expression.
384
+ class UnderscorizeMutationHashTransform < Transform
385
+ def apply(input_text)
386
+ if input_text =~ /def resolve\(\*\*/
387
+ processor = apply_processor(input_text, ReturnedHashLiteralProcessor.new)
388
+ # Use reverse_each to avoid messing up positions
389
+ processor.keys_to_upgrade.reverse_each do |key_data|
390
+ underscored_key = underscorize(key_data[:key].to_s)
391
+ if key_data[:operator] == ":"
392
+ input_text[key_data[:start]...key_data[:end]] = underscored_key
393
+ else
394
+ input_text[key_data[:start]...key_data[:end]] = ":#{underscored_key}"
395
+ end
396
+ end
397
+ end
398
+ input_text
399
+ end
400
+
401
+ class ReturnedHashLiteralProcessor < Parser::AST::Processor
402
+ attr_reader :keys_to_upgrade
403
+ def initialize
404
+ @keys_to_upgrade = []
405
+ end
406
+
407
+ def on_def(node)
408
+ method_name, _args, body = *node
409
+ if method_name == :resolve
410
+ possible_returned_hashes = find_returned_hashes(body, returning: false)
411
+ possible_returned_hashes.each do |hash_node|
412
+ pairs = *hash_node
413
+ pairs.each do |pair_node|
414
+ if pair_node.type == :pair # Skip over :kwsplat
415
+ pair_k, _pair_v = *pair_node
416
+ if pair_k.type == :sym && pair_k.children[0].to_s =~ /[a-z][A-Z]/ # Does it have any camelcase boundaries?
417
+ source_exp = pair_k.loc.expression
418
+ @keys_to_upgrade << {
419
+ start: source_exp.begin.begin_pos,
420
+ end: source_exp.end.end_pos,
421
+ key: pair_k.children[0],
422
+ operator: pair_node.loc.operator.source,
423
+ }
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ end
431
+
432
+ private
433
+
434
+ # Look for hash nodes, starting from `node`.
435
+ # Return hash nodes that are valid candiates for returning from this method.
436
+ def find_returned_hashes(node, returning:)
437
+ if node.is_a?(Array)
438
+ *possible_returns, last_expression = *node
439
+ return possible_returns.map { |c| find_returned_hashes(c, returning: false) }.flatten +
440
+ # Check the last expression of a method body
441
+ find_returned_hashes(last_expression, returning: returning)
442
+ end
443
+
444
+ case node.type
445
+ when :hash
446
+ if returning
447
+ [node]
448
+ else
449
+ # This is some random hash literal
450
+ []
451
+ end
452
+ when :begin
453
+ # Check the last expression of a method body
454
+ find_returned_hashes(node.children, returning: true)
455
+ when :resbody
456
+ _condition, _assign, body = *node
457
+ find_returned_hashes(body, returning: returning)
458
+ when :kwbegin
459
+ find_returned_hashes(node.children, returning: returning)
460
+ when :rescue
461
+ try_body, rescue_body, _ensure_body = *node
462
+ find_returned_hashes(try_body, returning: returning) + find_returned_hashes(rescue_body, returning: returning)
463
+ when :block
464
+ # Check methods with blocks for possible returns
465
+ method_call, _args, *body = *node
466
+ if method_call.type == :send
467
+ find_returned_hashes(body, returning: returning)
468
+ end
469
+ when :if
470
+ # Check each branch of a conditional
471
+ _condition, *branches = *node
472
+ branches.compact.map { |b| find_returned_hashes(b, returning: returning) }.flatten
473
+ when :return
474
+ find_returned_hashes(node.children.first, returning: true)
475
+ else
476
+ []
477
+ end
478
+ rescue
479
+ p "--- UnderscorizeMutationHashTransform crashed on node: ---"
480
+ p node
481
+ raise
482
+ end
483
+
484
+ end
485
+ end
486
+
487
+ class ResolveProcToMethodTransform < Transform
488
+ def apply(input_text)
489
+ if input_text =~ /resolve\(? ?->/
490
+ # - Find the proc literal
491
+ # - Get the three argument names (obj, arg, ctx)
492
+ # - Get the proc body
493
+ # - Find and replace:
494
+ # - The ctx argument becomes `@context`
495
+ # - The obj argument becomes `@object`
496
+ # - Args is trickier:
497
+ # - If it's not used, remove it
498
+ # - If it's used, abandon ship and make it `**args`
499
+ # - Convert string args access to symbol access, since it's a Ruby **splat
500
+ # - Convert camelized arg names to underscored arg names
501
+ # - (It would be nice to correctly become Ruby kwargs, but that might be too hard)
502
+ # - Add a `# TODO` comment to the method source?
503
+ # - Rebuild the method:
504
+ # - use the field name as the method name
505
+ # - handle args as described above
506
+ # - put the modified proc body as the method body
507
+
508
+ input_text.match(/(?<field_type>input_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/)
509
+ field_name = $~[:name]
510
+ processor = apply_processor(input_text, ResolveProcProcessor.new)
511
+ proc_body = input_text[processor.proc_start..processor.proc_end]
512
+ obj_arg_name, args_arg_name, ctx_arg_name = processor.proc_arg_names
513
+ # This is not good, it will hit false positives
514
+ # Should use AST to make this substitution
515
+ if obj_arg_name != "_"
516
+ proc_body.gsub!(/([^\w:.]|^)#{obj_arg_name}([^\w:]|$)/, '\1@object\2')
517
+ end
518
+ if ctx_arg_name != "_"
519
+ proc_body.gsub!(/([^\w:.]|^)#{ctx_arg_name}([^\w:]|$)/, '\1@context\2')
520
+ end
521
+
522
+ method_def_indent = " " * (processor.resolve_indent - 2)
523
+ # Turn the proc body into a method body
524
+ method_body = reindent_lines(proc_body, from_indent: processor.resolve_indent + 2, to_indent: processor.resolve_indent)
525
+ # Add `def... end`
526
+ method_def = if input_text.include?("argument ")
527
+ # This field has arguments
528
+ "def #{field_name}(**#{args_arg_name})"
529
+ else
530
+ # No field arguments, so, no method arguments
531
+ "def #{field_name}"
532
+ end
533
+ # Wrap the body in def ... end
534
+ method_body = "\n#{method_def_indent}#{method_def}\n#{method_body}\n#{method_def_indent}end\n"
535
+ # Update Argument access to be underscore and symbols
536
+ # Update `args[...]` and `args.key?`
537
+ method_body = method_body.gsub(/#{args_arg_name}(?<method_begin>\.key\?\(?|\[)["':](?<arg_name>[a-zA-Z0-9_]+)["']?(?<method_end>\]|\))?/) do
538
+ method_begin = $~[:method_begin]
539
+ arg_name = underscorize($~[:arg_name])
540
+ method_end = $~[:method_end]
541
+ "#{args_arg_name}#{method_begin}:#{arg_name}#{method_end}"
542
+ end
543
+
544
+ # Replace the resolve proc with the method
545
+ input_text[processor.resolve_start..processor.resolve_end] = ""
546
+ # The replacement above might have left some preceeding whitespace,
547
+ # so remove it by deleting all whitespace chars before `resolve`:
548
+ preceeding_whitespace = processor.resolve_start - 1
549
+ while input_text[preceeding_whitespace] == " " && preceeding_whitespace > 0
550
+ input_text[preceeding_whitespace] = ""
551
+ preceeding_whitespace -= 1
552
+ end
553
+ input_text += method_body
554
+ input_text
555
+ else
556
+ # No resolve proc
557
+ input_text
558
+ end
559
+ end
560
+
561
+ class ResolveProcProcessor < Parser::AST::Processor
562
+ attr_reader :proc_start, :proc_end, :proc_arg_names, :resolve_start, :resolve_end, :resolve_indent
563
+ def initialize
564
+ @proc_arg_names = nil
565
+ @resolve_start = nil
566
+ @resolve_end = nil
567
+ @resolve_indent = nil
568
+ @proc_start = nil
569
+ @proc_end = nil
570
+ end
571
+
572
+ def on_send(node)
573
+ receiver, method_name, _args = *node
574
+ if method_name == :resolve && receiver.nil?
575
+ source_exp = node.loc.expression
576
+ @resolve_start = source_exp.begin.begin_pos
577
+ @resolve_end = source_exp.end.end_pos
578
+ @resolve_indent = source_exp.column
579
+ end
580
+ super(node)
581
+ end
582
+
583
+ def on_block(node)
584
+ send_node, args_node, body_node = node.children
585
+ _receiver, method_name, _send_args_node = *send_node
586
+ # Assume that the first three-argument proc we enter is the resolve
587
+ if method_name == :lambda && args_node.children.size == 3 && @proc_arg_names.nil?
588
+ source_exp = body_node.loc.expression
589
+ @proc_arg_names = args_node.children.map { |arg_node| arg_node.children[0].to_s }
590
+ @proc_start = source_exp.begin.begin_pos
591
+ @proc_end = source_exp.end.end_pos
592
+ end
593
+ super(node)
594
+ end
595
+ end
596
+ end
597
+
598
+ # Transform `interfaces [A, B, C]` to `implements A\nimplements B\nimplements C\n`
599
+ class InterfacesToImplementsTransform < Transform
600
+ PATTERN = /(?<indent>\s*)(?:interfaces) \[\s*(?<interfaces>(?:[a-zA-Z_0-9:\.,\s]+))\]/m
601
+ def apply(input_text)
602
+ input_text.gsub(PATTERN) do
603
+ indent = $~[:indent]
604
+ interfaces = $~[:interfaces].split(',').map(&:strip).reject(&:empty?)
605
+ # Preserve leading newlines before the `interfaces ...`
606
+ # call, but don't re-insert them between `implements` calls.
607
+ extra_leading_newlines = "\n" * (indent[/^\n*/].length - 1)
608
+ indent = indent.sub(/^\n*/m, "")
609
+ interfaces_calls = interfaces
610
+ .map { |interface| "\n#{indent}implements #{interface}" }
611
+ .join
612
+ extra_leading_newlines + interfaces_calls
613
+ end
614
+ end
615
+ end
616
+
617
+ # Transform `possible_types [A, B, C]` to `possible_types(A, B, C)`
618
+ class PossibleTypesTransform < Transform
619
+ PATTERN = /(?<indent>\s*)(?:possible_types) \[\s*(?<possible_types>(?:[a-zA-Z_0-9:\.,\s]+))\]/m
620
+ def apply(input_text)
621
+ input_text.gsub(PATTERN) do
622
+ indent = $~[:indent]
623
+ possible_types = $~[:possible_types].split(',').map(&:strip).reject(&:empty?)
624
+ extra_leading_newlines = indent[/^\n*/]
625
+ method_indent = indent.sub(/^\n*/m, "")
626
+ type_indent = " " + method_indent
627
+ possible_types_call = "#{method_indent}possible_types(\n#{possible_types.map { |t| "#{type_indent}#{t},"}.join("\n")}\n#{method_indent})"
628
+ extra_leading_newlines + trim_lines(possible_types_call)
629
+ end
630
+ end
631
+ end
632
+
633
+ class UpdateMethodSignatureTransform < Transform
634
+ def apply(input_text)
635
+ input_text.scan(/(?:input_field|field|return_field|connection|argument) .*$/).each do |field|
636
+ matches = /(?<field_type>input_field|return_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)?(:?, +(?<return_type>([A-Za-z\[\]\.\!_0-9\(\)]|::|-> ?\{ ?| ?\})+))?(?<remainder>( |,|$).*)/.match(field)
637
+ if matches
638
+ name = matches[:name]
639
+ return_type = matches[:return_type]
640
+ remainder = matches[:remainder]
641
+ field_type = matches[:field_type]
642
+ with_block = remainder.gsub!(/\ do$/, '')
643
+
644
+ remainder.gsub! /,$/, ''
645
+ remainder.gsub! /^,/, ''
646
+ remainder.chomp!
647
+
648
+ if return_type
649
+ non_nullable = return_type.sub! /(^|[^\[])!/, '\1'
650
+ nullable = !non_nullable
651
+ return_type = normalize_type_expression(return_type)
652
+ else
653
+ non_nullable = nil
654
+ nullable = nil
655
+ end
656
+
657
+ input_text.sub!(field) do
658
+ is_argument = ['argument', 'input_field'].include?(field_type)
659
+ f = "#{is_argument ? 'argument' : 'field'} :#{name}"
660
+
661
+ if return_type
662
+ f += ", #{return_type}"
663
+ end
664
+
665
+ unless remainder.empty?
666
+ f += ',' + remainder
667
+ end
668
+
669
+ if is_argument
670
+ if nullable
671
+ f += ', required: false'
672
+ elsif non_nullable
673
+ f += ', required: true'
674
+ end
675
+ else
676
+ if nullable
677
+ f += ', null: true'
678
+ elsif non_nullable
679
+ f += ', null: false'
680
+ end
681
+ end
682
+
683
+ if field_type == 'connection'
684
+ f += ', connection: true'
685
+ end
686
+
687
+ if with_block
688
+ f += ' do'
689
+ end
690
+
691
+ f
692
+ end
693
+ end
694
+ end
695
+
696
+ input_text
697
+ end
698
+ end
699
+
700
+ class RemoveEmptyBlocksTransform < Transform
701
+ def apply(input_text)
702
+ input_text.gsub(/\s*do\s*end/m, "")
703
+ end
704
+ end
705
+
706
+ # Remove redundant newlines, which may have trailing spaces
707
+ # Remove double newline after `do`
708
+ # Remove double newline before `end`
709
+ # Remove lines with whitespace only
710
+ class RemoveExcessWhitespaceTransform < Transform
711
+ def apply(input_text)
712
+ input_text
713
+ .gsub(/\n{3,}/m, "\n\n")
714
+ .gsub(/do\n{2,}/m, "do\n")
715
+ .gsub(/\n{2,}(\s*)end/m, "\n\\1end")
716
+ .gsub(/\n +\n/m, "\n\n")
717
+ end
718
+ end
719
+
720
+ # Skip this file if you see any `field`
721
+ # helpers with `null: true` or `null: false` keywords
722
+ # or `argument` helpers with `required:` keywords,
723
+ # because it's already been transformed
724
+ class SkipOnNullKeyword
725
+ def skip?(input_text)
726
+ input_text =~ /field.*null: (true|false)/ || input_text =~ /argument.*required: (true|false)/
727
+ end
728
+ end
729
+
730
+ class Member
731
+ def initialize(member, skip: SkipOnNullKeyword, type_transforms: DEFAULT_TYPE_TRANSFORMS, field_transforms: DEFAULT_FIELD_TRANSFORMS, clean_up_transforms: DEFAULT_CLEAN_UP_TRANSFORMS)
732
+ @member = member
733
+ @skip = skip
734
+ @type_transforms = type_transforms
735
+ @field_transforms = field_transforms
736
+ @clean_up_transforms = clean_up_transforms
737
+ end
738
+
739
+ DEFAULT_TYPE_TRANSFORMS = [
740
+ TypeDefineToClassTransform,
741
+ MutationResolveProcToMethodTransform, # Do this before switching to class, so we can detect that its a mutation
742
+ UnderscorizeMutationHashTransform,
743
+ MutationDefineToClassTransform,
744
+ NameTransform,
745
+ InterfacesToImplementsTransform,
746
+ PossibleTypesTransform,
747
+ ProcToClassMethodTransform.new("coerce_input"),
748
+ ProcToClassMethodTransform.new("coerce_result"),
749
+ ProcToClassMethodTransform.new("resolve_type"),
750
+ ]
751
+
752
+ DEFAULT_FIELD_TRANSFORMS = [
753
+ RemoveNewlinesTransform,
754
+ RemoveMethodParensTransform,
755
+ PositionalTypeArgTransform,
756
+ ConfigurationToKwargTransform.new(kwarg: "property"),
757
+ ConfigurationToKwargTransform.new(kwarg: "description"),
758
+ ConfigurationToKwargTransform.new(kwarg: "deprecation_reason"),
759
+ ConfigurationToKwargTransform.new(kwarg: "hash_key"),
760
+ PropertyToMethodTransform,
761
+ UnderscoreizeFieldNameTransform,
762
+ ResolveProcToMethodTransform,
763
+ UpdateMethodSignatureTransform,
764
+ RemoveRedundantKwargTransform.new(kwarg: "hash_key"),
765
+ RemoveRedundantKwargTransform.new(kwarg: "method"),
766
+ ]
767
+
768
+ DEFAULT_CLEAN_UP_TRANSFORMS = [
769
+ RemoveExcessWhitespaceTransform,
770
+ RemoveEmptyBlocksTransform,
771
+ ]
772
+
773
+ def upgrade
774
+ type_source = @member.dup
775
+ should_skip = @skip.new.skip?(type_source)
776
+ # return the unmodified code
777
+ if should_skip
778
+ return type_source
779
+ end
780
+ # Transforms on type defn code:
781
+ type_source = apply_transforms(type_source, @type_transforms)
782
+ # Transforms on each field:
783
+ field_sources = find_fields(type_source)
784
+ field_sources.each do |field_source|
785
+ transformed_field_source = apply_transforms(field_source.dup, @field_transforms)
786
+ # Replace the original source code with the transformed source code:
787
+ type_source = type_source.gsub(field_source, transformed_field_source)
788
+ end
789
+ # Clean-up:
790
+ type_source = apply_transforms(type_source, @clean_up_transforms)
791
+ # Return the transformed source:
792
+ type_source
793
+ end
794
+
795
+ def upgradeable?
796
+ return false if @member.include? '< GraphQL::Schema::'
797
+ return false if @member =~ /< Types::Base#{GRAPHQL_TYPES}/
798
+
799
+ true
800
+ end
801
+
802
+ private
803
+
804
+ def apply_transforms(source_code, transforms, idx: 0)
805
+ next_transform = transforms[idx]
806
+ case next_transform
807
+ when nil
808
+ # We got to the end of the list
809
+ source_code
810
+ when Class
811
+ # Apply a class
812
+ next_source_code = next_transform.new.apply(source_code)
813
+ apply_transforms(next_source_code, transforms, idx: idx + 1)
814
+ else
815
+ # Apply an already-initialized object which responds to `apply`
816
+ next_source_code = next_transform.apply(source_code)
817
+ apply_transforms(next_source_code, transforms, idx: idx + 1)
818
+ end
819
+ end
820
+
821
+ # Parse the type, find calls to `field` and `connection`
822
+ # Return strings containing those calls
823
+ def find_fields(type_source)
824
+ type_ast = Parser::CurrentRuby.parse(type_source)
825
+ finder = FieldFinder.new
826
+ finder.process(type_ast)
827
+ field_sources = []
828
+ # For each of the locations we found, extract the text for that definition.
829
+ # The text will be transformed independently,
830
+ # then the transformed text will replace the original text.
831
+ FieldFinder::DEFINITION_METHODS.each do |def_method|
832
+ finder.locations[def_method].each do |name, (starting_idx, ending_idx)|
833
+ field_source = type_source[starting_idx..ending_idx]
834
+ field_sources << field_source
835
+ end
836
+ end
837
+ # Here's a crazy thing: the transformation is pure,
838
+ # so definitions like `argument :id, types.ID` can be transformed once
839
+ # then replaced everywhere. So:
840
+ # - make a unique array here
841
+ # - use `gsub` after performing the transformation.
842
+ field_sources.uniq!
843
+ field_sources
844
+ rescue Parser::SyntaxError
845
+ puts "Error Source:"
846
+ puts type_source
847
+ raise
848
+ end
849
+
850
+ class FieldFinder < Parser::AST::Processor
851
+ # These methods are definition DSLs which may accept a block,
852
+ # each of these definitions is passed for transformation in its own right.
853
+ # `field` and `connection` take priority. In fact, they upgrade their
854
+ # own arguments, so those upgrades turn out to be no-ops.
855
+ DEFINITION_METHODS = [:field, :connection, :input_field, :return_field, :argument]
856
+ attr_reader :locations
857
+
858
+ def initialize
859
+ # Pairs of `{ { method_name => { name => [start, end] } }`,
860
+ # since fields/arguments are unique by name, within their category
861
+ @locations = Hash.new { |h,k| h[k] = {} }
862
+ end
863
+
864
+ # @param send_node [node] The node which might be a `field` call, etc
865
+ # @param source_node [node] The node whose source defines the bounds of the definition (eg, the surrounding block)
866
+ def add_location(send_node:,source_node:)
867
+ receiver_node, method_name, *arg_nodes = *send_node
868
+ # Implicit self and one of the recognized methods
869
+ if receiver_node.nil? && DEFINITION_METHODS.include?(method_name)
870
+ name = arg_nodes[0]
871
+ # This field may have already been added because
872
+ # we find `(block ...)` nodes _before_ we find `(send ...)` nodes.
873
+ if @locations[method_name][name].nil?
874
+ starting_idx = source_node.loc.expression.begin.begin_pos
875
+ ending_idx = source_node.loc.expression.end.end_pos
876
+ @locations[method_name][name] = [starting_idx, ending_idx]
877
+ end
878
+ end
879
+ end
880
+
881
+ def on_block(node)
882
+ send_node, _args_node, _body_node = *node
883
+ add_location(send_node: send_node, source_node: node)
884
+ super(node)
885
+ end
886
+
887
+ def on_send(node)
888
+ add_location(send_node: node, source_node: node)
889
+ super(node)
890
+ end
891
+ end
892
+ end
893
+ end
894
+ end