twelvedata_ruby 0.3.0 → 0.4.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.
@@ -1,285 +1,335 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Handles endpoint definitions, validation, and parameter management
3
4
  module TwelvedataRuby
4
5
  class Endpoint
5
- DEFAULT_FORMAT = :json
6
- VALID_FORMATS = [DEFAULT_FORMAT, :csv].freeze
6
+ DEFAULT_FORMAT = :json
7
+ VALID_FORMATS = [DEFAULT_FORMAT, :csv].freeze
7
8
 
8
- DEFINITIONS = {
9
- api_usage: {
10
- parameters: {keys: %i[format]},
11
- response: {keys: %i[timestamp current_usage plan_limit]}
9
+ # Complete endpoint definitions with parameters and response structure
10
+ DEFINITIONS = {
11
+ api_usage: {
12
+ parameters: { keys: %i[format] },
13
+ response: { keys: %i[timestamp current_usage plan_limit] },
14
+ },
15
+ stocks: {
16
+ parameters: { keys: %i[symbol exchange country type format] },
17
+ response: { data_keys: %i[symbol name currency exchange country type], collection: :data },
18
+ },
19
+ forex_pairs: {
20
+ parameters: { keys: %i[symbol currency_base currency_quote format] },
21
+ response: { data_keys: %i[symbol currency_group currency_base currency_quote], collection: :data },
22
+ },
23
+ cryptocurrencies: {
24
+ parameters: { keys: %i[symbol exchange currency_base currency_quote format] },
25
+ response: { data_keys: %i[symbol available_exchanges currency_base currency_quote], collection: :data },
26
+ },
27
+ etf: {
28
+ parameters: { keys: %i[symbol format] },
29
+ response: { data_keys: %i[symbol name currency exchange], collection: :data },
30
+ },
31
+ indices: {
32
+ parameters: { keys: %i[symbol country format] },
33
+ response: { data_keys: %i[symbol name country currency], collection: :data },
34
+ },
35
+ exchanges: {
36
+ parameters: { keys: %i[type name code country format] },
37
+ response: { data_keys: %i[name country code timezone], collection: :data },
38
+ },
39
+ cryptocurrency_exchanges: {
40
+ parameters: { keys: %i[name format] },
41
+ response: { data_keys: %i[name], collection: :data },
42
+ },
43
+ technical_indicators: {
44
+ parameters: { keys: [] },
45
+ response: { keys: %i[enable full_name description type overlay parameters output_values tinting] },
46
+ },
47
+ symbol_search: {
48
+ parameters: { keys: %i[symbol outputsize], required: %i[symbol] },
49
+ response: {
50
+ data_keys: %i[symbol instrument_name exchange exchange_timezone instrument_type country],
51
+ collection: :data,
12
52
  },
13
- stocks: {
14
- parameters: {keys: %i[symbol exchange country type format]},
15
- response: {data_keys: %i[symbol name currency exchange country type], collection: :data}
53
+ },
54
+ earliest_timestamp: {
55
+ parameters: { keys: %i[symbol interval exchange] },
56
+ response: { keys: %i[datetime unix_time] },
57
+ },
58
+ time_series: {
59
+ parameters: {
60
+ keys: %i[
61
+ symbol interval exchange country type outputsize format dp order timezone
62
+ start_date end_date previous_close
63
+ ],
64
+ required: %i[symbol interval],
16
65
  },
17
- forex_pairs: {
18
- parameters: {keys: %i[symbol currency_base currency_quote format]},
19
- response: {data_keys: %i[symbol currency_group currency_base currency_quote], collection: :data}
66
+ response: {
67
+ value_keys: %i[datetime open high low close volume],
68
+ collection: :values,
69
+ meta_keys: %i[symbol interval currency exchange_timezone exchange type],
20
70
  },
21
- cryptocurrencies: {
22
- parameters: {keys: %i[symbol exchange currency_base currency_quote format]},
23
- response: {data_keys: %i[symbol available_exchanges currency_base currency_quote], collection: :data}
71
+ },
72
+ quote: {
73
+ parameters: {
74
+ keys: %i[symbol interval exchange country volume_time_period type format],
75
+ required: %i[symbol],
24
76
  },
25
- etf: {
26
- parameters: {keys: %i[symbol format]},
27
- response: {data_keys: %i[symbol name currency exchange], collection: :data}
77
+ response: {
78
+ keys: %i[
79
+ symbol name exchange currency datetime open high low close volume
80
+ previous_close change percent_change average_volume fifty_two_week
81
+ ],
28
82
  },
29
- indices: {
30
- parameters: {keys: %i[symbol country format]},
31
- response: {data_keys: %i[symbol name country currency], collection: :data}
83
+ },
84
+ price: {
85
+ parameters: { keys: %i[symbol exchange country type format], required: %i[symbol] },
86
+ response: { keys: %i[price] },
87
+ },
88
+ eod: {
89
+ parameters: { keys: %i[symbol exchange country type prepost dp], required: %i[symbol] },
90
+ response: { keys: %i[symbol exchange currency datetime close] },
91
+ },
92
+ exchange_rate: {
93
+ parameters: { keys: %i[symbol format precision timezone], required: %i[symbol] },
94
+ response: { keys: %i[symbol rate timestamp] },
95
+ },
96
+ currency_conversion: {
97
+ parameters: { keys: %i[symbol amount format precision timezone], required: %i[symbol amount] },
98
+ response: { keys: %i[symbol rate amount timestamp] },
99
+ },
100
+ complex_data: {
101
+ parameters: {
102
+ keys: %i[symbols intervals start_date end_date dp order timezone methods name],
103
+ required: %i[symbols intervals start_date end_date],
32
104
  },
33
- exchanges: {
34
- parameters: {keys: %i[type name code country format]},
35
- response: {data_keys: %i[name country code timezone], collection: :data}
105
+ response: { keys: %i[data status] },
106
+ http_verb: :post,
107
+ },
108
+ earnings: {
109
+ parameters: { keys: %i[symbol exchange country type period outputsize format], required: %i[symbol] },
110
+ response: { keys: %i[date time eps_estimate eps_actual difference surprise_prc] },
111
+ },
112
+ earnings_calendar: {
113
+ parameters: { keys: %i[format] },
114
+ response: {
115
+ keys: %i[
116
+ symbol name currency exchange country time eps_estimate eps_actual difference surprise_prc
117
+ ],
36
118
  },
37
- cryptocurrency_exchanges: {
38
- parameters: {keys: %i[name format]},
39
- response: {data_keys: %i[name], collection: :data}
40
- },
41
- technical_indicators: {
42
- parameters: {keys: []},
43
- response: {
44
- keys: %i[enable full_name description type overlay parameters output_values tinting]
45
- }
46
- },
47
- symbol_search: {
48
- parameters: {keys: %i[symbol outputsize], required: %i[symbol]},
49
- response: {
50
- data_keys: %i[symbol instrument_name exchange exchange_timezone instrument_type country],
51
- collection: :data
52
- }
53
- },
54
- earliest_timestamp: {
55
- parameters: {keys: %i[symbol interval exchange]},
56
- response: {keys: %i[datetime unix_time]}
57
- },
58
- time_series: {
59
- parameters: {
60
- keys: %i[
61
- symbol
62
- interval
63
- exchange
64
- country
65
- type
66
- outputsize
67
- format
68
- dp
69
- order
70
- timezone
71
- start_date
72
- end_date
73
- previous_close
74
- ],
75
- required: %i[symbol interval]
76
- },
77
- response: {
78
- value_keys: %i[datetime open high low close volume],
79
- collection: :values,
80
- meta_keys: %i[symbol interval currency exchange_timezone exchange type]
81
- }
82
- },
83
- quote: {
84
- parameters: {
85
- keys: %i[symbol interval exchange country volume_time_period type format],
86
- required: %i[symbol],
87
- },
88
- response: {
89
- keys: %i[
90
- symbol
91
- name
92
- exchange
93
- currency
94
- datetime
95
- open
96
- high
97
- low
98
- close
99
- volume
100
- previous_close
101
- change
102
- percent_change
103
- average_volume
104
- fifty_two_week
105
- ]
106
- }
107
- },
108
- price: {
109
- parameters: {keys: %i[symbol exchange country type format], required: %i[symbol]},
110
- response: {keys: %i[price]}
111
- },
112
- eod: {
113
- parameters: {keys: %i[symbol exchange country type prepost dp], required: %i[symbol]},
114
- response: {keys: %i[symbol exchange currency datetime close]}
115
- },
116
- exchange_rate: {
117
- parameters: {keys: %i[symbol format precision timezone], required: %i[symbol]},
118
- response: {keys: %i[symbol rate timestamp]}
119
- },
120
- currency_conversion: {
121
- parameters: {keys: %i[symbol amount format precision timezone], required: %i[symbol amount]},
122
- response: {keys: %i[symbol rate amount timestamp]}
123
- },
124
- complex_data: {
125
- parameters: {
126
- keys: %i[symbols intervals start_date end_date dp order timezone methods name],
127
- required: %i[symbols intervals start_date end_date]
128
- },
129
- response: {keys: %i[data status]},
130
- http_verb: :post
131
- },
132
- earnings: {
133
- parameters: {keys: %i[symbol exchange country type period outputsize format], required: %i[symbol]},
134
- response: {keys: %i[date time eps_estimate eps_actual difference surprise_prc]}
135
- },
136
- earnings_calendar: {
137
- parameters: {keys: %i[format]},
138
- response: {
139
- keys: %i[
140
- symbol
141
- name
142
- currency
143
- exchange
144
- country
145
- time
146
- eps_estimate
147
- eps_estimate
148
- eps_actual
149
- difference
150
- surprise_prc
151
- ]
152
- }
153
- }
154
- }.freeze
155
-
156
- class << self
157
- def definitions
158
- @definitions ||= DEFINITIONS.transform_values {|v|
159
- v.merge(
160
- parameters: {
161
- keys: v[:parameters][:keys].push(:apikey),
162
- required: (v[:parameters][:required] || []).push(:apikey)
163
- }
164
- )
165
- }.to_h
166
- end
167
-
168
- def names
169
- @names ||= definitions.keys
170
- end
171
-
172
- def default_apikey_params
173
- {apikey: Client.instance.apikey}
174
- end
175
-
176
- def valid_name?(name)
177
- names.include?(name.to_sym)
178
- end
179
-
180
- def valid_params?(name, **params)
181
- new(name, **params).valid?
182
- end
183
- alias valid? valid_params?
184
- end
119
+ },
120
+ }.freeze
185
121
 
186
- attr_reader :name, :query_params
122
+ class << self
123
+ # Get processed endpoint definitions with apikey parameter added
124
+ #
125
+ # @return [Hash] Complete endpoint definitions
126
+ def definitions
127
+ @definitions ||= build_definitions
128
+ end
187
129
 
188
- def initialize(name, **query_params)
189
- self.name = name
190
- self.query_params = query_params
130
+ # Get all valid endpoint names
131
+ #
132
+ # @return [Array<Symbol>] Array of endpoint names
133
+ def names
134
+ @names ||= definitions.keys
191
135
  end
192
136
 
193
- def definition
194
- @definition ||= self.class.definitions[name]
137
+ # Get default API key parameters
138
+ #
139
+ # @return [Hash] Default parameters including API key
140
+ def default_apikey_params
141
+ { apikey: Client.instance.apikey }
195
142
  end
196
143
 
197
- def errors
198
- (@errors || {}).compact
144
+ # Validate endpoint name
145
+ #
146
+ # @param name [Symbol, String] Endpoint name to validate
147
+ # @return [Boolean] True if valid endpoint name
148
+ def valid_name?(name)
149
+ names.include?(name&.to_sym)
199
150
  end
200
151
 
201
- def name=(name)
202
- assign_attribute(:name, name.to_s.downcase.to_sym)
152
+ # Validate endpoint parameters
153
+ #
154
+ # @param name [Symbol, String] Endpoint name
155
+ # @param params [Hash] Parameters to validate
156
+ # @return [Boolean] True if parameters are valid
157
+ def valid_params?(name, **params)
158
+ new(name, **params).valid?
203
159
  end
204
160
 
205
- def parameters
206
- return @parameters if definition.nil? || @parameters
161
+ private
207
162
 
208
- params = definition[:parameters]
209
- params.push(:filename) if params.include?(:format) && query_parameters[:format] == :csv
210
- params
211
- end
163
+ def build_definitions
164
+ DEFINITIONS.transform_values do |definition|
165
+ enhanced_params = definition[:parameters].dup
166
+ enhanced_params[:keys] = enhanced_params[:keys] + [:apikey]
167
+ enhanced_params[:required] = (enhanced_params[:required] || []) + [:apikey]
212
168
 
213
- def parameters_keys
214
- keys = parameters&.send(:[], :keys)
215
- keys.push(:filename) if keys && query_params && query_params[:format] == :csv
216
- keys
169
+ definition.merge(parameters: enhanced_params)
170
+ end.freeze
217
171
  end
172
+ end
218
173
 
219
- def query_params_keys
220
- query_params.keys
221
- end
174
+ attr_reader :name, :query_params
222
175
 
223
- def query_params=(query_params)
224
- if (parameters_keys || []).include?(:format) &&
225
- !VALID_FORMATS.include?(query_params[:format])
226
- query_params[:format] = DEFAULT_FORMAT
227
- end
228
- query_params.delete(:filename) if query_params[:filename] && query_params[:format] != :csv
229
- assign_attribute(:query_params, self.class.default_apikey_params.merge(query_params.compact))
230
- end
176
+ # Initialize endpoint with name and parameters
177
+ #
178
+ # @param name [Symbol, String] Endpoint name
179
+ # @param query_params [Hash] Query parameters
180
+ def initialize(name, **query_params)
181
+ @errors = {}
182
+ self.name = name
183
+ self.query_params = query_params
184
+ end
231
185
 
232
- def required_parameters
233
- parameters&.send(:[], :required)
234
- end
186
+ # Get endpoint definition
187
+ #
188
+ # @return [Hash, nil] Endpoint definition hash
189
+ def definition
190
+ @definition ||= self.class.definitions[name]
191
+ end
235
192
 
236
- def valid?
237
- valid_name? && valid_query_params?
238
- end
193
+ # Get validation errors
194
+ #
195
+ # @return [Hash] Hash of validation errors
196
+ def errors
197
+ @errors.compact
198
+ end
239
199
 
240
- def valid_at_attributes?(*attrs)
241
- errors.values_at(*attrs).compact.empty?
242
- end
200
+ # Set endpoint name with validation
201
+ #
202
+ # @param name [Symbol, String] Endpoint name
203
+ def name=(name)
204
+ reset_cached_data
205
+ @name = name.to_s.downcase.to_sym
206
+ validate_name
207
+ end
243
208
 
244
- def valid_name?
245
- valid_at_attributes?(:name)
246
- end
209
+ # Get parameter definition
210
+ #
211
+ # @return [Hash, nil] Parameter definition
212
+ def parameters
213
+ definition&.dig(:parameters)
214
+ end
247
215
 
248
- def valid_query_params?
249
- valid_at_attributes?(:parameters_keys, :required_parameters)
250
- end
216
+ # Get parameter keys including format-specific ones
217
+ #
218
+ # @return [Array<Symbol>, nil] Array of valid parameter keys
219
+ def parameters_keys
220
+ return nil unless parameters
251
221
 
252
- private
222
+ keys = parameters[:keys].dup
223
+ keys << :filename if csv_format_with_filename?
224
+ keys
225
+ end
253
226
 
254
- def assign_attribute(attr_name, value)
255
- @parameters = nil
256
- @definition = nil
257
- instance_variable_set(:"@#{attr_name}", value)
258
- send(:"validate_#{attr_name}")
259
- send(attr_name)
260
- end
227
+ # Get query parameter keys
228
+ #
229
+ # @return [Array<Symbol>] Array of current query parameter keys
230
+ def query_params_keys
231
+ query_params.keys
232
+ end
261
233
 
262
- def init_error(attr_name, invalid_values, error_klass=nil)
263
- error_klass ||= Kernel.const_get("#{self.class.name}#{Utils.camelize(attr_name)}Error")
264
- error_klass.new(endpoint: self, invalid: invalid_values)
265
- end
234
+ # Set query parameters with validation and processing
235
+ #
236
+ # @param query_params [Hash] Query parameters to set
237
+ def query_params=(query_params)
238
+ reset_cached_data
239
+ processed_params = process_query_params(query_params)
240
+ @query_params = self.class.default_apikey_params.merge(processed_params.compact)
241
+ validate_query_params
242
+ end
266
243
 
267
- def update_errors(attrib, invalids, klass=nil)
268
- @errors = errors.merge(attrib => !invalids.nil? && !invalids.empty? ? init_error(attrib, invalids, klass) : nil)
244
+ # Get required parameter keys
245
+ #
246
+ # @return [Array<Symbol>, nil] Array of required parameter keys
247
+ def required_parameters
248
+ parameters&.dig(:required)
249
+ end
250
+
251
+ # Check if endpoint and parameters are valid
252
+ #
253
+ # @return [Boolean] True if valid
254
+ def valid?
255
+ valid_name? && valid_query_params?
256
+ end
257
+
258
+ # Check if name is valid
259
+ #
260
+ # @return [Boolean] True if name is valid
261
+ def valid_name?
262
+ errors[:name].nil?
263
+ end
264
+
265
+ # Check if query parameters are valid
266
+ #
267
+ # @return [Boolean] True if query parameters are valid
268
+ def valid_query_params?
269
+ validate_query_params
270
+ errors[:parameters_keys].nil? && errors[:required_parameters].nil?
271
+ end
272
+
273
+ private
274
+
275
+ def reset_cached_data
276
+ @definition = nil
277
+ end
278
+
279
+ def csv_format_with_filename?
280
+ parameters&.dig(:keys)&.include?(:format) && query_params&.dig(:format) == :csv
281
+ end
282
+
283
+ def process_query_params(params)
284
+ processed = params.dup
285
+
286
+ # Normalize format parameter
287
+ if parameters_keys&.include?(:format)
288
+ processed[:format] = normalize_format(processed[:format])
269
289
  end
270
290
 
271
- def validate_name
272
- is_valid = self.class.valid_name?(name)
273
- invalid_name = name.nil? || name.empty? ? "a blank name" : name
274
- update_errors(:name, is_valid ? nil : invalid_name)
275
- validate_query_params if is_valid && query_params && !valid_query_params?
291
+ # Remove filename if not CSV format
292
+ if processed[:filename] && processed[:format] != :csv
293
+ processed.delete(:filename)
276
294
  end
277
295
 
278
- def validate_query_params
279
- return update_errors(:required_parameters, "Invalid name", EndpointError) unless parameters_keys
296
+ processed
297
+ end
280
298
 
281
- update_errors(:required_parameters, required_parameters.difference(query_params_keys))
282
- update_errors(:parameters_keys, query_params_keys.difference(parameters_keys))
283
- end
299
+ def normalize_format(format)
300
+ VALID_FORMATS.include?(format) ? format : DEFAULT_FORMAT
301
+ end
302
+
303
+ def validate_name
304
+ (@errors.delete(:name) || true) and return if self.class.valid_name?(name)
305
+
306
+ invalid_name = name.nil? || name.to_s.empty? ? "blank name" : name
307
+ @errors[:name] = create_error(:name, invalid_name, EndpointNameError)
308
+ end
309
+
310
+ def validate_query_params
311
+ return unless valid_name? && parameters_keys
312
+
313
+ validate_required_parameters
314
+ validate_parameter_keys
315
+ end
316
+
317
+ def validate_required_parameters
318
+ missing = required_parameters - query_params_keys
319
+ (@errors.delete(:required_parameters) || true) and return if missing.empty?
320
+
321
+ @errors[:required_parameters] = create_error(:required_parameters, missing, EndpointRequiredParametersError)
322
+ end
323
+
324
+ def validate_parameter_keys
325
+ invalid = query_params_keys - parameters_keys
326
+ (@errors.delete(:parameters_keys) || true) and return if invalid.empty?
327
+
328
+ @errors[:parameters_keys] = create_error(:parameters_keys, invalid, EndpointParametersKeysError)
329
+ end
330
+
331
+ def create_error(attr_name, invalid_values, error_class)
332
+ error_class.new(endpoint: self, invalid: invalid_values)
333
+ end
284
334
  end
285
335
  end