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,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ class VariableDefinition < T::Struct
7
+ extend T::Sig
8
+ include Comparable
9
+
10
+ const :name, Symbol
11
+ const :graphql_name, String
12
+ const :signature, String
13
+ const :serializer, String
14
+ const :dependency, T.nilable(String)
15
+
16
+ sig {returns(T::Boolean)}
17
+ def optional?
18
+ signature.start_with?("T.nilable")
19
+ end
20
+
21
+ sig {params(other: VariableDefinition).returns(T.nilable(Integer))}
22
+ def <=>(other)
23
+ if optional? && !other.optional?
24
+ 1
25
+ elsif other.optional? && !optional?
26
+ -1
27
+ else
28
+ name <=> other.name
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ module Converters
6
+ class Date
7
+ extend T::Sig
8
+ extend ScalarConverter
9
+
10
+ sig {override.returns(T::Types::Base)}
11
+ def self.type_alias
12
+ T.type_alias {::Date}
13
+ end
14
+
15
+ sig {override.params(value: ::Date).returns(String)}
16
+ def self.serialize(value)
17
+ value.iso8601
18
+ end
19
+
20
+ sig {override.params(raw_value: SCALAR_TYPE).returns(::Date)}
21
+ def self.deserialize(raw_value)
22
+ raise "Unexpected value returned for Date: #{raw_value.inspect}" if !raw_value.is_a?(String)
23
+
24
+ ::Date.iso8601(raw_value)
25
+ end
26
+ end
27
+
28
+ class Time
29
+ extend T::Sig
30
+ extend ScalarConverter
31
+
32
+ sig {override.returns(T::Types::Base)}
33
+ def self.type_alias
34
+ T.type_alias {::Time}
35
+ end
36
+
37
+ sig {override.params(value: ::Time).returns(String)}
38
+ def self.serialize(value)
39
+ value.iso8601
40
+ end
41
+
42
+ sig {override.params(raw_value: SCALAR_TYPE).returns(::Time)}
43
+ def self.deserialize(raw_value)
44
+ raise "Unexpected value returned for Time: #{raw_value.inspect}" if !raw_value.is_a?(String)
45
+
46
+ ::Time.iso8601(raw_value)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ module ErrorResult
6
+ include Kernel
7
+ extend T::Sig
8
+ extend T::Helpers
9
+
10
+ interface!
11
+
12
+ sig {abstract.returns(T.nilable(T::Array[OBJECT_TYPE]))}
13
+ def errors; end
14
+
15
+ class OnlyErrors
16
+ extend T::Sig
17
+ include ErrorResult
18
+
19
+ sig {params(errors: T::Array[OBJECT_TYPE]).void}
20
+ def initialize(errors)
21
+ @errors = T.let(errors, T::Array[OBJECT_TYPE])
22
+ end
23
+
24
+ sig {override.returns(T.nilable(T::Array[OBJECT_TYPE]))}
25
+ def errors
26
+ @errors
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,80 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ module Yogurt
9
+ class Http
10
+ extend T::Sig
11
+ include QueryExecutor
12
+
13
+ sig {params(uri: String, headers: T::Hash[String, String]).void}
14
+ def initialize(uri, headers: {})
15
+ parsed = URI.parse(uri)
16
+ if parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
17
+ @uri = T.let(parsed, T.any(URI::HTTP, URI::HTTPS))
18
+ else
19
+ raise ArgumentError, "Invalid URI: #{uri} (must be HTTP or HTTPS)"
20
+ end
21
+
22
+ @headers = T.let(headers, T::Hash[String, String])
23
+ end
24
+
25
+ # You can override this method in a subclass to set options on the HTTP request
26
+ sig {overridable.params(options: T.untyped).returns(T::Hash[String, String])}
27
+ def headers(options)
28
+ @headers
29
+ end
30
+
31
+ # You can override this in a subclass to get strongly typed options on auto-generated code
32
+ sig {override.returns(T::Types::Base)}
33
+ def options_type_alias
34
+ T.type_alias {T.untyped}
35
+ end
36
+
37
+ sig do
38
+ override.params(
39
+ query: String,
40
+ operation_name: String,
41
+ variables: T.nilable(T::Hash[String, T.untyped]),
42
+ options: T.untyped,
43
+ ).returns(T::Hash[String, T.untyped])
44
+ end
45
+ def execute(query, operation_name:, variables: nil, options: nil)
46
+ request = Net::HTTP::Post.new(@uri.request_uri)
47
+ request.basic_auth(@uri.user, @uri.password) if @uri.user || @uri.password
48
+
49
+ request["Accept"] = "application/json"
50
+ request["Content-Type"] = "application/json"
51
+
52
+ headers(options).each do |name, value|
53
+ request[name] = value
54
+ end
55
+
56
+ body = T.let({}, T::Hash[String, T.untyped])
57
+ body["query"] = query
58
+ body["variables"] = variables if variables&.any?
59
+ body["operationName"] = operation_name
60
+ request.body = JSON.generate(body)
61
+
62
+ response = connection.request(request)
63
+ case response
64
+ when Net::HTTPOK, Net::HTTPBadRequest
65
+ JSON.parse(response.body)
66
+ else
67
+ { "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
68
+ end
69
+ end
70
+
71
+ # Returns an HTTP connection. You can override in subclasses to customize the conection.
72
+ sig {overridable.returns(Net::HTTP)}
73
+ def connection
74
+ client = Net::HTTP.new(@uri.host, @uri.port)
75
+ client.use_ssl = @uri.scheme == "https"
76
+
77
+ client
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'pp'
5
+
6
+ module Yogurt
7
+ module Inspectable
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ include PP::ObjectMixin
11
+
12
+ abstract!
13
+
14
+ sig {abstract.params(p: PP::PPMethods).void}
15
+ def pretty_print(p); end
16
+
17
+ sig {returns(String)}
18
+ def inspect
19
+ pretty_print_inspect
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ module Memoize
6
+ extend T::Sig
7
+
8
+ sig do
9
+ type_parameters(:ReturnType)
10
+ .params(
11
+ name: Symbol,
12
+ block: T.proc.returns(T.type_parameter(:ReturnType)),
13
+ )
14
+ .returns(T.type_parameter(:ReturnType))
15
+ end
16
+ def memoize_as(name, &block)
17
+ memoized_values = @memoized_values
18
+ memoized_values = @memoized_values = {} if memoized_values.nil?
19
+
20
+ return memoized_values[name] if memoized_values.key?(name)
21
+
22
+ memoized_values[name] = yield
23
+ end
24
+
25
+ sig {returns(T.self_type)}
26
+ def freeze
27
+ @memoized_values = T.let(@memoized_values, T.nilable(T::Hash[Symbol, T.untyped]))
28
+ @memoized_values&.freeze
29
+ super
30
+ self
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class Query
6
+ extend T::Sig
7
+ extend T::Helpers
8
+ abstract!
9
+
10
+ sig {params(result: OBJECT_TYPE).returns(T.any(T.attached_class, Yogurt::ErrorResult::OnlyErrors))}
11
+ def self.from_result(result)
12
+ data = result['data']
13
+ if data
14
+ new(data, result['errors'])
15
+ else
16
+ Yogurt::ErrorResult::OnlyErrors.new(result['errors'])
17
+ end
18
+ end
19
+
20
+ sig {params(data: OBJECT_TYPE, errors: T.nilable(OBJECT_TYPE)).void}
21
+ def initialize(data, errors); end
22
+ end
23
+ end
@@ -0,0 +1,89 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ module QueryContainer
6
+ extend T::Sig
7
+
8
+ CONTAINER = T.type_alias do
9
+ T.all(Module, QueryContainer)
10
+ end
11
+
12
+ VALIDATION_RULES = T.let(
13
+ [
14
+ *GraphQL::StaticValidation::ALL_RULES,
15
+ InterfacesAndUnionsHaveTypename
16
+ ].freeze,
17
+ T::Array[T.untyped],
18
+ )
19
+
20
+ sig {params(other: Module).void}
21
+ def self.included(other)
22
+ Kernel.raise(ValidationError, "You need to `extend Yogurt::QueryContainer`, you cannot use `include`.")
23
+ end
24
+
25
+ sig {params(other: Module).void}
26
+ def self.extended(other)
27
+ super
28
+ QueryContainer.containers << T.cast(other, CONTAINER)
29
+ end
30
+
31
+ sig {returns(T::Array[CONTAINER])}
32
+ def self.containers
33
+ @containers = T.let(@containers, T.nilable(T::Array[CONTAINER]))
34
+ @containers ||= []
35
+ end
36
+
37
+ sig {returns(T::Array[QueryDeclaration])}
38
+ def declared_queries
39
+ @declared_queries = T.let(@declared_queries, T.nilable(T::Array[QueryDeclaration]))
40
+ @declared_queries ||= []
41
+ end
42
+
43
+ sig {params(operation_name: String).returns(QueryDeclaration)}
44
+ def fetch_query(operation_name)
45
+ result = declared_queries.detect {|d| d.operations.include?(operation_name)}
46
+ T.must(result)
47
+ end
48
+
49
+ sig do
50
+ params(
51
+ query_text: String,
52
+ schema: T.nilable(T.class_of(GraphQL::Schema)),
53
+ ).void
54
+ end
55
+ def declare_query(query_text, schema: nil)
56
+ schema ||= Yogurt.default_schema
57
+ Kernel.raise(ValidationError, "You need to either provide a `schema:` to declare_query, or set Yogurt.default_schema") if schema.nil?
58
+
59
+ case (container = self)
60
+ when Module
61
+ # noop
62
+ else
63
+ Kernel.raise(ValidationError, "You need to `extend Yogurt::QueryContainer`, you cannot use `include`.")
64
+ end
65
+
66
+ Kernel.raise(ValidationError, "Query containers must be classes or modules that are assigned to constants.") if container.name.nil?
67
+
68
+ validator = GraphQL::StaticValidation::Validator.new(schema: schema, rules: VALIDATION_RULES)
69
+ query = GraphQL::Query.new(schema, query_text)
70
+ validation_result = validator.validate(query)
71
+
72
+ error = validation_result[:errors][0]
73
+ Kernel.raise(ValidationError, error.message) if error
74
+
75
+ if query.operations.key?(nil)
76
+ Kernel.raise(ValidationError, "You must provide a name for each of the operations in your GraphQL query.")
77
+ elsif query.operations.none?
78
+ Kernel.raise(ValidationError, "Your query did not define any operations.")
79
+ end
80
+
81
+ declared_queries << QueryDeclaration.new(
82
+ container: container,
83
+ operations: query.operations.keys.freeze,
84
+ query_text: query_text,
85
+ schema: schema,
86
+ )
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,107 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ module QueryContainer
6
+ module InterfacesAndUnionsHaveTypename
7
+ include Kernel
8
+ extend T::Sig
9
+
10
+ class Error < GraphQL::StaticValidation::Error
11
+ extend T::Sig
12
+
13
+ sig {returns(T.nilable(String))}
14
+ attr_reader :type_name
15
+
16
+ sig {returns(String)}
17
+ attr_reader :node_name
18
+
19
+ sig do
20
+ params(
21
+ message: String,
22
+ node_name: String,
23
+ path: T.nilable(String),
24
+ nodes: T.untyped,
25
+ type: T.nilable(String),
26
+ ).void
27
+ end
28
+ def initialize(message, node_name:, path: nil, nodes: [], type: nil)
29
+ super(message, path: path, nodes: nodes)
30
+ @node_name = T.let(node_name, String)
31
+ @type_name = T.let(type, T.nilable(String))
32
+ end
33
+
34
+ # A hash representation of this Message
35
+ sig {returns(T::Hash[String, T.untyped])}
36
+ def to_h
37
+ extensions = {
38
+ "code" => code,
39
+ "nodeName" => @node_name
40
+ }
41
+
42
+ extensions['typeName'] = @type_name if @type_name
43
+ super.merge({ "extensions" => extensions })
44
+ end
45
+
46
+ sig {returns(String)}
47
+ def code
48
+ "interfaceOrUnionMissingTypename"
49
+ end
50
+ end
51
+
52
+ sig {params(node: GraphQL::Language::Nodes::Field, parent: T.untyped).void}
53
+ def on_field(node, parent)
54
+ super if validate_interface_union_includes_typename(node, T.unsafe(self).field_definition.type.unwrap)
55
+ end
56
+
57
+ sig {params(node: GraphQL::Language::Nodes::OperationDefinition, parent: T.untyped).void}
58
+ def on_operation_definition(node, parent)
59
+ super if validate_interface_union_includes_typename(node, T.unsafe(self).type_definition)
60
+ end
61
+
62
+ sig do
63
+ params(
64
+ node: T.any(GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::OperationDefinition),
65
+ type_definition: T.untyped,
66
+ ).returns(T::Boolean)
67
+ end
68
+ private def validate_interface_union_includes_typename(node, type_definition)
69
+ return true if node.selections.nil?
70
+ return true if node.selections.empty?
71
+
72
+ type_kind = type_definition.kind
73
+ return true unless type_kind.interface? || type_kind.union?
74
+ return true if node.selections.any? do |selection|
75
+ next false unless selection.is_a?(GraphQL::Language::Nodes::Field)
76
+ next false unless selection.name == '__typename'
77
+
78
+ selection.alias.nil?
79
+ end
80
+
81
+ msg = "Interfaces and unions must include the __typename field (%{node_name} returns #{type_definition.graphql_name} but doesn't select __typename)"
82
+ node_name = case node
83
+ when GraphQL::Language::Nodes::Field
84
+ "field '#{node.name}'"
85
+ when GraphQL::Language::Nodes::OperationDefinition
86
+ if node.name.nil?
87
+ "anonymous query"
88
+ else
89
+ "#{node.operation_type} '#{node.name}'"
90
+ end
91
+ else
92
+ T.absurd(node)
93
+ end
94
+
95
+ error = Error.new(
96
+ format(msg, node_name: node_name),
97
+ nodes: node,
98
+ node_name: node_name,
99
+ type: type_definition.graphql_name,
100
+ )
101
+
102
+ send(:add_error, error)
103
+ false
104
+ end
105
+ end
106
+ end
107
+ end