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.
@@ -0,0 +1,281 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ class FragmentClient
7
+ # Base class for a typed `addLedgerEntries` payload. Abstract: one subclass per
8
+ # `(entry type, typeVersion)` is built by {FragmentClient::TypedEntries.load}.
9
+ #
10
+ # A subclass declares one keyword argument and one reader per Schema parameter,
11
+ # alongside the common `LedgerEntryInput` fields, and {#to_entry_input} reshapes
12
+ # them into the nested `AddLedgerEntryInput` the API takes.
13
+ #
14
+ # entry = FragmentClient::Entries::AuthCaptureV1.new(
15
+ # ik: 'ik-1', ledger_ik: 'prod', capture_amount: '100'
16
+ # )
17
+ # client.add_ledger_entries(entries: [entry])
18
+ #
19
+ # Section references are to `typed-batch-entries.md`; see
20
+ # `docs/spec-conformance.md`.
21
+ class TypedLedgerEntry
22
+ extend T::Sig
23
+ extend T::Helpers
24
+ abstract!
25
+
26
+ # The `LedgerEntryInput` fields every payload carries, whatever its entry type
27
+ # (spec 2.3a). Fixed by `LedgerEntryInput`, not derived from the operation,
28
+ # which binds only what its CLI version chose to expose.
29
+ #
30
+ # `lines` is absent deliberately: it cannot be combined with an entry that has
31
+ # a `type`. `type`, `typeVersion` and `parameters` are derived, never supplied.
32
+ COMMON_FIELDS = T.let(
33
+ %i[ik ledger_ik posted description tags groups conditions].freeze,
34
+ T::Array[Symbol]
35
+ )
36
+
37
+ class << self
38
+ extend T::Sig
39
+
40
+ # Build a payload class for one derived spec.
41
+ sig do
42
+ params(spec: FragmentClient::TypedEntries::EntrySpec, origin: T.nilable(String))
43
+ .returns(T.class_of(TypedLedgerEntry))
44
+ end
45
+ def build(spec, origin: nil)
46
+ klass = Class.new(self)
47
+ klass.instance_variable_set(:@spec, spec)
48
+ klass.instance_variable_set(:@source_path, origin)
49
+
50
+ spec.parameters.each do |parameter|
51
+ name = parameter.name
52
+ klass.send(:define_method, name) do
53
+ T.bind(self, TypedLedgerEntry)
54
+ parameter_value(name)
55
+ end
56
+ end
57
+
58
+ klass
59
+ end
60
+
61
+ # What this payload was derived from.
62
+ sig { returns(FragmentClient::TypedEntries::EntrySpec) }
63
+ def spec
64
+ @spec = T.let(@spec, T.nilable(FragmentClient::TypedEntries::EntrySpec))
65
+ @spec || raise(
66
+ NotImplementedError,
67
+ "#{self} has no derived spec. Typed payload classes are built by " \
68
+ 'FragmentClient::TypedEntries.load, not subclassed by hand.'
69
+ )
70
+ end
71
+
72
+ sig { returns(String) }
73
+ def entry_type
74
+ spec.entry_type
75
+ end
76
+
77
+ sig { returns(Integer) }
78
+ def type_version
79
+ spec.type_version
80
+ end
81
+
82
+ # The Schema parameters, in source order (spec 2.4).
83
+ sig { returns(T::Array[FragmentClient::TypedEntries::Parameter]) }
84
+ def parameters
85
+ spec.parameters
86
+ end
87
+
88
+ # The `.graphql` file this came from, if it came from one.
89
+ sig { returns(T.nilable(String)) }
90
+ def source_path
91
+ @source_path = T.let(@source_path, T.nilable(String))
92
+ end
93
+ end
94
+
95
+ sig { returns(String) }
96
+ attr_reader :ik
97
+
98
+ sig { returns(String) }
99
+ attr_reader :ledger_ik
100
+
101
+ # The optional common fields: `nil` when unset, so `&.` and truthiness behave
102
+ # as usual. {#set?} is what separates "not set" from "set to nil".
103
+ sig { returns(T.nilable(String)) }
104
+ attr_reader :posted
105
+
106
+ sig { returns(T.nilable(String)) }
107
+ attr_reader :description
108
+
109
+ sig { returns(T.nilable(T::Array[T.untyped])) }
110
+ attr_reader :tags
111
+
112
+ sig { returns(T.nilable(T::Array[T.untyped])) }
113
+ attr_reader :groups
114
+
115
+ sig { returns(T.nilable(T::Array[T.untyped])) }
116
+ attr_reader :conditions
117
+
118
+ # Optional fields default to the {FragmentClient::TypedEntries::UNSET} sentinel
119
+ # rather than `nil`, so an omitted keyword can be told from an explicit `nil`
120
+ # and left out of the payload (spec 3.2). It is converted to `nil` here and
121
+ # never returned.
122
+ #
123
+ # Schema parameters arrive through `**parameters` under the names this class
124
+ # declares. Presence and unknown names are checked here; their types come from
125
+ # the RBI `tapioca dsl` generates.
126
+ sig do
127
+ params(
128
+ ik: String,
129
+ ledger_ik: String,
130
+ posted: T.any(String, NilClass, FragmentClient::TypedEntries::Unset),
131
+ description: T.any(String, NilClass, FragmentClient::TypedEntries::Unset),
132
+ tags: T.any(T::Array[T.untyped], NilClass, FragmentClient::TypedEntries::Unset),
133
+ groups: T.any(T::Array[T.untyped], NilClass, FragmentClient::TypedEntries::Unset),
134
+ conditions: T.any(T::Array[T.untyped], NilClass, FragmentClient::TypedEntries::Unset),
135
+ parameters: T.untyped
136
+ ).void
137
+ end
138
+ # rubocop:disable Metrics/AbcSize -- one assignment per common field; `typed:
139
+ # strict` rules out setting them in a loop.
140
+ def initialize(ik:, ledger_ik:,
141
+ posted: FragmentClient::TypedEntries::UNSET,
142
+ description: FragmentClient::TypedEntries::UNSET,
143
+ tags: FragmentClient::TypedEntries::UNSET,
144
+ groups: FragmentClient::TypedEntries::UNSET,
145
+ conditions: FragmentClient::TypedEntries::UNSET,
146
+ **parameters)
147
+ @ik = ik
148
+ @ledger_ik = ledger_ik
149
+ @parameters = T.let(validate(parameters), T::Hash[Symbol, T.untyped])
150
+ @provided = T.let(Set.new(@parameters.keys + %i[ik ledger_ik]), T::Set[Symbol])
151
+ @posted = T.let(record(:posted, posted), T.nilable(String))
152
+ @description = T.let(record(:description, description), T.nilable(String))
153
+ @tags = T.let(record(:tags, tags), T.nilable(T::Array[T.untyped]))
154
+ @groups = T.let(record(:groups, groups), T.nilable(T::Array[T.untyped]))
155
+ @conditions = T.let(record(:conditions, conditions), T.nilable(T::Array[T.untyped]))
156
+ end
157
+ # rubocop:enable Metrics/AbcSize
158
+
159
+ # Whether the caller set `name` -- a common field or a parameter. The readers
160
+ # report an omitted field and an explicit `nil` alike, so this is the only way
161
+ # to tell them apart.
162
+ sig { params(name: Symbol).returns(T::Boolean) }
163
+ def set?(name)
164
+ @provided.include?(name)
165
+ end
166
+
167
+ # Same class, same wire payload -- which includes agreeing on what is set.
168
+ sig { params(other: T.untyped).returns(T::Boolean) }
169
+ def ==(other)
170
+ other.instance_of?(self.class) && other.to_entry_input == to_entry_input
171
+ end
172
+
173
+ alias eql? ==
174
+
175
+ sig { returns(Integer) }
176
+ def hash
177
+ [self.class, to_entry_input].hash
178
+ end
179
+
180
+ # The `AddLedgerEntryInput` this payload posts (spec 3.1). Keys lexicographic
181
+ # at every level except `parameters`, which keeps source order (spec 3.4).
182
+ sig { returns(T::Hash[String, T.untyped]) }
183
+ def to_entry_input
184
+ { 'entry' => entry_input, 'ik' => @ik }
185
+ end
186
+
187
+ alias to_h to_entry_input
188
+
189
+ # The `parameters` payload, keyed by verbatim Schema parameter name -- so an
190
+ # escaped parameter still travels under its own key (spec 2.5, 3.3).
191
+ sig { returns(T::Hash[String, T.untyped]) }
192
+ def entry_parameters
193
+ self.class.parameters.each_with_object({}) do |parameter, out|
194
+ out[parameter.wire_name] = @parameters[parameter.name] if @parameters.key?(parameter.name)
195
+ end
196
+ end
197
+
198
+ sig { returns(String) }
199
+ def inspect
200
+ shown = COMMON_FIELDS.select { |name| set?(name) }
201
+ .map { |name| "#{name}: #{public_send(name).inspect}" }
202
+ shown.concat(@parameters.map { |name, value| "#{name}: #{value.inspect}" })
203
+ "#<#{self.class} #{shown.join(', ')}>"
204
+ end
205
+
206
+ private
207
+
208
+ # Records that `name` was supplied, and unwraps the sentinel to `nil`.
209
+ sig { params(name: Symbol, value: T.untyped).returns(T.untyped) }
210
+ def record(name, value)
211
+ return nil if value.is_a?(FragmentClient::TypedEntries::Unset)
212
+
213
+ @provided << name
214
+ value
215
+ end
216
+
217
+ sig { params(name: Symbol).returns(T.untyped) }
218
+ def parameter_value(name)
219
+ @parameters[name]
220
+ end
221
+
222
+ sig { returns(T::Hash[String, T.untyped]) }
223
+ def entry_input
224
+ input = T.let({}, T::Hash[String, T.untyped])
225
+ input['conditions'] = @conditions if set?(:conditions)
226
+ input['description'] = @description if set?(:description)
227
+ input['groups'] = @groups if set?(:groups)
228
+ input['ledger'] = { 'ik' => @ledger_ik }
229
+ # Always present: derived rather than supplied, so it has no unset state.
230
+ input['parameters'] = entry_parameters
231
+ input['posted'] = @posted if set?(:posted)
232
+ input['tags'] = @tags if set?(:tags)
233
+ input['type'] = self.class.entry_type
234
+ input['typeVersion'] = self.class.type_version
235
+ input
236
+ end
237
+
238
+ sig { params(supplied: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
239
+ def validate(supplied)
240
+ reject_unknown(supplied)
241
+ require_declared(supplied)
242
+ supplied
243
+ end
244
+
245
+ sig { params(supplied: T::Hash[Symbol, T.untyped]).void }
246
+ def reject_unknown(supplied)
247
+ known = self.class.parameters.map(&:name)
248
+ unknown = supplied.keys - known
249
+ return if unknown.empty?
250
+
251
+ raise ArgumentError,
252
+ "unknown keyword#{plural(unknown)}: #{list(unknown)} for #{describe}. " \
253
+ "Declared parameters: #{known.empty? ? '(none)' : list(known)}."
254
+ end
255
+
256
+ sig { params(supplied: T::Hash[Symbol, T.untyped]).void }
257
+ def require_declared(supplied)
258
+ missing = self.class.parameters
259
+ .select { |parameter| parameter.required && !supplied.key?(parameter.name) }
260
+ .map(&:name)
261
+ return if missing.empty?
262
+
263
+ raise ArgumentError, "missing keyword#{plural(missing)}: #{list(missing)} for #{describe}."
264
+ end
265
+
266
+ sig { returns(String) }
267
+ def describe
268
+ "#{self.class.entry_type.inspect} v#{self.class.type_version}"
269
+ end
270
+
271
+ sig { params(names: T::Array[Symbol]).returns(String) }
272
+ def plural(names)
273
+ names.length == 1 ? '' : 's'
274
+ end
275
+
276
+ sig { params(names: T::Array[Symbol]).returns(String) }
277
+ def list(names)
278
+ names.map(&:inspect).join(', ')
279
+ end
280
+ end
281
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FragmentSDK
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
@@ -1,15 +1,19 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'json'
5
5
  require 'graphql/client'
6
6
  require 'graphql/client/http'
7
+ require 'logger'
7
8
  require 'sorbet-runtime'
8
9
  require 'uri'
9
10
  require 'net/http'
10
11
  require 'fragment_client/version'
12
+ require 'fragment_client/typed_entries'
11
13
  module GraphQL
12
14
  module StaticValidation
15
+ # Accepts an inline object literal for the JSON-shaped scalars, which
16
+ # graphql-ruby otherwise rejects -- and which every `parameters: {...}` is.
13
17
  class LiteralValidator
14
18
  alias recursive_validate_old recursively_validate
15
19
  def recursively_validate(ast_value, type)
@@ -48,6 +52,7 @@ module FragmentGraphQl
48
52
 
49
53
  # Create a custom client class for Fragment-specific behavior
50
54
  class CustomClient < GraphQL::Client
55
+ # Keeps a memory address out of the operation name sent to the API.
51
56
  class Definition < GraphQL::Client::Definition
52
57
  def definition_name
53
58
  super.gsub(/#<Module.*>/, 'FragmentGraphQl__Dynamic')
@@ -69,6 +74,71 @@ module FragmentGraphQl
69
74
  end
70
75
 
71
76
  FragmentQueries = T.let(parse_queries("#{__dir__}/queries.graphql"), T.untyped)
77
+
78
+ # Look up one parsed operation by name.
79
+ #
80
+ # `FragmentQueries::AddLedgerEntries` reads better but does not resolve: the
81
+ # constants are created by `GraphQL::Client.parse` at runtime.
82
+ sig { params(name: Symbol).returns(T.untyped) }
83
+ def self.operation(name)
84
+ FragmentQueries.const_get(name)
85
+ end
86
+
87
+ # The instance method `FragmentClient` defines for an operation:
88
+ # `ListLedgerAccounts` -> `list_ledger_accounts`.
89
+ sig { params(operation_name: String).returns(String) }
90
+ def self.method_name_for(operation_name)
91
+ operation_name.gsub(/[a-z]([A-Z])/) do |m|
92
+ format('%<lower>s_%<upper>s', lower: m[0], upper: T.must(m[1]).downcase)
93
+ end.gsub(/^[A-Z]/, &:downcase)
94
+ end
95
+
96
+ # Method names for every operation parsed so far, in the order first seen.
97
+ #
98
+ # Read by the Tapioca compiler, which cannot otherwise see methods that
99
+ # `define_method_from_queries` creates per instance.
100
+ sig { returns(T::Array[String]) }
101
+ def self.operation_method_names
102
+ @operation_method_names ||= T.let([], T.nilable(T::Array[String]))
103
+ end
104
+
105
+ sig { params(queries: T.untyped).void }
106
+ def self.record_operations(queries)
107
+ queries.constants.each do |constant|
108
+ name = method_name_for(constant.to_s)
109
+ operation_method_names << name unless operation_method_names.include?(name)
110
+ end
111
+ end
112
+
113
+ # Operation ASTs by operation name, for the compilers that need a selection set
114
+ # rather than just a method name.
115
+ sig { returns(T::Hash[String, GraphQL::Language::Nodes::OperationDefinition]) }
116
+ def self.operations
117
+ @operations ||= T.let({}, T.nilable(T::Hash[String,
118
+ GraphQL::Language::Nodes::OperationDefinition]))
119
+ end
120
+
121
+ sig { params(source: String).void }
122
+ def self.record_document(source)
123
+ GraphQL.parse(source).definitions.each do |definition|
124
+ next unless definition.is_a?(GraphQL::Language::Nodes::OperationDefinition)
125
+
126
+ name = definition.name
127
+ operations[name] ||= definition if name
128
+ end
129
+ end
130
+
131
+ # Forget operations recorded from anything but `queries.graphql`. For tests.
132
+ sig { void }
133
+ def self.reset_operations!
134
+ operation_method_names.clear
135
+ operations.clear
136
+ record_operations(FragmentQueries)
137
+ record_document(File.read("#{__dir__}/queries.graphql"))
138
+ end
139
+
140
+ record_operations(FragmentQueries)
141
+ record_document(File.read("#{__dir__}/queries.graphql"))
72
142
  end
73
143
 
74
144
  # A client for Fragment
@@ -81,15 +151,19 @@ class FragmentClient
81
151
 
82
152
  extend T::Sig
83
153
 
154
+ DEFAULT_OAUTH_URL = 'https://auth.fragment.dev/oauth2/token'
155
+ DEFAULT_OAUTH_SCOPE = 'https://api.fragment.dev/*'
156
+
84
157
  sig do
85
158
  params(client_id: String, client_secret: String, extra_queries_filenames: T.nilable(T::Array[String]),
86
159
  api_url: T.nilable(String), oauth_url: T.nilable(String), oauth_scope: T.nilable(String)).void
87
160
  end
88
161
 
89
162
  def initialize(client_id, client_secret, extra_queries_filenames: nil, api_url: nil,
90
- oauth_url: 'https://auth.fragment.dev/oauth2/token', oauth_scope: 'https://api.fragment.dev/*')
91
- @oauth_scope = T.let(oauth_scope, String)
92
- @oauth_url = T.let(URI.parse(oauth_url), URI)
163
+ oauth_url: DEFAULT_OAUTH_URL, oauth_scope: DEFAULT_OAUTH_SCOPE)
164
+ # Both are nilable, so nil means the default rather than an assertion failure.
165
+ @oauth_scope = T.let(oauth_scope || DEFAULT_OAUTH_SCOPE, String)
166
+ @oauth_url = T.let(parse_oauth_url(oauth_url || DEFAULT_OAUTH_URL), URI::HTTP)
93
167
  @client_id = T.let(client_id, String)
94
168
  @client_secret = T.let(client_secret, String)
95
169
 
@@ -108,10 +182,63 @@ class FragmentClient
108
182
  define_method_from_queries(FragmentGraphQl::FragmentQueries)
109
183
  return if extra_queries_filenames.nil?
110
184
 
111
- extra_queries_filenames.each do |filename|
112
- queries = T.let(FragmentGraphQl.parse_queries(filename), T.untyped)
113
- define_method_from_queries(queries)
185
+ extra_queries_filenames.each { |filename| define_method_from_queries(self.class.load_queries(filename)) }
186
+ end
187
+
188
+ # Parse a `.graphql` document and register what can be derived from it: the
189
+ # operation names {FragmentClient} will answer to, and a typed payload class per
190
+ # Ledger Entry type it declares.
191
+ #
192
+ # Needs neither credentials nor network, so `bundle exec tapioca dsl` can see
193
+ # both if this runs in an initializer. {#initialize} calls it for every
194
+ # `extra_queries_filenames` entry, so passing the files is usually enough.
195
+ sig { params(paths: String).returns(T.untyped) }
196
+ def self.load_queries(*paths)
197
+ queries = paths.map do |path|
198
+ source = File.read(path)
199
+ parsed = T.let(FragmentGraphQl.parse_queries(path), T.untyped)
200
+ FragmentGraphQl.record_operations(parsed)
201
+ FragmentGraphQl.record_document(source)
202
+ TypedEntries.load(path)
203
+ parsed
114
204
  end
205
+ queries.length == 1 ? queries.first : queries
206
+ end
207
+
208
+ # Operations with a hand-written wrapper below, which the dynamic definer must
209
+ # not shadow -- a singleton method beats an instance method. Listed explicitly
210
+ # rather than tested with `method_defined?`, which would also match `clone`,
211
+ # `freeze` and everything else on Object.
212
+ WRAPPED_OPERATIONS = T.let(%w[add_ledger_entries].freeze, T::Array[String])
213
+
214
+ # Commit a batch of Ledger Entries atomically: every entry commits or none do,
215
+ # so there is no partial-batch state to reconcile.
216
+ #
217
+ #
218
+ # `entries` may mix typed payloads from {FragmentClient::TypedEntries} with raw
219
+ # `AddLedgerEntryInput` hashes, in any order; the API returns results in the
220
+ # order sent (spec 3.5). Idempotency keys are per entry, so `isIkReplay` is
221
+ # reported per result.
222
+ #
223
+ # The response is a union. Narrow on `__typename` before reading `results`, and
224
+ # read `errors` on `AddLedgerEntriesError` for the per-entry failures, each
225
+ # carrying the `ik` of the entry that failed (spec 4).
226
+ # Takes the operation's variables, like every other operation method, so
227
+ # `add_ledger_entries(entries: [...])` and `add_ledger_entries({ entries: [...] })`
228
+ # both work. Typed payloads in `entries` are converted; anything else passes
229
+ # through.
230
+ #
231
+ # The return type stays untyped here, unlike the generated methods: a signature in
232
+ # shipped source cannot name `FragmentClient::Responses::AddLedgerEntries`, which
233
+ # does not exist until a consumer runs `tapioca dsl`.
234
+ sig { params(variables: T::Hash[T.untyped, T.untyped]).returns(T.untyped) }
235
+ def add_ledger_entries(variables)
236
+ query(
237
+ FragmentGraphQl.operation(:AddLedgerEntries),
238
+ variables.to_h do |name, value|
239
+ [name, name.to_s == 'entries' ? TypedEntries.to_entry_inputs(value) : value]
240
+ end
241
+ )
115
242
  end
116
243
 
117
244
  # Move these error class definitions up, before the query method
@@ -130,9 +257,10 @@ class FragmentClient
130
257
 
131
258
  def define_method_from_queries(queries)
132
259
  queries.constants.each do |qry|
133
- name = qry.to_s.gsub(/[a-z]([A-Z])/) do |m|
134
- format('%<lower>s_%<upper>s', lower: m[0], upper: m[1].downcase)
135
- end.gsub(/^[A-Z]/, &:downcase)
260
+ name = FragmentGraphQl.method_name_for(qry.to_s)
261
+
262
+ # Leave the hand-written wrapper in place; see WRAPPED_OPERATIONS.
263
+ next if WRAPPED_OPERATIONS.include?(name)
136
264
 
137
265
  # Get the original query
138
266
  original_query = queries.const_get(qry)
@@ -149,6 +277,7 @@ class FragmentClient
149
277
 
150
278
  # Define the new method that uses the stored original method
151
279
  definition_node.singleton_class.send(:define_method, :name) do
280
+ T.bind(self, T.untyped)
152
281
  original_name.gsub(/#<Module.*?>/, 'FragmentGraphQl__Dynamic__Custom')
153
282
  end
154
283
  end
@@ -161,17 +290,40 @@ class FragmentClient
161
290
  end
162
291
  end
163
292
 
164
- sig { returns(Token) }
165
- def create_token
166
- uri = URI.parse(@oauth_url.to_s)
167
- post = Net::HTTP::Post.new(uri.request_uri)
293
+ # Reject an unusable `oauth_url` here rather than in {create_token}, which fails
294
+ # on `request_uri` for a non-HTTP URL and says nothing about the argument.
295
+ sig { params(oauth_url: String).returns(URI::HTTP) }
296
+ def parse_oauth_url(oauth_url)
297
+ parsed = begin
298
+ URI.parse(oauth_url)
299
+ rescue URI::InvalidURIError => e
300
+ raise ArgumentError, "oauth_url is not a valid URL (#{e.message}), got #{oauth_url.inspect}"
301
+ end
302
+ # URI::HTTPS subclasses URI::HTTP, so this accepts both.
303
+ return parsed if parsed.is_a?(URI::HTTP)
304
+
305
+ raise ArgumentError, "oauth_url must be an http or https URL, got #{oauth_url.inspect}"
306
+ end
307
+
308
+ # RFC 6749 §4.4.2 and §2.3.1: client_credentials over
309
+ # application/x-www-form-urlencoded, with the client id and secret as HTTP Basic.
310
+ sig { returns(Net::HTTP::Post) }
311
+ def token_request
312
+ post = Net::HTTP::Post.new(@oauth_url.request_uri)
168
313
  post.basic_auth(@client_id, @client_secret)
169
- post.content_type = "application/x-www-form-urlencoded"
314
+ post.content_type = 'application/x-www-form-urlencoded'
170
315
  post.body = URI.encode_www_form(
171
316
  grant_type: 'client_credentials',
172
317
  scope: @oauth_scope,
173
318
  client_id: @client_id
174
319
  )
320
+ post
321
+ end
322
+
323
+ sig { returns(Token) }
324
+ def create_token
325
+ uri = @oauth_url
326
+ post = token_request
175
327
 
176
328
  begin
177
329
  http = Net::HTTP.new(uri.host, uri.port)
@@ -197,6 +349,7 @@ class FragmentClient
197
349
  end
198
350
  end
199
351
 
352
+ # Process-wide settings, set with {FragmentClient.configure}.
200
353
  class Configuration
201
354
  extend T::Sig
202
355
 
@@ -223,7 +376,7 @@ class FragmentClient
223
376
 
224
377
  sig { params(blk: T.proc.params(config: Configuration).void).void }
225
378
  def configure(&blk)
226
- yield(configuration)
379
+ blk.call(configuration)
227
380
  end
228
381
  end
229
382
 
data/lib/queries.graphql CHANGED
@@ -94,6 +94,52 @@ mutation DeleteLedger($ledger: LedgerMatchInput!) {
94
94
  }
95
95
  }
96
96
 
97
+ mutation AddLedgerEntries($entries: [AddLedgerEntryInput!]!) {
98
+ addLedgerEntries(entries: $entries) {
99
+ __typename
100
+ ... on AddLedgerEntriesResult {
101
+ results {
102
+ isIkReplay
103
+ entry {
104
+ type
105
+ id
106
+ ik
107
+ posted
108
+ created
109
+ }
110
+ lines {
111
+ id
112
+ amount
113
+ account {
114
+ path
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ... on AddLedgerEntriesError {
120
+ code
121
+ message
122
+ retryable
123
+ errors {
124
+ ik
125
+ code
126
+ message
127
+ retryable
128
+ }
129
+ }
130
+ ... on BadRequestError {
131
+ code
132
+ message
133
+ retryable
134
+ }
135
+ ... on InternalError {
136
+ code
137
+ message
138
+ retryable
139
+ }
140
+ }
141
+ }
142
+
97
143
  mutation AddLedgerEntry(
98
144
  $ik: SafeString!
99
145
  $ledgerIk: SafeString!
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Tapioca::Dsl::Compilers)
5
+
6
+ require 'fragment_client'
7
+
8
+ module Tapioca
9
+ module Dsl
10
+ module Compilers
11
+ # Declares the query and mutation methods {FragmentClient} answers to.
12
+ #
13
+ # `define_method_from_queries` creates one singleton method per operation on
14
+ # each client instance, so without this Sorbet reports `Method
15
+ # 'create_ledger' does not exist` for every one of them and a consumer has to
16
+ # write `T.unsafe(client)`.
17
+ #
18
+ # Covers the operations shipped in `queries.graphql`, plus any document
19
+ # passed to `FragmentClient.load_queries`. Operations in
20
+ # {FragmentClient::WRAPPED_OPERATIONS} are skipped: those have hand-written
21
+ # methods with real signatures already.
22
+ #
23
+ # `variables` is the operation's variables hash and stays untyped. Its keys
24
+ # are the GraphQL variable names verbatim, as the rest of this SDK passes
25
+ # them. The return type comes from `FragmentResponseTypes`.
26
+ class FragmentQueryMethods < Compiler
27
+ extend T::Sig
28
+
29
+ ConstantType = type_member { { fixed: T.class_of(FragmentClient) } }
30
+
31
+ class << self
32
+ extend T::Sig
33
+
34
+ sig { override.returns(T::Enumerable[Module]) }
35
+ def gather_constants
36
+ [FragmentClient]
37
+ end
38
+ end
39
+
40
+ sig { override.void }
41
+ def decorate
42
+ methods = FragmentGraphQl.operations.keys.to_h do |operation|
43
+ [FragmentGraphQl.method_name_for(operation), operation]
44
+ end
45
+ methods.reject! { |name, _| FragmentClient::WRAPPED_OPERATIONS.include?(name) }
46
+ return if methods.empty?
47
+
48
+ root.create_path(constant) do |klass|
49
+ methods.keys.sort.each do |name|
50
+ klass.create_method(
51
+ name,
52
+ parameters: [create_param('variables', type: 'T::Hash[Symbol, T.untyped]')],
53
+ return_type: "::FragmentClient::Responses::#{methods.fetch(name)}"
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end