yogurt 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/workflows/tests.yml +117 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +113 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +7 -0
  10. data/CHANGELOG.md +0 -0
  11. data/Gemfile +6 -0
  12. data/Gemfile.lock +82 -0
  13. data/LICENSE +201 -0
  14. data/README.md +38 -0
  15. data/Rakefile +9 -0
  16. data/bin/bundle +105 -0
  17. data/bin/byebug +29 -0
  18. data/bin/coderay +29 -0
  19. data/bin/console +16 -0
  20. data/bin/htmldiff +29 -0
  21. data/bin/ldiff +29 -0
  22. data/bin/pry +29 -0
  23. data/bin/rake +29 -0
  24. data/bin/rspec +29 -0
  25. data/bin/rubocop +29 -0
  26. data/bin/ruby-parse +29 -0
  27. data/bin/ruby-rewrite +29 -0
  28. data/bin/setup +8 -0
  29. data/bin/srb +29 -0
  30. data/bin/srb-rbi +29 -0
  31. data/lib/yogurt.rb +95 -0
  32. data/lib/yogurt/code_generator.rb +706 -0
  33. data/lib/yogurt/code_generator/defined_class.rb +22 -0
  34. data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
  35. data/lib/yogurt/code_generator/defined_method.rb +24 -0
  36. data/lib/yogurt/code_generator/enum_class.rb +56 -0
  37. data/lib/yogurt/code_generator/field_access_method.rb +273 -0
  38. data/lib/yogurt/code_generator/field_access_path.rb +56 -0
  39. data/lib/yogurt/code_generator/generated_file.rb +53 -0
  40. data/lib/yogurt/code_generator/input_class.rb +52 -0
  41. data/lib/yogurt/code_generator/leaf_class.rb +68 -0
  42. data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
  43. data/lib/yogurt/code_generator/root_class.rb +130 -0
  44. data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
  45. data/lib/yogurt/code_generator/typed_input.rb +12 -0
  46. data/lib/yogurt/code_generator/typed_output.rb +16 -0
  47. data/lib/yogurt/code_generator/utils.rb +140 -0
  48. data/lib/yogurt/code_generator/variable_definition.rb +33 -0
  49. data/lib/yogurt/converters.rb +50 -0
  50. data/lib/yogurt/error_result.rb +30 -0
  51. data/lib/yogurt/http.rb +80 -0
  52. data/lib/yogurt/inspectable.rb +22 -0
  53. data/lib/yogurt/memoize.rb +33 -0
  54. data/lib/yogurt/query.rb +23 -0
  55. data/lib/yogurt/query_container.rb +89 -0
  56. data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
  57. data/lib/yogurt/query_declaration.rb +11 -0
  58. data/lib/yogurt/query_executor.rb +23 -0
  59. data/lib/yogurt/query_result.rb +25 -0
  60. data/lib/yogurt/scalar_converter.rb +19 -0
  61. data/lib/yogurt/unexpected_object_type.rb +30 -0
  62. data/lib/yogurt/validation_error.rb +6 -0
  63. data/lib/yogurt/version.rb +6 -0
  64. data/sorbet/config +10 -0
  65. data/sorbet/rbi/fake_schema.rbi +14 -0
  66. data/sorbet/rbi/graphql.rbi +11 -0
  67. data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
  68. data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
  69. data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
  70. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
  71. data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
  72. data/sorbet/rbi/todo.rbi +6 -0
  73. data/yogurt.gemspec +54 -0
  74. metadata +286 -0
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ module DefinedClass
7
+ include Kernel
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ abstract!
11
+
12
+ sig {abstract.returns(String)}
13
+ def name; end
14
+
15
+ sig {abstract.returns(String)}
16
+ def to_ruby; end
17
+
18
+ sig {abstract.returns(T::Array[String])}
19
+ def dependencies; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'tsort'
5
+ module Yogurt
6
+ class CodeGenerator
7
+ class DefinedClassSorter
8
+ extend T::Sig
9
+ include TSort
10
+
11
+ sig {params(classes: T::Array[DefinedClass]).void}
12
+ def initialize(classes)
13
+ @classes = T.let(
14
+ classes.map {|k| [k.name, k]}.to_h,
15
+ T::Hash[String, DefinedClass],
16
+ )
17
+ end
18
+
19
+ sig {returns(T::Array[DefinedClass])}
20
+ def sorted_classes
21
+ tsort
22
+ end
23
+
24
+ sig {params(block: T.proc.params(arg0: DefinedClass).void).void}
25
+ private def tsort_each_node(&block)
26
+ @classes.keys.sort.each do |key|
27
+ yield(@classes.fetch(key))
28
+ end
29
+ end
30
+
31
+ sig do
32
+ params(
33
+ input_class: DefinedClass,
34
+ block: T.proc.params(arg0: DefinedClass).void,
35
+ ).void
36
+ end
37
+ private def tsort_each_child(input_class, &block)
38
+ input_class.dependencies.sort.each do |name|
39
+ yield(@classes.fetch(name))
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ module DefinedMethod
7
+ extend T::Sig
8
+ extend T::Helpers
9
+ include Kernel
10
+ abstract!
11
+
12
+ sig {abstract.returns(Symbol)}
13
+ def name; end
14
+
15
+ # Attempts to merge this method with the other defined method. Returns true if
16
+ # successful, false if they are incompatible.
17
+ sig {abstract.params(other: DefinedMethod).returns(T::Boolean)}
18
+ def merge?(other); end
19
+
20
+ sig {abstract.returns(String)}
21
+ def to_ruby; end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ # For GraphQL enum classes
7
+ class EnumClass < T::Struct
8
+ extend T::Sig
9
+ include Utils
10
+ include DefinedClass
11
+
12
+ const :name, String
13
+ const :serialized_values, T::Array[String]
14
+
15
+ sig {override.returns(T::Array[String])}
16
+ def dependencies
17
+ []
18
+ end
19
+
20
+ sig {override.returns(String)}
21
+ def to_ruby
22
+ existing_constants = []
23
+
24
+ definitions = serialized_values.sort.map do |name|
25
+ const_name = safe_constant_name(name, existing_constants)
26
+ existing_constants << const_name
27
+ "#{const_name} = new(#{name.inspect})"
28
+ end
29
+
30
+ <<~STRING
31
+ class #{name} < T::Enum
32
+ enums do
33
+ #{indent(definitions.join("\n"), 2).strip}
34
+ end
35
+ end
36
+ STRING
37
+ end
38
+
39
+ # Returns a valid Ruby constant name that doesn't conflict with the
40
+ # existing constants.
41
+ sig {params(desired_name: String, existing_constants: T::Array[String]).returns(String)}
42
+ private def safe_constant_name(desired_name, existing_constants)
43
+ desired_name = underscore(desired_name).upcase
44
+ base_desired_name = desired_name
45
+ escaping_level = 1
46
+
47
+ while existing_constants.include?(desired_name)
48
+ escaping_level += 1
49
+ desired_name = "#{base_desired_name}#{escaping_level}"
50
+ end
51
+
52
+ desired_name
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,273 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ # Method that is used to access a field on an object
7
+ class FieldAccessMethod < T::Struct
8
+ extend T::Sig
9
+ include Memoize
10
+ include DefinedMethod
11
+ include Utils
12
+
13
+ # Indicates the possible object types that could occur at runtime, and which expressions
14
+ # should be used if that object type appears.
15
+ class FragmentBranch < T::Struct
16
+ extend T::Sig
17
+ include Comparable
18
+ include Memoize
19
+ include Utils
20
+
21
+ const :typenames, T::Set[String]
22
+ const :expression, String
23
+
24
+ sig {returns(T::Array[String])}
25
+ def sorted_typenames
26
+ memoize_as(:sorted_typenames) {typenames.to_a.sort}
27
+ end
28
+
29
+ sig {returns(String)}
30
+ def to_ruby
31
+ <<~STRING.strip
32
+ when #{sorted_typenames.map(&:inspect).join(', ')}
33
+ #{indent(expression, 1)}
34
+ STRING
35
+ end
36
+
37
+ sig {override.params(other: T.untyped).returns(T.nilable(Integer))}
38
+ def <=>(other)
39
+ return unless other.is_a?(FragmentBranch)
40
+
41
+ comparison = sorted_typenames <=> other.sorted_typenames
42
+ return comparison if comparison != 0
43
+
44
+ expression <=> other.expression
45
+ end
46
+ end
47
+
48
+ IMPOSSIBLE_FIELD_SIGNATURE = T.let("NilClass", String)
49
+ IMPOSSIBLE_FIELD_BODY = T.let(<<~STRING, String)
50
+ # The combination of fragments used to retrieve this field make it impossible
51
+ # for the field to have any value other than `nil`.
52
+ nil
53
+ STRING
54
+
55
+ # Name of the method
56
+ const :name, Symbol
57
+
58
+ # Paths with the fragments that indicate how this method is accessed
59
+ const :field_access_paths, T::Array[FieldAccessPath]
60
+
61
+ # GraphQL schema for the query that is executing
62
+ const :schema, GRAPHQL_SCHEMA
63
+
64
+ # Attempts to merge this method with the other defined method. Returns true if
65
+ # successful, false if they are incompatible.
66
+ sig {override.params(other: DefinedMethod).returns(T::Boolean)}
67
+ def merge?(other)
68
+ return false unless other.is_a?(FieldAccessMethod)
69
+
70
+ field_access_paths.concat(other.field_access_paths)
71
+ true
72
+ end
73
+
74
+ sig {override.returns(String)}
75
+ def to_ruby
76
+ <<~STRING
77
+ sig {returns(#{signature})}
78
+ def #{name}
79
+ #{indent(body, 1).strip}
80
+ end
81
+ STRING
82
+ end
83
+
84
+ # Returns the different branches of execution that could happen based on the actual
85
+ # object type that is returned by the GraphQL query.
86
+ sig {returns(T::Array[FragmentBranch])}
87
+ def branches
88
+ reduce!
89
+ memoize_as(:branches) do
90
+ # Construct the branches for each of the possible fragments. When grouped by
91
+ # expression, the typenames possible for each branch should be disjoint. If they're
92
+ # not, the query would have been rejected as invalid by the `FieldsWillMerge`
93
+ # static validation rule.
94
+ result = field_access_paths.group_by(&:expression).map do |expression, group|
95
+ typenames = T.let(Set.new, T::Set[String])
96
+ group.each do |path|
97
+ typenames.merge(path.compatible_object_types)
98
+ end
99
+
100
+ FragmentBranch.new(expression: expression, typenames: typenames)
101
+ end
102
+
103
+ # Invariant: Make sure that the behavior of the world matches our expectations.
104
+ invalid_branches = result.combination(2).select do |b1, b2|
105
+ next if b1.nil?
106
+ next if b2.nil?
107
+
108
+ b1.typenames.intersect?(b2.typenames)
109
+ end
110
+
111
+ if invalid_branches.any?
112
+ raise <<~STRING
113
+ Some field access branches have overlapping types, but different field resolution expressions.
114
+ #{invalid_branches.map {|b1, b2| { branch1: T.must(b1).serialize, branch2: T.must(b2).serialize }}.inspect}
115
+ STRING
116
+ end
117
+
118
+ result.sort!
119
+ result.freeze
120
+ end
121
+ end
122
+
123
+ sig {returns(String)}
124
+ def body
125
+ reduce!
126
+ memoize_as(:body) do
127
+ break IMPOSSIBLE_FIELD_BODY if field_access_is_impossible?
128
+
129
+ if field_access_is_guaranteed? && branches.size == 1
130
+ branches.fetch(0).expression
131
+ elsif field_access_is_guaranteed?
132
+ <<~STRING
133
+ case (type = __typename)
134
+ #{branches.map(&:to_ruby).join("\n")}
135
+ else
136
+ __unexpected_type(field: #{name.inspect}, observed_type: type, expected_types: POSSIBLE_TYPES)
137
+ end
138
+ STRING
139
+ elsif branches.size == 1
140
+ branch = branches.fetch(0)
141
+ condition = branch.sorted_typenames.map {|type| "type == #{type.inspect}"}.join(' || ')
142
+ <<~STRING
143
+ type = __typename
144
+ return unless #{condition}
145
+ #{branch.expression}
146
+ STRING
147
+ else
148
+ <<~STRING
149
+ case (type = __typename)
150
+ #{branches.map(&:to_ruby).join("\n")}
151
+ end
152
+ STRING
153
+ end
154
+ end
155
+ end
156
+
157
+ sig {returns(String)}
158
+ def signature
159
+ reduce!
160
+ memoize_as(:signature) do
161
+ break IMPOSSIBLE_FIELD_SIGNATURE if field_access_is_impossible?
162
+
163
+ signatures = field_access_paths.map do |path|
164
+ signature = path.signature
165
+ next signature if !signature.start_with?("T.nilable")
166
+
167
+ # Preserve the original signature if we're guaranteed to always return this field
168
+ next signature if field_access_is_guaranteed?
169
+
170
+ # If fragments might cause the field to be omitted, strip off the nilability
171
+ # because we'll wrap the composite signature in a `T.nilable`
172
+ signature.delete_prefix("T.nilable(").delete_suffix(")")
173
+ end
174
+
175
+ signatures.uniq!
176
+ signatures.sort!
177
+
178
+ composite_signature = if signatures.size == 1
179
+ signatures.fetch(0)
180
+ else
181
+ "T.any(#{signatures.join(', ')})"
182
+ end
183
+
184
+ if field_access_is_guaranteed?
185
+ composite_signature
186
+ else
187
+ "T.nilable(#{composite_signature})"
188
+ end
189
+ end
190
+ end
191
+
192
+ sig {returns(T::Boolean)}
193
+ def field_access_is_impossible?
194
+ reduce!
195
+ field_access_paths.none?
196
+ end
197
+
198
+ # Returns true if this field will always be evaluated when the query is run.
199
+ # Returns false if it's possible for the field to be excluded from the query because
200
+ # the types of the fragments might not match the type of the object.
201
+ sig {returns(T::Boolean)}
202
+ def field_access_is_guaranteed?
203
+ reduce!
204
+ memoize_as(:field_access_is_guaranteed?) do
205
+ # Field access is not guaranteed if, after eliminating all of the unnecessary field
206
+ # access paths, there are any that only return a value for a subset of the possible
207
+ # field types at the root of the fragment.
208
+ field_access_paths.all? do |path|
209
+ root_possible_types.subset?(path.compatible_object_types)
210
+ end
211
+ end
212
+ end
213
+
214
+ # Returns the types of objects that are possible at the root of all of the
215
+ # field access paths. This will be the same for all of the field access paths,
216
+ # since they should all be starting from the same place.
217
+ sig {returns(T::Set[String])}
218
+ private def root_possible_types
219
+ reduce!
220
+ memoize_as(:root_possible_types) do
221
+ root_type = field_access_paths[0]&.fragment_types&.fetch(0)
222
+ break Set.new if root_type.nil?
223
+
224
+ raise "Invariant violated: Expected all FieldAccessPath's to have the same root fragment type." if !field_access_paths.all? {|path| path.fragment_types.fetch(0) == root_type}
225
+
226
+ schema.possible_types(schema.types[root_type]).map(&:graphql_name).to_set
227
+ end
228
+ end
229
+
230
+ # Eliminates field access paths that are impossible or reduntant
231
+ sig {void}
232
+ private def reduce!
233
+ memoize_as(:reduce!) do
234
+ # Eliminate paths where there are no objects that could possibly satisfy the fragment conditions
235
+ #
236
+ # For example:
237
+ #
238
+ # query {
239
+ # node(id: "foobar") {
240
+ # ... on Commit {
241
+ # ... on Node {
242
+ # ... on User {
243
+ # # This field can never be accessed, because User's will never be Commit's
244
+ # id
245
+ # }
246
+ # }
247
+ # }
248
+ # }
249
+ # }
250
+
251
+ field_access_paths.reject! {|path| path.compatible_object_types.empty?}
252
+
253
+ # Eliminate paths where the compatible object types are a subset of some other path's
254
+ # compatible object types. (These are redundant.)
255
+ supersets = T.let([], T::Array[FieldAccessPath])
256
+
257
+ # Put all of the supersets at the beginning of the array
258
+ field_access_paths.sort_by! {|path| -path.compatible_object_types.size}
259
+ field_access_paths.each do |path|
260
+ next if supersets.any? {|super_path| super_path.compatible_object_types.superset?(path.compatible_object_types)}
261
+
262
+ supersets.push(path)
263
+ end
264
+
265
+ field_access_paths.select! {|path| supersets.include?(path)}
266
+ field_access_paths.each(&:freeze)
267
+ field_access_paths.freeze
268
+ true
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ class FieldAccessPath < T::Struct
7
+ extend T::Sig
8
+ extend Utils
9
+ include Utils
10
+ include Memoize
11
+
12
+ # Name of the method
13
+ const :name, Symbol
14
+
15
+ # Sorbet signature for the value of the field
16
+ const :signature, String
17
+
18
+ # Expression for converting the value of the field
19
+ const :expression, String
20
+
21
+ # GraphQL schema for the query that this FieldAccessPath was derived from
22
+ const :schema, GRAPHQL_SCHEMA
23
+
24
+ # The types of all of the fragments leading to this field
25
+ const :fragment_types, T::Array[String]
26
+
27
+ sig {returns(T.self_type)}
28
+ def freeze
29
+ compatible_object_types
30
+ fragment_types.each(&:freeze)
31
+ fragment_types.freeze
32
+ expression.freeze
33
+ signature.freeze
34
+ super
35
+ self
36
+ end
37
+
38
+ # This field access path will only be evaluated if the object in the query
39
+ # is one of the objects in this set.
40
+ sig {returns(T::Set[String])}
41
+ def compatible_object_types
42
+ memoize_as(:compatible_object_types) do
43
+ result = schema.possible_types(schema.types[fragment_types.fetch(0)]).to_set
44
+
45
+ if fragment_types.size > 1
46
+ T.must(fragment_types[1..-1]).each do |next_type|
47
+ result = result.intersection(schema.possible_types(schema.types[next_type]))
48
+ end
49
+ end
50
+
51
+ result.map(&:graphql_name).to_set.freeze
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end