fragment-dev 2.0.0 → 2.1.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.
- checksums.yaml +4 -4
- data/lib/fragment.schema.json +824 -103
- data/lib/fragment_client/graphql_ast.rb +73 -0
- data/lib/fragment_client/typed_entries.rb +444 -0
- data/lib/fragment_client/typed_ledger_entry.rb +281 -0
- data/lib/fragment_client/version.rb +1 -1
- data/lib/fragment_client.rb +169 -16
- data/lib/queries.graphql +46 -0
- data/lib/tapioca/dsl/compilers/fragment_query_methods.rb +61 -0
- data/lib/tapioca/dsl/compilers/fragment_response_types.rb +222 -0
- data/lib/tapioca/dsl/compilers/fragment_typed_entries.rb +169 -0
- data/lib/tapioca/dsl/helpers/graphql_sorbet_types.rb +53 -0
- metadata +9 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(Tapioca::Dsl::Compilers)
|
|
5
|
+
|
|
6
|
+
require 'fragment_client'
|
|
7
|
+
require 'tapioca/dsl/helpers/graphql_sorbet_types'
|
|
8
|
+
|
|
9
|
+
module Tapioca
|
|
10
|
+
module Dsl
|
|
11
|
+
module Compilers
|
|
12
|
+
# Declares the response objects the query methods return, so that
|
|
13
|
+
# `client.get_ledger(...).data.ledger.name` typechecks.
|
|
14
|
+
#
|
|
15
|
+
# graphql-client builds those objects at runtime from anonymous classes whose
|
|
16
|
+
# fields are served by `method_missing`, so there is nothing to reflect on.
|
|
17
|
+
# The shape is instead derived statically from each operation's selection set
|
|
18
|
+
# and the pinned Schema -- the same two inputs graphql-client itself uses,
|
|
19
|
+
# which is what makes the declared types agree with the runtime.
|
|
20
|
+
#
|
|
21
|
+
# In particular graphql-client refuses at runtime to read a field the
|
|
22
|
+
# operation did not select (`ImplicitlyFetchedFieldError`), so a type derived
|
|
23
|
+
# from the selection set cannot promise more than the runtime allows.
|
|
24
|
+
#
|
|
25
|
+
# Emitted under `FragmentClient::Responses::<Operation>`, one nested class per
|
|
26
|
+
# selection level.
|
|
27
|
+
class FragmentResponseTypes < Compiler
|
|
28
|
+
extend T::Sig
|
|
29
|
+
|
|
30
|
+
ConstantType = type_member { { fixed: T.class_of(FragmentClient) } }
|
|
31
|
+
|
|
32
|
+
# Scalar names this Schema uses, mapped for `GraphqlSorbetTypes`.
|
|
33
|
+
SCALARS = T.let(
|
|
34
|
+
{
|
|
35
|
+
'String' => '::String', 'ID' => '::String', 'SafeString' => '::String',
|
|
36
|
+
'ParameterizedString' => '::String', 'Date' => '::String',
|
|
37
|
+
'DateTime' => '::String', 'FirstMoment' => '::String',
|
|
38
|
+
'LastMoment' => '::String', 'Period' => '::String',
|
|
39
|
+
'PeriodFilter' => '::String', 'UTCOffset' => '::String',
|
|
40
|
+
'Int96' => '::String', 'Int' => '::Integer', 'Float' => '::Float',
|
|
41
|
+
'Boolean' => 'T::Boolean', 'JSON' => 'T.untyped', 'Parameters' => 'T.untyped'
|
|
42
|
+
}.freeze,
|
|
43
|
+
T::Hash[String, String]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
RESPONSES_NAMESPACE = 'FragmentClient::Responses'
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
extend T::Sig
|
|
50
|
+
|
|
51
|
+
sig { override.returns(T::Enumerable[Module]) }
|
|
52
|
+
def gather_constants
|
|
53
|
+
[FragmentClient]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { override.void }
|
|
58
|
+
def decorate
|
|
59
|
+
operations = FragmentGraphQl.operations
|
|
60
|
+
return if operations.empty?
|
|
61
|
+
|
|
62
|
+
root.create_module(RESPONSES_NAMESPACE) do |namespace|
|
|
63
|
+
operations.keys.sort.each do |name|
|
|
64
|
+
declare_operation(namespace, name, T.must(operations[name]))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
sig do
|
|
72
|
+
params(namespace: RBI::Scope, name: String,
|
|
73
|
+
operation: GraphQL::Language::Nodes::OperationDefinition).void
|
|
74
|
+
end
|
|
75
|
+
def declare_operation(namespace, name, operation)
|
|
76
|
+
root_type = operation.operation_type == 'mutation' ? schema.mutation : schema.query
|
|
77
|
+
return if root_type.nil?
|
|
78
|
+
|
|
79
|
+
namespace.create_class(name) do |response|
|
|
80
|
+
response.create_method('data', return_type: "T.nilable(#{RESPONSES_NAMESPACE}::#{name}::Data)")
|
|
81
|
+
response.create_method('errors', return_type: 'T.untyped')
|
|
82
|
+
response.create_method('original_hash', return_type: 'T::Hash[String, T.untyped]')
|
|
83
|
+
declare_object(response, 'Data', operation.selections, root_type)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# One nested class per selection level. `path` is its fully qualified name,
|
|
88
|
+
# which children extend so nested references resolve.
|
|
89
|
+
sig do
|
|
90
|
+
params(parent: RBI::Scope, class_name: String, selections: T::Array[T.untyped],
|
|
91
|
+
graphql_type: T.untyped).void
|
|
92
|
+
end
|
|
93
|
+
def declare_object(parent, class_name, selections, graphql_type)
|
|
94
|
+
fields = collect_fields(selections, graphql_type)
|
|
95
|
+
|
|
96
|
+
parent.create_class(class_name) do |scope|
|
|
97
|
+
fields.each do |field|
|
|
98
|
+
scope.create_method(field.fetch(:method), return_type: field.fetch(:type),
|
|
99
|
+
comments: field.fetch(:comments))
|
|
100
|
+
nested = field[:nested]
|
|
101
|
+
next unless nested
|
|
102
|
+
|
|
103
|
+
declare_object(scope, nested.fetch(:class_name), nested.fetch(:selections),
|
|
104
|
+
nested.fetch(:type))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Flatten a selection set into field descriptors, following inline fragments.
|
|
110
|
+
#
|
|
111
|
+
# A union or interface selection contributes every branch's fields, each
|
|
112
|
+
# made nilable: graphql-client returns one object whatever the `__typename`,
|
|
113
|
+
# and only the branch that matched carries values.
|
|
114
|
+
sig do
|
|
115
|
+
params(selections: T::Array[T.untyped], graphql_type: T.untyped, force_nilable: T::Boolean,
|
|
116
|
+
taken: T::Set[String])
|
|
117
|
+
.returns(T::Array[T::Hash[Symbol, T.untyped]])
|
|
118
|
+
end
|
|
119
|
+
def collect_fields(selections, graphql_type, force_nilable: false, taken: Set.new)
|
|
120
|
+
selections.flat_map do |selection|
|
|
121
|
+
case selection
|
|
122
|
+
when GraphQL::Language::Nodes::Field
|
|
123
|
+
field = describe_field(selection, graphql_type, force_nilable, taken)
|
|
124
|
+
field ? [field] : []
|
|
125
|
+
when GraphQL::Language::Nodes::InlineFragment
|
|
126
|
+
branch = selection.type ? lookup_type(selection.type.name) : graphql_type
|
|
127
|
+
next [] if branch.nil?
|
|
128
|
+
|
|
129
|
+
collect_fields(selection.selections, branch, force_nilable: true, taken: taken)
|
|
130
|
+
else
|
|
131
|
+
[]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig do
|
|
137
|
+
params(selection: GraphQL::Language::Nodes::Field, graphql_type: T.untyped,
|
|
138
|
+
force_nilable: T::Boolean, taken: T::Set[String])
|
|
139
|
+
.returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
|
140
|
+
end
|
|
141
|
+
def describe_field(selection, graphql_type, force_nilable, taken)
|
|
142
|
+
name = selection.alias || selection.name
|
|
143
|
+
method = ActiveSupport::Inflector.underscore(name)
|
|
144
|
+
return nil if taken.include?(method)
|
|
145
|
+
|
|
146
|
+
taken << method
|
|
147
|
+
return { method: method, type: '::String', comments: [], nested: nil } if name == '__typename'
|
|
148
|
+
|
|
149
|
+
field = graphql_type.respond_to?(:fields) ? graphql_type.fields[selection.name] : nil
|
|
150
|
+
return { method: method, type: 'T.untyped', comments: [], nested: nil } if field.nil?
|
|
151
|
+
|
|
152
|
+
build_field(selection, field, method, name, force_nilable, taken)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
sig do
|
|
156
|
+
params(selection: GraphQL::Language::Nodes::Field, field: T.untyped, method: String,
|
|
157
|
+
name: String, force_nilable: T::Boolean, taken: T::Set[String])
|
|
158
|
+
.returns(T::Hash[Symbol, T.untyped])
|
|
159
|
+
end
|
|
160
|
+
def build_field(selection, field, method, name, force_nilable, taken)
|
|
161
|
+
unwrapped = field.type.unwrap
|
|
162
|
+
nilable = force_nilable || !field.type.non_null?
|
|
163
|
+
|
|
164
|
+
if selection.selections.empty?
|
|
165
|
+
return { method: method, type: wrap(scalar_type(unwrapped), field.type, nilable),
|
|
166
|
+
comments: [comment("`#{name}`: #{field.type.to_type_signature}")], nested: nil }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class_name = ActiveSupport::Inflector.camelize(method)
|
|
170
|
+
class_name = "#{class_name}Object" if taken.include?("class:#{class_name}")
|
|
171
|
+
taken << "class:#{class_name}"
|
|
172
|
+
|
|
173
|
+
{ method: method, type: wrap(class_name, field.type, nilable),
|
|
174
|
+
comments: [comment("`#{name}`: #{field.type.to_type_signature}")],
|
|
175
|
+
nested: { class_name: class_name, selections: selection.selections,
|
|
176
|
+
type: unwrapped } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Apply the GraphQL type's list and nullability wrappers to `inner`.
|
|
180
|
+
#
|
|
181
|
+
# `[Ledger!]!` becomes `T::Array[X]`, `[Ledger]` becomes
|
|
182
|
+
# `T::Array[T.nilable(X)]`. `T.untyped` already includes nil, so it is never
|
|
183
|
+
# wrapped again.
|
|
184
|
+
sig { params(inner: String, graphql_type: T.untyped, nilable: T::Boolean).returns(String) }
|
|
185
|
+
def wrap(inner, graphql_type, nilable)
|
|
186
|
+
bare = graphql_type.non_null? ? graphql_type.of_type : graphql_type
|
|
187
|
+
type = if bare.list?
|
|
188
|
+
"T::Array[#{nilable_unless(inner, bare.of_type.non_null?)}]"
|
|
189
|
+
else
|
|
190
|
+
inner
|
|
191
|
+
end
|
|
192
|
+
nilable_unless(type, !nilable)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { params(type: String, non_null: T::Boolean).returns(String) }
|
|
196
|
+
def nilable_unless(type, non_null)
|
|
197
|
+
non_null || type == 'T.untyped' ? type : "T.nilable(#{type})"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
sig { params(unwrapped: T.untyped).returns(String) }
|
|
201
|
+
def scalar_type(unwrapped)
|
|
202
|
+
Helpers::GraphqlSorbetTypes.translate(unwrapped.graphql_name, scalars: SCALARS)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
sig { params(name: String).returns(T.untyped) }
|
|
206
|
+
def lookup_type(name)
|
|
207
|
+
schema.types[name]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
sig { returns(T.untyped) }
|
|
211
|
+
def schema
|
|
212
|
+
FragmentGraphQl::FragmentSchema
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
sig { params(text: String).returns(RBI::Comment) }
|
|
216
|
+
def comment(text)
|
|
217
|
+
RBI::Comment.new(text)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(Tapioca::Dsl::Compilers)
|
|
5
|
+
|
|
6
|
+
require 'fragment_client'
|
|
7
|
+
require 'tapioca/dsl/helpers/graphql_sorbet_types'
|
|
8
|
+
|
|
9
|
+
module Tapioca
|
|
10
|
+
module Dsl
|
|
11
|
+
module Compilers
|
|
12
|
+
# Declares the typed payload classes {FragmentClient::TypedEntries.load}
|
|
13
|
+
# builds at load time, giving each a real `initialize` signature and typed
|
|
14
|
+
# readers.
|
|
15
|
+
#
|
|
16
|
+
# `bundle exec tapioca dsl` writes them to
|
|
17
|
+
# `sorbet/rbi/dsl/fragment_client/entries/`. The classes are only visible if
|
|
18
|
+
# whatever calls `TypedEntries.load` runs when Tapioca loads the application;
|
|
19
|
+
# it needs no credentials, so an initializer works.
|
|
20
|
+
class FragmentTypedEntries < Compiler
|
|
21
|
+
extend T::Sig
|
|
22
|
+
|
|
23
|
+
ConstantType = type_member { { fixed: T.class_of(FragmentClient::TypedLedgerEntry) } }
|
|
24
|
+
|
|
25
|
+
# Sorbet types for the scalars a Fragment Schema binds parameters to.
|
|
26
|
+
# Anything absent -- enums, input objects, newer scalars -- falls through to
|
|
27
|
+
# `T.untyped` rather than a guess that could reject a valid call.
|
|
28
|
+
GRAPHQL_SCALARS = T.let(
|
|
29
|
+
{
|
|
30
|
+
'String' => '::String',
|
|
31
|
+
'ID' => '::String',
|
|
32
|
+
'SafeString' => '::String',
|
|
33
|
+
'ParameterizedString' => '::String',
|
|
34
|
+
# ISO 8601 strings, not Ruby date objects.
|
|
35
|
+
'Date' => '::String',
|
|
36
|
+
'DateTime' => '::String',
|
|
37
|
+
'FirstMoment' => '::String',
|
|
38
|
+
'LastMoment' => '::String',
|
|
39
|
+
'Period' => '::String',
|
|
40
|
+
'PeriodFilter' => '::String',
|
|
41
|
+
'UTCOffset' => '::String',
|
|
42
|
+
# Fragment's big integers are strings on the wire.
|
|
43
|
+
'Int96' => '::String',
|
|
44
|
+
'Int' => '::Integer',
|
|
45
|
+
'Float' => '::Float',
|
|
46
|
+
'Boolean' => 'T::Boolean',
|
|
47
|
+
'JSON' => 'T.untyped',
|
|
48
|
+
'Parameters' => 'T.untyped'
|
|
49
|
+
}.freeze,
|
|
50
|
+
T::Hash[String, String]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# The optional common fields, in the order the base class declares them.
|
|
54
|
+
COMMON_OPTIONAL_FIELDS = T.let(
|
|
55
|
+
{
|
|
56
|
+
posted: '::String',
|
|
57
|
+
description: '::String',
|
|
58
|
+
tags: 'T::Array[T.untyped]',
|
|
59
|
+
groups: 'T::Array[T.untyped]',
|
|
60
|
+
conditions: 'T::Array[T.untyped]'
|
|
61
|
+
}.freeze,
|
|
62
|
+
T::Hash[Symbol, String]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
extend T::Sig
|
|
67
|
+
|
|
68
|
+
sig { override.returns(T::Enumerable[Module]) }
|
|
69
|
+
def gather_constants
|
|
70
|
+
# `name_of` is nil for a payload in an anonymous namespace, whose name
|
|
71
|
+
# (`#<Module:0x...>::AuthCaptureV1`) Sorbet cannot resolve anyway.
|
|
72
|
+
descendants_of(FragmentClient::TypedLedgerEntry).select { |klass| name_of(klass) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { override.void }
|
|
77
|
+
def decorate
|
|
78
|
+
spec = constant.spec
|
|
79
|
+
|
|
80
|
+
# `create_path` would omit the superclass, leaving Sorbet unaware that a
|
|
81
|
+
# payload inherits `to_entry_input`, `set?` and the common-field readers.
|
|
82
|
+
root.create_class(constant.to_s,
|
|
83
|
+
superclass_name: '::FragmentClient::TypedLedgerEntry') do |payload|
|
|
84
|
+
payload.create_method('initialize', parameters: initialize_parameters(spec),
|
|
85
|
+
return_type: 'void')
|
|
86
|
+
|
|
87
|
+
spec.parameters.each do |parameter|
|
|
88
|
+
payload.create_method(parameter.name.to_s, return_type: reader_type(parameter),
|
|
89
|
+
comments: parameter_comments(parameter))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
payload.create_method('entry_type', return_type: '::String', class_method: true,
|
|
93
|
+
comments: [comment(spec.entry_type.inspect)])
|
|
94
|
+
payload.create_method('type_version', return_type: '::Integer', class_method: true,
|
|
95
|
+
comments: [comment(spec.type_version.to_s)])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
sig do
|
|
102
|
+
params(spec: FragmentClient::TypedEntries::EntrySpec)
|
|
103
|
+
.returns(T::Array[RBI::TypedParam])
|
|
104
|
+
end
|
|
105
|
+
def initialize_parameters(spec)
|
|
106
|
+
required, optional = spec.parameters.partition(&:required)
|
|
107
|
+
|
|
108
|
+
# Required before optional: Sorbet rejects a `sig` that interleaves them,
|
|
109
|
+
# though Ruby permits it for keyword arguments. Source order survives
|
|
110
|
+
# within each group and on the wire, which is where spec 2.4 applies.
|
|
111
|
+
parameters = [
|
|
112
|
+
create_kw_param('ik', type: '::String'),
|
|
113
|
+
create_kw_param('ledger_ik', type: '::String')
|
|
114
|
+
]
|
|
115
|
+
required.each do |parameter|
|
|
116
|
+
parameters << create_kw_param(parameter.name.to_s, type: sorbet_type(parameter))
|
|
117
|
+
end
|
|
118
|
+
optional.each do |parameter|
|
|
119
|
+
parameters << create_kw_opt_param(parameter.name.to_s, type: optional_type(parameter),
|
|
120
|
+
default: 'T.unsafe(nil)')
|
|
121
|
+
end
|
|
122
|
+
COMMON_OPTIONAL_FIELDS.each do |name, type|
|
|
123
|
+
parameters << create_kw_opt_param(name.to_s, type: nilable(type),
|
|
124
|
+
default: 'T.unsafe(nil)')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
parameters
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
|
|
131
|
+
def reader_type(parameter)
|
|
132
|
+
parameter.required ? sorbet_type(parameter) : optional_type(parameter)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
|
|
136
|
+
def optional_type(parameter)
|
|
137
|
+
nilable(sorbet_type(parameter))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Optional fields are plainly nilable: the unset sentinel is private to the
|
|
141
|
+
# constructor and never reaches a caller.
|
|
142
|
+
sig { params(type: String).returns(String) }
|
|
143
|
+
def nilable(type)
|
|
144
|
+
type == 'T.untyped' ? 'T.untyped' : "T.nilable(#{type})"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
|
|
148
|
+
def sorbet_type(parameter)
|
|
149
|
+
Helpers::GraphqlSorbetTypes.translate(parameter.graphql_type, scalars: GRAPHQL_SCALARS)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(T::Array[RBI::Comment]) }
|
|
153
|
+
def parameter_comments(parameter)
|
|
154
|
+
text = "Schema parameter `#{parameter.wire_name}` (`#{parameter.graphql_type}`)."
|
|
155
|
+
if parameter.escaped?
|
|
156
|
+
text += " Exposed as `#{parameter.name}` because `#{parameter.wire_name}` is " \
|
|
157
|
+
'already taken; the wire name is unchanged.'
|
|
158
|
+
end
|
|
159
|
+
[comment(text)]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
sig { params(text: String).returns(RBI::Comment) }
|
|
163
|
+
def comment(text)
|
|
164
|
+
RBI::Comment.new(text)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module Dsl
|
|
6
|
+
module Helpers
|
|
7
|
+
# Translates a GraphQL type expression, as written in a document, into a
|
|
8
|
+
# Sorbet type string.
|
|
9
|
+
#
|
|
10
|
+
# Nothing here knows about Fragment: the scalar table is supplied by the
|
|
11
|
+
# caller, and anything absent from it becomes `T.untyped`. Kept separate so
|
|
12
|
+
# it can move to a gem of its own if a second consumer appears -- no gem
|
|
13
|
+
# does this today, and Tapioca ships no GraphQL compiler.
|
|
14
|
+
#
|
|
15
|
+
# translate('[SafeString!]!', scalars: { 'SafeString' => '::String' })
|
|
16
|
+
# #=> "T::Array[::String]"
|
|
17
|
+
# translate('[String]', scalars: { 'String' => '::String' })
|
|
18
|
+
# #=> "T::Array[T.nilable(::String)]"
|
|
19
|
+
#
|
|
20
|
+
# Top-level nullability is the caller's to apply, since only the caller knows
|
|
21
|
+
# whether an optional field is nilable, defaulted, or something else.
|
|
22
|
+
# Nullability inside a list is applied here, having nowhere else to go.
|
|
23
|
+
module GraphqlSorbetTypes
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
UNTYPED = 'T.untyped'
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
extend T::Sig
|
|
30
|
+
|
|
31
|
+
sig { params(graphql_type: String, scalars: T::Hash[String, String]).returns(String) }
|
|
32
|
+
def translate(graphql_type, scalars:)
|
|
33
|
+
type = graphql_type.delete_suffix('!')
|
|
34
|
+
return scalars.fetch(type, UNTYPED) unless type.start_with?('[')
|
|
35
|
+
|
|
36
|
+
element = type.delete_prefix('[').delete_suffix(']')
|
|
37
|
+
"T::Array[#{list_element(element, scalars: scalars)}]"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
sig { params(element: String, scalars: T::Hash[String, String]).returns(String) }
|
|
43
|
+
def list_element(element, scalars:)
|
|
44
|
+
type = translate(element, scalars: scalars)
|
|
45
|
+
return type if element.end_with?('!') || type == UNTYPED
|
|
46
|
+
|
|
47
|
+
"T.nilable(#{type})"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fragment-dev
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fragment
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: graphql
|
|
@@ -67,8 +67,15 @@ files:
|
|
|
67
67
|
- lib/fragment.schema.json
|
|
68
68
|
- lib/fragment_client.rb
|
|
69
69
|
- lib/fragment_client.rbi
|
|
70
|
+
- lib/fragment_client/graphql_ast.rb
|
|
71
|
+
- lib/fragment_client/typed_entries.rb
|
|
72
|
+
- lib/fragment_client/typed_ledger_entry.rb
|
|
70
73
|
- lib/fragment_client/version.rb
|
|
71
74
|
- lib/queries.graphql
|
|
75
|
+
- lib/tapioca/dsl/compilers/fragment_query_methods.rb
|
|
76
|
+
- lib/tapioca/dsl/compilers/fragment_response_types.rb
|
|
77
|
+
- lib/tapioca/dsl/compilers/fragment_typed_entries.rb
|
|
78
|
+
- lib/tapioca/dsl/helpers/graphql_sorbet_types.rb
|
|
72
79
|
homepage: https://fragment.dev
|
|
73
80
|
licenses:
|
|
74
81
|
- Apache-2.0
|