yogurt 0.1.1

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 (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