twelvedata_ruby 0.2.2 → 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.
- checksums.yaml +4 -4
- data/.rspec +4 -0
- data/.rubocop.yml +102 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +199 -4
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +392 -88
- data/Rakefile +110 -4
- data/lib/twelvedata_ruby/client.rb +137 -88
- data/lib/twelvedata_ruby/endpoint.rb +292 -242
- data/lib/twelvedata_ruby/error.rb +93 -45
- data/lib/twelvedata_ruby/request.rb +106 -33
- data/lib/twelvedata_ruby/response.rb +270 -99
- data/lib/twelvedata_ruby/utils.rb +91 -14
- data/lib/twelvedata_ruby/version.rb +6 -0
- data/lib/twelvedata_ruby.rb +41 -30
- data/twelvedata_ruby.gemspec +28 -23
- metadata +25 -143
- data/.gitignore +0 -13
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -80
- data/bin/console +0 -22
- data/bin/setup +0 -8
@@ -1,90 +1,138 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TwelvedataRuby
|
4
|
+
# Base error class for all TwelvedataRuby errors
|
4
5
|
class Error < StandardError
|
5
|
-
|
6
|
-
|
7
|
-
"
|
8
|
-
"
|
9
|
-
|
10
|
-
"EndpointRequiredParametersError" => "Missing
|
11
|
-
|
12
|
-
"ResponseError" => "
|
6
|
+
# Default error messages for different error types
|
7
|
+
DEFAULT_MESSAGES = {
|
8
|
+
"EndpointError" => "Endpoint is not valid: %{invalid}",
|
9
|
+
"EndpointNameError" => "`%{invalid}` is not a valid endpoint. Valid endpoints: %{valid_names}",
|
10
|
+
"EndpointParametersKeysError" => "Invalid parameters: %{invalid}. Valid parameters for `%{name}`: %{parameters}",
|
11
|
+
"EndpointRequiredParametersError" => "Missing required parameters: %{invalid}.\
|
12
|
+
Required for `%{name}`: %{required}",
|
13
|
+
"ResponseError" => "Response error occurred",
|
14
|
+
"ConfigurationError" => "Configuration error: %{message}",
|
15
|
+
"NetworkError" => "Network error: %{message}",
|
13
16
|
}.freeze
|
14
17
|
|
15
|
-
attr_reader :
|
18
|
+
attr_reader :attributes, :original_error
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
# Initialize error with message and attributes
|
21
|
+
#
|
22
|
+
# @param message [String, nil] Custom error message
|
23
|
+
# @param attributes [Hash] Error attributes for interpolation
|
24
|
+
# @param original_error [Exception, nil] Original exception that caused this error
|
25
|
+
def initialize(message: nil, attributes: {}, original_error: nil)
|
26
|
+
@attributes = attributes
|
27
|
+
@original_error = original_error
|
28
|
+
|
29
|
+
error_message = message || format_default_message
|
30
|
+
super(error_message)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def format_default_message
|
36
|
+
template = DEFAULT_MESSAGES[Utils.demodulize(self.class.name)]
|
37
|
+
return "An error occurred" unless template
|
38
|
+
|
39
|
+
format(template, **attributes)
|
40
|
+
rescue KeyError => e
|
41
|
+
"Error message template missing key: #{e.key}"
|
20
42
|
end
|
21
43
|
end
|
22
44
|
|
45
|
+
# Configuration-related errors
|
46
|
+
class ConfigurationError < Error; end
|
47
|
+
|
48
|
+
# Network-related errors
|
49
|
+
class NetworkError < Error; end
|
50
|
+
|
51
|
+
# Base class for endpoint-related errors
|
23
52
|
class EndpointError < Error
|
24
|
-
def initialize(**
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
)
|
53
|
+
def initialize(endpoint:, invalid:, **options)
|
54
|
+
attributes = {
|
55
|
+
name: endpoint.name,
|
56
|
+
invalid: invalid,
|
57
|
+
valid_names: endpoint.class.names.join(", "),
|
58
|
+
parameters: endpoint&.parameters_keys&.join(", "),
|
59
|
+
required: endpoint&.required_parameters&.join(", "),
|
60
|
+
}
|
61
|
+
|
62
|
+
super(attributes: attributes, **options)
|
35
63
|
end
|
36
64
|
end
|
37
65
|
|
66
|
+
# Error for invalid endpoint names
|
38
67
|
class EndpointNameError < EndpointError; end
|
39
68
|
|
69
|
+
# Error for invalid endpoint parameters
|
40
70
|
class EndpointParametersKeysError < EndpointError; end
|
41
71
|
|
72
|
+
# Error for missing required parameters
|
42
73
|
class EndpointRequiredParametersError < EndpointError; end
|
43
74
|
|
75
|
+
# Base class for API response errors
|
44
76
|
class ResponseError < Error
|
45
|
-
|
77
|
+
# Mapping of API error codes to specific error classes
|
78
|
+
API_ERROR_CODES = {
|
46
79
|
400 => "BadRequestResponseError",
|
47
80
|
401 => "UnauthorizedResponseError",
|
48
81
|
403 => "ForbiddenResponseError",
|
49
82
|
404 => "NotFoundResponseError",
|
50
83
|
414 => "ParameterTooLongResponseError",
|
51
84
|
429 => "TooManyRequestsResponseError",
|
52
|
-
500 => "InternalServerResponseError"
|
85
|
+
500 => "InternalServerResponseError",
|
53
86
|
}.freeze
|
54
|
-
|
87
|
+
|
88
|
+
# Mapping of HTTP error codes to specific error classes
|
89
|
+
HTTP_ERROR_CODES = {
|
55
90
|
404 => "PageNotFoundResponseError",
|
56
91
|
}.freeze
|
57
92
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
93
|
+
class << self
|
94
|
+
# Find appropriate error class for given code and type
|
95
|
+
#
|
96
|
+
# @param code [Integer] Error code
|
97
|
+
# @param error_type [Symbol] Type of error (:api or :http)
|
98
|
+
# @return [String, nil] Error class name
|
99
|
+
def error_class_for_code(code, error_type = :api)
|
100
|
+
case error_type
|
101
|
+
when :api
|
102
|
+
API_ERROR_CODES[code]
|
103
|
+
when :http
|
104
|
+
HTTP_ERROR_CODES[code]
|
105
|
+
end
|
106
|
+
end
|
62
107
|
end
|
63
108
|
|
64
|
-
attr_reader :
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
109
|
+
attr_reader :json_data, :status_code, :request
|
110
|
+
|
111
|
+
# Initialize response error
|
112
|
+
#
|
113
|
+
# @param json_data [Hash] JSON response data
|
114
|
+
# @param request [Request] Original request object
|
115
|
+
# @param status_code [Integer] HTTP/API status code
|
116
|
+
# @param message [String, nil] Custom error message
|
117
|
+
def initialize(json_data: {}, request: nil, status_code: nil, message: nil, **options)
|
118
|
+
@json_data = json_data.is_a?(Hash) ? json_data : {}
|
119
|
+
@status_code = status_code || @json_data[:code]
|
70
120
|
@request = request
|
71
|
-
|
121
|
+
|
122
|
+
error_message = message || @json_data[:message] || "Response error occurred"
|
123
|
+
super(message: error_message, **options)
|
72
124
|
end
|
73
125
|
end
|
74
126
|
|
127
|
+
# Specific API error classes
|
75
128
|
class BadRequestResponseError < ResponseError; end
|
76
|
-
|
77
129
|
class UnauthorizedResponseError < ResponseError; end
|
78
|
-
|
79
130
|
class ForbiddenResponseError < ResponseError; end
|
80
|
-
|
81
131
|
class NotFoundResponseError < ResponseError; end
|
82
|
-
|
83
|
-
class PageNotFoundResponseError < ResponseError; end
|
84
|
-
|
85
132
|
class ParameterTooLongResponseError < ResponseError; end
|
86
|
-
|
87
133
|
class TooManyRequestsResponseError < ResponseError; end
|
134
|
+
class InternalServerResponseError < ResponseError; end
|
88
135
|
|
89
|
-
|
136
|
+
# HTTP-specific error classes
|
137
|
+
class PageNotFoundResponseError < ResponseError; end
|
90
138
|
end
|
@@ -1,54 +1,127 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "forwardable"
|
4
|
+
|
5
|
+
# Represents an API request to Twelve Data
|
4
6
|
module TwelvedataRuby
|
5
7
|
class Request
|
6
|
-
|
8
|
+
extend Forwardable
|
7
9
|
|
8
|
-
|
10
|
+
# Default HTTP method for API requests
|
11
|
+
DEFAULT_HTTP_VERB = :get
|
9
12
|
|
10
|
-
|
13
|
+
def_delegators :endpoint, :name, :valid?, :query_params, :errors
|
11
14
|
|
12
|
-
|
13
|
-
self.endpoint = Endpoint.new(name, **query_params)
|
14
|
-
end
|
15
|
-
def_delegators :endpoint, :name, :valid?, :query_params, :errors
|
15
|
+
attr_reader :endpoint
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
# Initialize a new request
|
18
|
+
#
|
19
|
+
# @param name [Symbol, String] Endpoint name
|
20
|
+
# @param query_params [Hash] Query parameters for the request
|
21
|
+
def initialize(name, **query_params)
|
22
|
+
@endpoint = Endpoint.new(name, **query_params)
|
23
|
+
end
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
# Send the request using the client
|
26
|
+
#
|
27
|
+
# @return [Response, Hash, ResponseError] Response or error information
|
28
|
+
def fetch
|
29
|
+
Client.instance.fetch(self)
|
30
|
+
end
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
32
|
+
# Get the HTTP verb for this request
|
33
|
+
#
|
34
|
+
# @return [Symbol, nil] HTTP verb or nil if invalid
|
35
|
+
def http_verb
|
36
|
+
return nil unless valid?
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
end
|
38
|
+
endpoint.definition[:http_verb] || DEFAULT_HTTP_VERB
|
39
|
+
end
|
32
40
|
|
33
|
-
|
34
|
-
|
35
|
-
|
41
|
+
# Get request parameters formatted for HTTP client
|
42
|
+
#
|
43
|
+
# @return [Hash, nil] Parameters hash or nil if invalid
|
44
|
+
def params
|
45
|
+
return nil unless valid?
|
36
46
|
|
37
|
-
|
38
|
-
|
39
|
-
end
|
47
|
+
{ params: endpoint.query_params }
|
48
|
+
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
50
|
+
# Get the relative URL path for this request
|
51
|
+
#
|
52
|
+
# @return [String, nil] URL path or nil if invalid
|
53
|
+
def relative_url
|
54
|
+
return nil unless valid?
|
55
|
+
|
56
|
+
name.to_s
|
57
|
+
end
|
45
58
|
|
46
|
-
|
59
|
+
# Get the complete URL for this request
|
60
|
+
#
|
61
|
+
# @return [String, nil] Full URL or nil if invalid
|
62
|
+
def full_url
|
63
|
+
return nil unless valid?
|
47
64
|
|
48
|
-
|
65
|
+
"#{Client::BASE_URL}/#{relative_url}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Convert request to hash representation
|
69
|
+
#
|
70
|
+
# @return [Hash, nil] Request as hash or nil if invalid
|
71
|
+
def to_h
|
72
|
+
return nil unless valid?
|
73
|
+
|
74
|
+
{
|
75
|
+
http_verb: http_verb,
|
76
|
+
url: full_url,
|
77
|
+
params: query_params,
|
78
|
+
}
|
79
|
+
end
|
49
80
|
|
50
|
-
|
51
|
-
|
81
|
+
# Convert request to array format for HTTPX
|
82
|
+
#
|
83
|
+
# @return [Array, nil] Request as array or nil if invalid
|
84
|
+
def to_a
|
85
|
+
return nil unless valid?
|
86
|
+
|
87
|
+
[http_verb.to_s.upcase, full_url, params]
|
88
|
+
end
|
89
|
+
alias build to_a
|
90
|
+
|
91
|
+
# String representation of the request
|
92
|
+
#
|
93
|
+
# @return [String] Human-readable request description
|
94
|
+
def to_s
|
95
|
+
if valid?
|
96
|
+
"#{http_verb.to_s.upcase} #{full_url} with params: #{query_params}"
|
97
|
+
else
|
98
|
+
"Invalid request for endpoint: #{name}"
|
52
99
|
end
|
53
100
|
end
|
101
|
+
|
102
|
+
# Detailed inspection of the request
|
103
|
+
#
|
104
|
+
# @return [String] Detailed request information
|
105
|
+
def inspect
|
106
|
+
"#<#{self.class.name}:#{object_id} endpoint=#{name} valid=#{valid?} params=#{query_params.keys}>"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check equality with another request
|
110
|
+
#
|
111
|
+
# @param other [Request] Request to compare with
|
112
|
+
# @return [Boolean] True if requests are equivalent
|
113
|
+
def ==(other)
|
114
|
+
return false unless other.is_a?(self.class)
|
115
|
+
|
116
|
+
name == other.name && query_params == other.query_params
|
117
|
+
end
|
118
|
+
alias eql? ==
|
119
|
+
|
120
|
+
# Generate hash code for request
|
121
|
+
#
|
122
|
+
# @return [Integer] Hash code
|
123
|
+
def hash
|
124
|
+
[name, query_params].hash
|
125
|
+
end
|
126
|
+
end
|
54
127
|
end
|