yogurt 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/tests.yml +117 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +113 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +201 -0
- data/README.md +38 -0
- data/Rakefile +9 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/srb +29 -0
- data/bin/srb-rbi +29 -0
- data/lib/yogurt.rb +95 -0
- data/lib/yogurt/code_generator.rb +706 -0
- data/lib/yogurt/code_generator/defined_class.rb +22 -0
- data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
- data/lib/yogurt/code_generator/defined_method.rb +24 -0
- data/lib/yogurt/code_generator/enum_class.rb +56 -0
- data/lib/yogurt/code_generator/field_access_method.rb +273 -0
- data/lib/yogurt/code_generator/field_access_path.rb +56 -0
- data/lib/yogurt/code_generator/generated_file.rb +53 -0
- data/lib/yogurt/code_generator/input_class.rb +52 -0
- data/lib/yogurt/code_generator/leaf_class.rb +68 -0
- data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
- data/lib/yogurt/code_generator/root_class.rb +130 -0
- data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
- data/lib/yogurt/code_generator/typed_input.rb +12 -0
- data/lib/yogurt/code_generator/typed_output.rb +16 -0
- data/lib/yogurt/code_generator/utils.rb +140 -0
- data/lib/yogurt/code_generator/variable_definition.rb +33 -0
- data/lib/yogurt/converters.rb +50 -0
- data/lib/yogurt/error_result.rb +30 -0
- data/lib/yogurt/http.rb +80 -0
- data/lib/yogurt/inspectable.rb +22 -0
- data/lib/yogurt/memoize.rb +33 -0
- data/lib/yogurt/query.rb +23 -0
- data/lib/yogurt/query_container.rb +89 -0
- data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
- data/lib/yogurt/query_declaration.rb +11 -0
- data/lib/yogurt/query_executor.rb +23 -0
- data/lib/yogurt/query_result.rb +25 -0
- data/lib/yogurt/scalar_converter.rb +19 -0
- data/lib/yogurt/unexpected_object_type.rb +30 -0
- data/lib/yogurt/validation_error.rb +6 -0
- data/lib/yogurt/version.rb +6 -0
- data/sorbet/config +10 -0
- data/sorbet/rbi/fake_schema.rbi +14 -0
- data/sorbet/rbi/graphql.rbi +11 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
- data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
- data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
- data/sorbet/rbi/todo.rbi +6 -0
- data/yogurt.gemspec +54 -0
- 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
|
data/lib/yogurt/http.rb
ADDED
@@ -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
|
data/lib/yogurt/query.rb
ADDED
@@ -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
|