workato-connector-sdk 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 053e48199f2b1406ef47a84fb43d7b7d0cc74d74ce0f1f1b650c01fc422bc683
4
- data.tar.gz: e2155ca61c45230234b105ff0f4ab033205c86b49b35838d88e8a8e473a957b3
3
+ metadata.gz: b49034b5762b2b44d0987d72eb74327e379284347b793714149d85d354a5bb57
4
+ data.tar.gz: 1be17d19381fe42689ef4a157a56eca0fb8c61a9c0e90e981e5a373305ed771f
5
5
  SHA512:
6
- metadata.gz: 4d34393bda1fc5eef5e138c7e742a2a05a7c1ced8fb5a4a8969a1770473c4534cf015c7096cf17a18eb69c669301ca8761ff51a88b3ee8a04ad4550467a23588
7
- data.tar.gz: b293f65e21f45ef2bab573d687cd12cd77981a7375bec619ce45561ead9e96f895f633e65910dad7226213daacb38b95453c890b96e7b7a957b5085e5055ff94
6
+ metadata.gz: 5bcc365d25ec713df12b75d9d6d5fb8a6a73266b94cf012829e4a2c98af955f1fcfa7b0d0f7e69861a4ee6cb54af24ed8952bbb590553e93a37fcf2933c69459
7
+ data.tar.gz: 1da6193b9a1b04e3d3e2c6fb34fcd0eeec7666908c61df5c7ec386381ba8f33207b50b8ffc1e9d581fecb599fc423f84f9f79867120322356aa4d645b9b7da5e
data/README.md CHANGED
@@ -16,6 +16,7 @@ This guide below showcases how you can do the following things:
16
16
  2. Choose between Ruby versions `2.4.10` `2.5.X` or `2.7.X`. Our preferred version is `2.7.X`.
17
17
  3. Verify you're running a valid ruby version. Do this by running either `ruby -v` or the commands within your version manager. i.e., `rvm current` if you have installed RVM.
18
18
  4. For Windows you need tzinfo-data gem installed as well. `gem install tzinfo-data`
19
+ 5. SDK depends on `charlock_holmes` gem. Check [gem's documentation](https://github.com/brianmario/charlock_holmes#installing) if you have troubles when install this dependency. Additional [details for Windows](https://github.com/brianmario/charlock_holmes/issues/84#issuecomment-652877605)
19
20
 
20
21
  ```bash
21
22
  ruby -v
@@ -2,11 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'thor'
5
+ require_relative './multi_auth_selected_fallback'
5
6
 
6
7
  module Workato
7
8
  module CLI
8
9
  class ExecCommand
9
10
  include Thor::Shell
11
+ include MultiAuthSelectedFallback
10
12
 
11
13
  DebugExceptionError = Class.new(StandardError)
12
14
 
@@ -63,7 +65,7 @@ module Workato
63
65
  oauth2_code: options[:oauth2_code],
64
66
  redirect_url: options[:redirect_url],
65
67
  refresh_token: options[:refresh_token],
66
- recipe_id: SecureRandom.uuid
68
+ recipe_id: Workato::Connector::Sdk::Operation.recipe_id!
67
69
  }
68
70
  end
69
71
 
@@ -84,16 +86,21 @@ module Workato
84
86
  )
85
87
  @settings = settings_store.read
86
88
 
87
- Workato::Connector::Sdk::Connection.on_settings_update = lambda do |message, &refresher|
88
- begin
89
- $stdout.pause if verbose?
90
- say('')
91
- say(message)
89
+ Workato::Connector::Sdk::Connection.multi_auth_selected_fallback = lambda do |options|
90
+ next @selected_auth_type if @selected_auth_type
91
+
92
+ with_user_interaction do
93
+ @selected_auth_type = multi_auth_selected_fallback(options)
94
+ end
95
+ end
92
96
 
93
- new_settings = refresher.call
94
- break unless new_settings
97
+ Workato::Connector::Sdk::Connection.on_settings_update = lambda do |message, &refresher|
98
+ new_settings = refresher.call
99
+ break unless new_settings
95
100
 
101
+ with_user_interaction do
96
102
  loop do
103
+ say(message)
97
104
  answer = ask('Updated settings file with new connection attributes? (Yes or No)').to_s.downcase
98
105
  break new_settings if %w[n no].include?(answer)
99
106
  next unless %w[y yes].include?(answer)
@@ -101,8 +108,6 @@ module Workato
101
108
  settings_store.update(new_settings)
102
109
  break new_settings
103
110
  end
104
- ensure
105
- $stdout.resume if verbose?
106
111
  end
107
112
  end
108
113
 
@@ -131,6 +136,11 @@ module Workato
131
136
 
132
137
  def execute_path
133
138
  connector.invoke(path, params)
139
+ rescue Workato::Connector::Sdk::InvalidMultiAuthDefinition => e
140
+ raise "#{e.message}. Please ensure:\n"\
141
+ "- 'selected' block is defined and returns value from 'options' list\n" \
142
+ "- settings file contains value expected by 'selected' block\n\n"\
143
+ 'See more: https://docs.workato.com/developing-connectors/sdk/guides/authentication/multi_auth.html'
134
144
  rescue Exception => e # rubocop:disable Lint/RescueException
135
145
  raise DebugExceptionError, e if options[:debug]
136
146
 
@@ -181,6 +191,16 @@ module Workato
181
191
  output
182
192
  end
183
193
 
194
+ def with_user_interaction
195
+ $stdout.pause if verbose?
196
+ say('')
197
+
198
+ yield
199
+ ensure
200
+ say('')
201
+ $stdout.resume if verbose?
202
+ end
203
+
184
204
  class ProgressLogger < SimpleDelegator
185
205
  def initialize(progress)
186
206
  super($stdout)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module CLI
5
+ module MultiAuthSelectedFallback
6
+ private
7
+
8
+ def multi_auth_selected_fallback(options)
9
+ say('Please select current auth type for multi-auth connector:')
10
+ options = options.keys
11
+ options.each_with_index do |option, idx|
12
+ say "[#{idx + 1}] #{option}"
13
+ end
14
+ say '[q] <exit>'
15
+ say('')
16
+
17
+ multi_auth_selected_fallback = loop do
18
+ answer = ask('Your choice:').to_s.downcase
19
+ break if answer == 'q'
20
+ next unless /\d+/ =~ answer && options[answer.to_i - 1]
21
+
22
+ break options[answer.to_i - 1]
23
+ end
24
+ return unless multi_auth_selected_fallback
25
+
26
+ say('')
27
+ say('Put selected auth type in your settings file to avoid this message in future')
28
+
29
+ multi_auth_selected_fallback
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,11 +3,13 @@
3
3
 
4
4
  require 'securerandom'
5
5
  require 'workato/web/app'
6
+ require_relative './multi_auth_selected_fallback'
6
7
 
7
8
  module Workato
8
9
  module CLI
9
10
  class OAuth2Command
10
11
  include Thor::Shell
12
+ include MultiAuthSelectedFallback
11
13
 
12
14
  AWAIT_CODE_TIMEOUT_INTERVAL = 180 # seconds
13
15
  AWAIT_CODE_SLEEP_INTERVAL = 5 # seconds
@@ -24,6 +26,7 @@ module Workato
24
26
  end
25
27
 
26
28
  def call
29
+ ensure_oauth2_type
27
30
  require_gems
28
31
  start_webrick
29
32
 
@@ -94,16 +97,31 @@ module Workato
94
97
  end
95
98
 
96
99
  def stop_webrick
100
+ return unless @thread
101
+
97
102
  Rack::Handler::WEBrick.shutdown
98
103
  @thread.exit
99
104
  end
100
105
 
106
+ def ensure_oauth2_type
107
+ unless connector.connection.authorization.oauth2?
108
+ raise 'Authorization type is not OAuth2. '\
109
+ 'For multi-auth connector ensure correct auth type was used. '\
110
+ "Expected: 'oauth2', got: '#{connector.connection.authorization.type}''"
111
+ end
112
+ rescue Workato::Connector::Sdk::InvalidMultiAuthDefinition => e
113
+ raise "#{e.message}. Please ensure:\n"\
114
+ "- 'selected' block is defined and returns value from 'options' list\n" \
115
+ "- settings file contains value expected by 'selected' block\n\n"\
116
+ 'See more: https://docs.workato.com/developing-connectors/sdk/guides/authentication/multi_auth.html'
117
+ end
118
+
101
119
  def client
102
120
  @client ||= OAuth2::Client.new(
103
- connector.connection.authorization.client_id(settings),
104
- connector.connection.authorization.client_secret(settings),
105
- site: connector.connection.base_uri(settings),
106
- token_url: connector.connection.authorization.token_url(settings),
121
+ connector.connection.authorization.client_id,
122
+ connector.connection.authorization.client_secret,
123
+ site: connector.connection.base_uri,
124
+ token_url: connector.connection.authorization.token_url,
107
125
  redirect_uri: redirect_url
108
126
  )
109
127
  end
@@ -112,14 +130,14 @@ module Workato
112
130
  return @authorize_url if defined?(@authorize_url)
113
131
 
114
132
  @authorize_url =
115
- if (authorization_url = connector.connection.authorization.authorization_url(settings))
133
+ if (authorization_url = connector.connection.authorization.authorization_url)
116
134
  params = {
117
135
  state: SecureRandom.hex(8),
118
- client_id: connector.connection.authorization.client_id(settings),
136
+ client_id: connector.connection.authorization.client_id,
119
137
  redirect_uri: redirect_url
120
- }
138
+ }.with_indifferent_access
121
139
  uri = URI(authorization_url)
122
- uri.query = params.with_indifferent_access.merge(Rack::Utils.parse_nested_query(uri.query || '')).to_param
140
+ uri.query = params.merge(Rack::Utils.parse_nested_query(uri.query || '')).to_param
123
141
  uri.to_s
124
142
  end
125
143
  end
@@ -133,12 +151,25 @@ module Workato
133
151
  end
134
152
 
135
153
  def settings
136
- @settings ||= settings_store.read
154
+ return @settings if defined?(@settings)
155
+
156
+ @settings = settings_store.read
157
+
158
+ Workato::Connector::Sdk::Connection.multi_auth_selected_fallback = lambda do |options|
159
+ next @selected_auth_type if @selected_auth_type
160
+
161
+ with_user_interaction do
162
+ @selected_auth_type = multi_auth_selected_fallback(options)
163
+ end
164
+ end
165
+
166
+ @settings
137
167
  end
138
168
 
139
169
  def connector
140
170
  @connector ||= Workato::Connector::Sdk::Connector.from_file(
141
- options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
171
+ options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH,
172
+ settings
142
173
  )
143
174
  end
144
175
 
@@ -156,8 +187,8 @@ module Workato
156
187
  end
157
188
 
158
189
  def acquire_token(code)
159
- if connector.source.dig(:connection, :authorization, :acquire)
160
- tokens, _, extra_settings = connector.connection.authorization.acquire(settings, await_code, redirect_url)
190
+ if connector.connection.authorization.source[:acquire]
191
+ tokens, _, extra_settings = connector.connection.authorization.acquire(nil, code, redirect_url)
161
192
  tokens ||= {}
162
193
  extra_settings ||= {}
163
194
  extra_settings.merge(tokens)
@@ -178,6 +209,13 @@ module Workato
178
209
  response = http.request(request)
179
210
  response.body
180
211
  end
212
+
213
+ def with_user_interaction
214
+ say('')
215
+ yield
216
+ ensure
217
+ say('')
218
+ end
181
219
  end
182
220
  end
183
221
  end
@@ -3,9 +3,33 @@
3
3
 
4
4
  require_relative './block_invocation_refinements'
5
5
 
6
+ using Workato::Extension::HashWithIndifferentAccess
7
+
6
8
  module Workato
7
9
  module Connector
8
10
  module Sdk
11
+ module SorbetTypes
12
+ AcquireOutput = T.type_alias do
13
+ T.any(
14
+ # oauth2
15
+ [
16
+ HashWithIndifferentAccess, # tokens
17
+ T.untyped, # resource_owner_id
18
+ T.nilable(HashWithIndifferentAccess) # settings
19
+ ],
20
+ [
21
+ HashWithIndifferentAccess, # tokens
22
+ T.untyped # resource_owner_id
23
+ ],
24
+ [
25
+ HashWithIndifferentAccess # tokens
26
+ ],
27
+ # custom_auth
28
+ HashWithIndifferentAccess
29
+ )
30
+ end
31
+ end
32
+
9
33
  class Connection
10
34
  extend T::Sig
11
35
 
@@ -15,6 +39,7 @@ module Workato
15
39
  attr_reader :source
16
40
 
17
41
  cattr_accessor :on_settings_update
42
+ cattr_accessor :multi_auth_selected_fallback
18
43
 
19
44
  sig do
20
45
  params(
@@ -24,8 +49,8 @@ module Workato
24
49
  ).void
25
50
  end
26
51
  def initialize(connection: {}, methods: {}, settings: {})
27
- @methods_source = T.let(methods.with_indifferent_access, HashWithIndifferentAccess)
28
- @source = T.let(connection.with_indifferent_access, HashWithIndifferentAccess)
52
+ @methods_source = T.let(HashWithIndifferentAccess.wrap(methods), HashWithIndifferentAccess)
53
+ @source = T.let(HashWithIndifferentAccess.wrap(connection), HashWithIndifferentAccess)
29
54
  @settings = T.let(settings, SorbetTypes::SettingsHash)
30
55
  end
31
56
 
@@ -37,6 +62,7 @@ module Workato
37
62
  sig { returns(HashWithIndifferentAccess) }
38
63
  def settings
39
64
  # we can't freeze or memoise because some developers modify it for storing something temporary in it.
65
+ # always return a new copy
40
66
  @settings.with_indifferent_access
41
67
  end
42
68
 
@@ -64,7 +90,10 @@ module Workato
64
90
 
65
91
  sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
66
92
  def base_uri(settings = nil)
67
- source[:base_uri]&.call(settings ? settings.with_indifferent_access.freeze : self.settings)
93
+ return unless source[:base_uri]
94
+
95
+ merge_settings!(settings) if settings
96
+ Dsl::WithDsl.execute(self, self.settings, &source[:base_uri])
68
97
  end
69
98
 
70
99
  sig do
@@ -101,9 +130,6 @@ module Workato
101
130
  class Authorization
102
131
  extend T::Sig
103
132
 
104
- sig { returns(HashWithIndifferentAccess) }
105
- attr_reader :source
106
-
107
133
  sig do
108
134
  params(
109
135
  connection: Connection,
@@ -123,6 +149,16 @@ module Workato
123
149
  (source[:type].presence || 'none').to_s
124
150
  end
125
151
 
152
+ sig { returns(T::Boolean) }
153
+ def oauth2?
154
+ !!(/oauth2/i =~ type)
155
+ end
156
+
157
+ sig { returns(T::Boolean) }
158
+ def multi?
159
+ @source[:type].to_s == 'multi'
160
+ end
161
+
126
162
  sig { returns(T::Array[T.any(String, Symbol, Regexp, Integer)]) }
127
163
  def refresh_on
128
164
  Array.wrap(source[:refresh_on]).compact
@@ -135,10 +171,10 @@ module Workato
135
171
 
136
172
  sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
137
173
  def client_id(settings = nil)
174
+ @connection.merge_settings!(settings) if settings
138
175
  client_id = source[:client_id]
139
176
 
140
177
  if client_id.is_a?(Proc)
141
- @connection.merge_settings!(settings) if settings
142
178
  Dsl::WithDsl.execute(@connection, @connection.settings, &client_id)
143
179
  else
144
180
  client_id
@@ -147,10 +183,10 @@ module Workato
147
183
 
148
184
  sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
149
185
  def client_secret(settings = nil)
186
+ @connection.merge_settings!(settings) if settings
150
187
  client_secret_source = source[:client_secret]
151
188
 
152
189
  if client_secret_source.is_a?(Proc)
153
- @connection.merge_settings!(settings) if settings
154
190
  Dsl::WithDsl.execute(@connection, @connection.settings, &client_secret_source)
155
191
  else
156
192
  client_secret_source
@@ -159,12 +195,18 @@ module Workato
159
195
 
160
196
  sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
161
197
  def authorization_url(settings = nil)
162
- source[:authorization_url]&.call(settings&.with_indifferent_access || @connection.settings)
198
+ @connection.merge_settings!(settings) if settings
199
+ return unless source[:authorization_url]
200
+
201
+ Dsl::WithDsl.execute(@connection, @connection.settings, &source[:authorization_url])
163
202
  end
164
203
 
165
204
  sig { params(settings: T.nilable(SorbetTypes::SettingsHash)).returns(T.nilable(String)) }
166
205
  def token_url(settings = nil)
167
- source[:token_url]&.call(settings&.with_indifferent_access || @connection.settings)
206
+ @connection.merge_settings!(settings) if settings
207
+ return unless source[:token_url]
208
+
209
+ Dsl::WithDsl.execute(@connection, @connection.settings, &source[:token_url])
168
210
  end
169
211
 
170
212
  sig do
@@ -172,9 +214,10 @@ module Workato
172
214
  settings: T.nilable(SorbetTypes::SettingsHash),
173
215
  oauth2_code: T.nilable(String),
174
216
  redirect_url: T.nilable(String)
175
- ).returns(HashWithIndifferentAccess)
217
+ ).returns(T.nilable(SorbetTypes::AcquireOutput))
176
218
  end
177
219
  def acquire(settings = nil, oauth2_code = nil, redirect_url = nil)
220
+ @connection.merge_settings!(settings) if settings
178
221
  acquire_proc = source[:acquire]
179
222
  raise InvalidDefinitionError, "Expect 'acquire' block" unless acquire_proc
180
223
 
@@ -202,7 +245,7 @@ module Workato
202
245
  ).returns(T::Boolean)
203
246
  end
204
247
  def refresh?(http_code, http_body, exception)
205
- return false unless /oauth2/i =~ type || source[:acquire].present?
248
+ return false unless oauth2? || source[:acquire].present?
206
249
 
207
250
  refresh_on = self.refresh_on
208
251
  refresh_on.blank? || refresh_on.any? do |pattern|
@@ -214,10 +257,10 @@ module Workato
214
257
 
215
258
  sig { params(settings: HashWithIndifferentAccess).returns(T.nilable(HashWithIndifferentAccess)) }
216
259
  def refresh!(settings)
217
- if /oauth2/i =~ type
260
+ if oauth2?
218
261
  refresh_oauth2_token(settings)
219
262
  elsif source[:acquire].present?
220
- acquire(settings)
263
+ T.cast(acquire(settings), T.nilable(HashWithIndifferentAccess))
221
264
  end
222
265
  end
223
266
 
@@ -230,6 +273,7 @@ module Workato
230
273
  )
231
274
  end
232
275
  def refresh(settings = nil, refresh_token = nil)
276
+ @connection.merge_settings!(settings) if settings
233
277
  refresh_proc = source[:refresh]
234
278
  raise InvalidDefinitionError, "Expect 'refresh' block" unless refresh_proc
235
279
 
@@ -244,6 +288,27 @@ module Workato
244
288
  end
245
289
  end
246
290
 
291
+ sig { returns(HashWithIndifferentAccess) }
292
+ def source
293
+ return @source unless multi?
294
+
295
+ unless @source[:selected]
296
+ raise InvalidMultiAuthDefinition, "Multi-auth connection must define 'selected' block"
297
+ end
298
+
299
+ if @source[:options].blank?
300
+ raise InvalidMultiAuthDefinition, "Multi-auth connection must define 'options' list"
301
+ end
302
+
303
+ selected_auth_key = @source[:selected].call(@connection.settings)
304
+ selected_auth_key ||= @connection.multi_auth_selected_fallback&.call(@source[:options])
305
+ selected_auth_value = @source.dig(:options, selected_auth_key)
306
+
307
+ raise UnresolvedMultiAuthOptionError, selected_auth_key unless selected_auth_value
308
+
309
+ selected_auth_value
310
+ end
311
+
247
312
  private
248
313
 
249
314
  sig { returns(HashWithIndifferentAccess) }
@@ -266,7 +331,10 @@ module Workato
266
331
  sig { params(settings: HashWithIndifferentAccess).returns(HashWithIndifferentAccess) }
267
332
  def refresh_oauth2_token_using_refresh(settings)
268
333
  new_tokens, new_settings = refresh(settings, settings[:refresh_token])
269
- new_tokens.with_indifferent_access.merge(new_settings || {})
334
+ new_tokens = HashWithIndifferentAccess.wrap(new_tokens)
335
+ return new_tokens unless new_settings
336
+
337
+ new_tokens.merge(new_settings)
270
338
  end
271
339
 
272
340
  sig { params(settings: HashWithIndifferentAccess).returns(HashWithIndifferentAccess) }
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ using Workato::Extension::HashWithIndifferentAccess
5
+
4
6
  module Workato
5
7
  module Connector
6
8
  module Sdk
@@ -21,10 +23,10 @@ module Workato
21
23
 
22
24
  sig { params(definition: SorbetTypes::SourceHash, settings: SorbetTypes::SettingsHash).void }
23
25
  def initialize(definition, settings = {})
24
- @source = T.let(definition.with_indifferent_access, HashWithIndifferentAccess)
25
- @settings = T.let(settings.with_indifferent_access, HashWithIndifferentAccess)
26
- @connection_source = T.let(@source[:connection] || {}.with_indifferent_access, HashWithIndifferentAccess)
27
- @methods_source = T.let(@source[:methods] || {}.with_indifferent_access, HashWithIndifferentAccess)
26
+ @source = T.let(HashWithIndifferentAccess.wrap(definition), HashWithIndifferentAccess)
27
+ @settings = T.let(HashWithIndifferentAccess.wrap(settings), HashWithIndifferentAccess)
28
+ @connection_source = T.let(HashWithIndifferentAccess.wrap(@source[:connection]), HashWithIndifferentAccess)
29
+ @methods_source = T.let(HashWithIndifferentAccess.wrap(@source[:methods]), HashWithIndifferentAccess)
28
30
  end
29
31
 
30
32
  sig { params(path: String, params: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
@@ -2,6 +2,9 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'aws-sigv4'
5
+ require 'workato/utilities/xml'
6
+
7
+ using Workato::Extension::HashWithIndifferentAccess
5
8
 
6
9
  module Workato
7
10
  module Connector
@@ -55,7 +58,7 @@ module Workato
55
58
  method: method,
56
59
  path: path,
57
60
  params: params,
58
- headers: (headers || {}).with_indifferent_access,
61
+ headers: HashWithIndifferentAccess.wrap(headers),
59
62
  payload: payload
60
63
  )
61
64
 
@@ -134,7 +137,7 @@ module Workato
134
137
  headers: headers,
135
138
  method: :get
136
139
  )
137
- response = Workato::Connector::Sdk::Xml.parse_xml_to_hash(response.body)
140
+ response = Workato::Utilities::Xml.parse_xml_to_hash(response.body)
138
141
 
139
142
  temp_credentials = response.dig('AssumeRoleResponse', 0, 'AssumeRoleResult', 0, 'Credentials', 0)
140
143
  {
@@ -0,0 +1,125 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'csv'
5
+
6
+ module Workato
7
+ module Connector
8
+ module Sdk
9
+ CsvError = Class.new(Sdk::RuntimeError)
10
+
11
+ CsvFormatError = Class.new(CsvError)
12
+
13
+ class CsvFileTooBigError < CsvError
14
+ extend T::Sig
15
+
16
+ sig { returns(Integer) }
17
+ attr_reader :size
18
+
19
+ sig { returns(Integer) }
20
+ attr_reader :max
21
+
22
+ sig { params(size: Integer, max: Integer).void }
23
+ def initialize(size, max)
24
+ super("CSV file is too big. Max allowed: #{max.to_s(:human_size)}, got: #{size.to_s(:human_size)}")
25
+ @size = T.let(size, Integer)
26
+ @max = T.let(max, Integer)
27
+ end
28
+ end
29
+
30
+ class CsvFileTooManyLinesError < CsvError
31
+ extend T::Sig
32
+
33
+ sig { returns(Integer) }
34
+ attr_reader :max
35
+
36
+ sig { params(max: Integer).void }
37
+ def initialize(max)
38
+ super("CSV file has too many lines. Max allowed: #{max}")
39
+ @max = T.let(max, Integer)
40
+ end
41
+ end
42
+
43
+ module Dsl
44
+ module Csv
45
+ extend T::Sig
46
+
47
+ MAX_FILE_SIZE_FOR_PARSE = T.let(30.megabytes, Integer)
48
+ private_constant :MAX_FILE_SIZE_FOR_PARSE
49
+
50
+ MAX_LINES_FOR_PARSE = 65_000
51
+ private_constant :MAX_LINES_FOR_PARSE
52
+
53
+ class << self
54
+ extend T::Sig
55
+
56
+ sig do
57
+ params(
58
+ str: String,
59
+ headers: T.any(T::Boolean, T::Array[String], String),
60
+ col_sep: T.nilable(String),
61
+ row_sep: T.nilable(String),
62
+ quote_char: T.nilable(String),
63
+ skip_blanks: T.nilable(T::Boolean),
64
+ skip_first_line: T::Boolean
65
+ ).returns(
66
+ T::Array[T::Hash[String, T.untyped]]
67
+ )
68
+ end
69
+ def parse(str, headers:, col_sep: nil, row_sep: nil, quote_char: nil, skip_blanks: nil,
70
+ skip_first_line: false)
71
+ if str.bytesize > MAX_FILE_SIZE_FOR_PARSE
72
+ raise CsvFileTooBigError.new(str.bytesize, MAX_FILE_SIZE_FOR_PARSE)
73
+ end
74
+
75
+ index = 0
76
+ options = { col_sep: col_sep, row_sep: row_sep, quote_char: quote_char, headers: headers,
77
+ skip_blanks: skip_blanks }.compact
78
+ Enumerator.new do |consumer|
79
+ CSV.parse(str, options) do |row|
80
+ if index.zero? && skip_first_line
81
+ index += 1
82
+ next
83
+ end
84
+ if index == MAX_LINES_FOR_PARSE
85
+ raise CsvFileTooManyLinesError, MAX_LINES_FOR_PARSE
86
+ end
87
+
88
+ index += 1
89
+ consumer.yield(T.cast(row, CSV::Row).to_hash)
90
+ end
91
+ end.to_a
92
+ rescue CSV::MalformedCSVError => e
93
+ raise CsvFormatError, e
94
+ rescue ArgumentError => e
95
+ raise Sdk::RuntimeError, e.message
96
+ end
97
+
98
+ sig do
99
+ params(
100
+ str: T.nilable(String),
101
+ headers: T.nilable(T::Array[String]),
102
+ col_sep: T.nilable(String),
103
+ row_sep: T.nilable(String),
104
+ quote_char: T.nilable(String),
105
+ force_quotes: T.nilable(T::Boolean),
106
+ blk: T.proc.params(csv: CSV).void
107
+ ).returns(
108
+ String
109
+ )
110
+ end
111
+ def generate(str = nil, headers: nil, col_sep: nil, row_sep: nil, quote_char: nil, force_quotes: nil, &blk)
112
+ options = { col_sep: col_sep, row_sep: row_sep, quote_char: quote_char, headers: headers,
113
+ force_quotes: force_quotes }.compact
114
+ options[:write_headers] = options[:headers].present?
115
+
116
+ ::CSV.generate(str || String.new, options, &blk)
117
+ rescue ArgumentError => e
118
+ raise Sdk::RuntimeError, e.message
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end