api_adaptor 0.1.0 → 1.0.1
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/.rubocop.yml +3 -3
- data/.yardopts +10 -0
- data/CHANGELOG.md +36 -1
- data/CLAUDE.md +423 -0
- data/Gemfile.lock +23 -14
- data/README.md +85 -0
- data/Rakefile +7 -1
- data/lib/api_adaptor/base.rb +137 -0
- data/lib/api_adaptor/exceptions.rb +67 -4
- data/lib/api_adaptor/headers.rb +32 -0
- data/lib/api_adaptor/json_client.rb +172 -3
- data/lib/api_adaptor/list_response.rb +63 -21
- data/lib/api_adaptor/null_logger.rb +53 -39
- data/lib/api_adaptor/response.rb +107 -10
- data/lib/api_adaptor/variables.rb +37 -0
- data/lib/api_adaptor/version.rb +1 -1
- data/lib/api_adaptor.rb +31 -1
- metadata +46 -2
data/lib/api_adaptor/response.rb
CHANGED
|
@@ -4,65 +4,103 @@ require "json"
|
|
|
4
4
|
require "forwardable"
|
|
5
5
|
|
|
6
6
|
module ApiAdaptor
|
|
7
|
-
#
|
|
7
|
+
# Wraps an HTTP response with a JSON body and provides convenient access methods.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# domain context. However on systems within an API we want to present relative URLs.
|
|
12
|
-
# By specifying a base URI, this will convert all matching web_urls into relative URLs
|
|
9
|
+
# Response objects parse JSON and provide hash-like access to the response body.
|
|
10
|
+
# They also handle cache control headers and can convert absolute URLs to relative URLs.
|
|
13
11
|
#
|
|
14
|
-
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# response = client.get_json("https://api.example.com/users")
|
|
14
|
+
# users = response["results"]
|
|
15
|
+
# cache_duration = response.cache_control.max_age
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# => "/foo"
|
|
17
|
+
# @example With relative URLs
|
|
18
|
+
# response = Response.new(http_response, web_urls_relative_to: "https://www.example.com")
|
|
19
|
+
# response['results'][0]['web_url'] # => "/foo" instead of "https://www.example.com/foo"
|
|
20
|
+
#
|
|
21
|
+
# @example Checking cache headers
|
|
22
|
+
# if response.cache_control.public? && response.cache_control.max_age > 300
|
|
23
|
+
# # Cache this response
|
|
24
|
+
# end
|
|
19
25
|
class Response
|
|
20
26
|
extend Forwardable
|
|
21
27
|
include Enumerable
|
|
22
28
|
|
|
29
|
+
# Parses and provides access to HTTP Cache-Control header directives.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# cc = CacheControl.new("public, max-age=3600, must-revalidate")
|
|
33
|
+
# cc.public? # => true
|
|
34
|
+
# cc.max_age # => 3600
|
|
35
|
+
# cc.must_revalidate? # => true
|
|
23
36
|
class CacheControl < Hash
|
|
37
|
+
# Regex pattern for parsing Cache-Control directives
|
|
24
38
|
PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i
|
|
25
39
|
|
|
40
|
+
# Initializes a new CacheControl object by parsing a header value
|
|
41
|
+
#
|
|
42
|
+
# @param value [String, nil] Cache-Control header value
|
|
26
43
|
def initialize(value = nil)
|
|
27
44
|
super()
|
|
28
45
|
parse(value)
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# @return [Boolean] true if cache is public
|
|
31
49
|
def public?
|
|
32
50
|
self["public"]
|
|
33
51
|
end
|
|
34
52
|
|
|
53
|
+
# @return [Boolean] true if cache is private
|
|
35
54
|
def private?
|
|
36
55
|
self["private"]
|
|
37
56
|
end
|
|
38
57
|
|
|
58
|
+
# @return [Boolean] true if no-cache directive is present
|
|
39
59
|
def no_cache?
|
|
40
60
|
self["no-cache"]
|
|
41
61
|
end
|
|
42
62
|
|
|
63
|
+
# @return [Boolean] true if no-store directive is present
|
|
43
64
|
def no_store?
|
|
44
65
|
self["no-store"]
|
|
45
66
|
end
|
|
46
67
|
|
|
68
|
+
# @return [Boolean] true if must-revalidate directive is present
|
|
47
69
|
def must_revalidate?
|
|
48
70
|
self["must-revalidate"]
|
|
49
71
|
end
|
|
50
72
|
|
|
73
|
+
# @return [Boolean] true if proxy-revalidate directive is present
|
|
51
74
|
def proxy_revalidate?
|
|
52
75
|
self["proxy-revalidate"]
|
|
53
76
|
end
|
|
54
77
|
|
|
78
|
+
# Returns the max-age directive value
|
|
79
|
+
#
|
|
80
|
+
# @return [Integer, nil] Maximum age in seconds, or nil if not present
|
|
55
81
|
def max_age
|
|
56
82
|
self["max-age"].to_i if key?("max-age")
|
|
57
83
|
end
|
|
58
84
|
|
|
85
|
+
# Returns the r-maxage directive value
|
|
86
|
+
#
|
|
87
|
+
# Note: r-maxage is a non-standard Cache-Control directive and is not part
|
|
88
|
+
# of RFC 7234. This method exists for compatibility with custom cache implementations.
|
|
89
|
+
#
|
|
90
|
+
# @return [Integer, nil] Reverse maximum age in seconds, or nil if not present
|
|
59
91
|
def reverse_max_age
|
|
60
92
|
self["r-maxage"].to_i if key?("r-maxage")
|
|
61
93
|
end
|
|
62
94
|
alias r_maxage reverse_max_age
|
|
63
95
|
|
|
96
|
+
# Returns the s-maxage (shared max age) directive value
|
|
97
|
+
#
|
|
98
|
+
# The s-maxage directive is like max-age but only applies to shared caches
|
|
99
|
+
# (e.g., CDNs, proxies). It overrides max-age for shared caches.
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer, nil] Shared maximum age in seconds, or nil if not present
|
|
64
102
|
def shared_max_age
|
|
65
|
-
self["s-maxage"].to_i if key?("
|
|
103
|
+
self["s-maxage"].to_i if key?("s-maxage")
|
|
66
104
|
end
|
|
67
105
|
alias s_maxage shared_max_age
|
|
68
106
|
|
|
@@ -92,26 +130,67 @@ module ApiAdaptor
|
|
|
92
130
|
end
|
|
93
131
|
end
|
|
94
132
|
|
|
133
|
+
# @!method [](key)
|
|
134
|
+
# Access parsed JSON response by key
|
|
135
|
+
# @param key [String, Symbol] The key to access
|
|
136
|
+
# @return [Object] The value at the key
|
|
137
|
+
#
|
|
138
|
+
# @!method <=>(other)
|
|
139
|
+
# Compare responses
|
|
140
|
+
# @param other [Response] Another response
|
|
141
|
+
# @return [Integer] Comparison result
|
|
142
|
+
#
|
|
143
|
+
# @!method each(&block)
|
|
144
|
+
# Iterate over parsed response hash
|
|
145
|
+
# @yield [key, value] Each key-value pair
|
|
146
|
+
#
|
|
147
|
+
# @!method dig(*keys)
|
|
148
|
+
# Dig into nested hash structure
|
|
149
|
+
# @param keys [Array] Keys to traverse
|
|
150
|
+
# @return [Object, nil] The value at the path or nil
|
|
95
151
|
def_delegators :to_hash, :[], :"<=>", :each, :dig
|
|
96
152
|
|
|
153
|
+
# Initializes a new Response object
|
|
154
|
+
#
|
|
155
|
+
# @param http_response [RestClient::Response] The raw HTTP response
|
|
156
|
+
# @param options [Hash] Configuration options
|
|
157
|
+
# @option options [String] :web_urls_relative_to Base URL for converting absolute URLs to relative
|
|
158
|
+
#
|
|
159
|
+
# @example
|
|
160
|
+
# response = Response.new(http_response)
|
|
161
|
+
#
|
|
162
|
+
# @example With relative URLs
|
|
163
|
+
# response = Response.new(http_response, web_urls_relative_to: "https://www.example.com")
|
|
97
164
|
def initialize(http_response, options = {})
|
|
98
165
|
@http_response = http_response
|
|
99
166
|
@web_urls_relative_to = options[:web_urls_relative_to] ? URI.parse(options[:web_urls_relative_to]) : nil
|
|
100
167
|
end
|
|
101
168
|
|
|
169
|
+
# Returns the raw response body string
|
|
170
|
+
#
|
|
171
|
+
# @return [String] Raw response body
|
|
102
172
|
def raw_response_body
|
|
103
173
|
@http_response.body
|
|
104
174
|
end
|
|
105
175
|
|
|
176
|
+
# Returns the HTTP status code
|
|
177
|
+
#
|
|
178
|
+
# @return [Integer] HTTP status code
|
|
106
179
|
def code
|
|
107
180
|
# Return an integer code for consistency with HTTPErrorResponse
|
|
108
181
|
@http_response.code
|
|
109
182
|
end
|
|
110
183
|
|
|
184
|
+
# Returns the response headers
|
|
185
|
+
#
|
|
186
|
+
# @return [Hash] HTTP headers
|
|
111
187
|
def headers
|
|
112
188
|
@http_response.headers
|
|
113
189
|
end
|
|
114
190
|
|
|
191
|
+
# Calculates when the response expires based on cache headers
|
|
192
|
+
#
|
|
193
|
+
# @return [Time, nil] Expiration time or nil if not cacheable
|
|
115
194
|
def expires_at
|
|
116
195
|
if headers[:date] && cache_control.max_age
|
|
117
196
|
response_date = Time.parse(headers[:date])
|
|
@@ -121,6 +200,9 @@ module ApiAdaptor
|
|
|
121
200
|
end
|
|
122
201
|
end
|
|
123
202
|
|
|
203
|
+
# Calculates how many seconds until the response expires
|
|
204
|
+
#
|
|
205
|
+
# @return [Integer, nil] Seconds until expiration or nil if not cacheable
|
|
124
206
|
def expires_in
|
|
125
207
|
return unless headers[:date]
|
|
126
208
|
|
|
@@ -133,22 +215,37 @@ module ApiAdaptor
|
|
|
133
215
|
end
|
|
134
216
|
end
|
|
135
217
|
|
|
218
|
+
# Returns parsed Cache-Control header
|
|
219
|
+
#
|
|
220
|
+
# @return [CacheControl] Parsed cache control directives
|
|
136
221
|
def cache_control
|
|
137
222
|
@cache_control ||= CacheControl.new(headers[:cache_control])
|
|
138
223
|
end
|
|
139
224
|
|
|
225
|
+
# Returns the parsed JSON response as a hash
|
|
226
|
+
#
|
|
227
|
+
# @return [Hash] Parsed JSON response
|
|
140
228
|
def to_hash
|
|
141
229
|
parsed_content
|
|
142
230
|
end
|
|
143
231
|
|
|
232
|
+
# Returns the parsed and transformed JSON content
|
|
233
|
+
#
|
|
234
|
+
# @return [Hash, Array] Parsed JSON with transformed web_urls
|
|
144
235
|
def parsed_content
|
|
145
236
|
@parsed_content ||= transform_parsed(JSON.parse(@http_response.body))
|
|
146
237
|
end
|
|
147
238
|
|
|
239
|
+
# Always returns true (response is present)
|
|
240
|
+
#
|
|
241
|
+
# @return [Boolean] true
|
|
148
242
|
def present?
|
|
149
243
|
true
|
|
150
244
|
end
|
|
151
245
|
|
|
246
|
+
# Always returns false (response is not blank)
|
|
247
|
+
#
|
|
248
|
+
# @return [Boolean] false
|
|
152
249
|
def blank?
|
|
153
250
|
false
|
|
154
251
|
end
|
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ApiAdaptor
|
|
4
|
+
# Environment variable configuration for User-Agent metadata
|
|
5
|
+
#
|
|
6
|
+
# These variables are used to construct the User-Agent header for HTTP requests,
|
|
7
|
+
# allowing API providers to identify and contact clients if needed.
|
|
8
|
+
#
|
|
9
|
+
# @example Setting environment variables
|
|
10
|
+
# ENV["APP_NAME"] = "MyApiClient"
|
|
11
|
+
# ENV["APP_VERSION"] = "2.1.0"
|
|
12
|
+
# ENV["APP_CONTACT"] = "dev@example.com"
|
|
13
|
+
#
|
|
14
|
+
# @example User-Agent header format
|
|
15
|
+
# # "MyApiClient/2.1.0 (dev@example.com)"
|
|
16
|
+
#
|
|
17
|
+
# @see JSONClient.default_request_headers
|
|
4
18
|
module Variables
|
|
19
|
+
# Returns the application name from environment variable
|
|
20
|
+
#
|
|
21
|
+
# @return [String] Application name (default: "Ruby ApiAdaptor App")
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# ENV["APP_NAME"] = "WikidataClient"
|
|
25
|
+
# Variables.app_name # => "WikidataClient"
|
|
5
26
|
def self.app_name
|
|
6
27
|
ENV["APP_NAME"] || "Ruby ApiAdaptor App"
|
|
7
28
|
end
|
|
8
29
|
|
|
30
|
+
# Returns the application version from environment variable
|
|
31
|
+
#
|
|
32
|
+
# @return [String] Application version (default: "Version not stated")
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# ENV["APP_VERSION"] = "3.2.1"
|
|
36
|
+
# Variables.app_version # => "3.2.1"
|
|
9
37
|
def self.app_version
|
|
10
38
|
ENV["APP_VERSION"] || "Version not stated"
|
|
11
39
|
end
|
|
12
40
|
|
|
41
|
+
# Returns the application contact from environment variable
|
|
42
|
+
#
|
|
43
|
+
# Should be an email address or URL where API providers can reach you.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] Contact information (default: "Contact not stated")
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# ENV["APP_CONTACT"] = "api@example.com"
|
|
49
|
+
# Variables.app_contact # => "api@example.com"
|
|
13
50
|
def self.app_contact
|
|
14
51
|
ENV["APP_CONTACT"] || "Contact not stated"
|
|
15
52
|
end
|
data/lib/api_adaptor/version.rb
CHANGED
data/lib/api_adaptor.rb
CHANGED
|
@@ -3,7 +3,37 @@
|
|
|
3
3
|
require_relative "api_adaptor/version"
|
|
4
4
|
require_relative "api_adaptor/base"
|
|
5
5
|
|
|
6
|
+
# ApiAdaptor provides a framework for building JSON API clients with minimal boilerplate.
|
|
7
|
+
#
|
|
8
|
+
# It handles common patterns like request/response parsing, authentication, redirect handling,
|
|
9
|
+
# pagination, and error management, allowing you to focus on your API's specific logic.
|
|
10
|
+
#
|
|
11
|
+
# @example Building a simple API client
|
|
12
|
+
# class MyApiClient < ApiAdaptor::Base
|
|
13
|
+
# def initialize
|
|
14
|
+
# super("https://api.example.com")
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def get_user(id)
|
|
18
|
+
# get_json("/users/#{id}")
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# def create_user(data)
|
|
22
|
+
# post_json("/users", data)
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# client = MyApiClient.new
|
|
27
|
+
# user = client.get_user(123)
|
|
28
|
+
#
|
|
29
|
+
# @example Using JSONClient directly
|
|
30
|
+
# client = ApiAdaptor::JSONClient.new(bearer_token: "abc123")
|
|
31
|
+
# response = client.get_json("https://api.example.com/data")
|
|
32
|
+
# puts response["items"]
|
|
33
|
+
#
|
|
34
|
+
# @see Base Base class for building API clients
|
|
35
|
+
# @see JSONClient Low-level HTTP client with JSON support
|
|
6
36
|
module ApiAdaptor
|
|
37
|
+
# Base error class for all ApiAdaptor exceptions
|
|
7
38
|
class Error < StandardError; end
|
|
8
|
-
# Your code goes here...
|
|
9
39
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: api_adaptor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Huw Diprose
|
|
@@ -119,6 +119,20 @@ dependencies:
|
|
|
119
119
|
- - "~>"
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
121
|
version: '13.0'
|
|
122
|
+
- !ruby/object:Gem::Dependency
|
|
123
|
+
name: redcarpet
|
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - "~>"
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '3.6'
|
|
129
|
+
type: :development
|
|
130
|
+
prerelease: false
|
|
131
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - "~>"
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '3.6'
|
|
122
136
|
- !ruby/object:Gem::Dependency
|
|
123
137
|
name: rspec
|
|
124
138
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -147,6 +161,20 @@ dependencies:
|
|
|
147
161
|
- - "~>"
|
|
148
162
|
- !ruby/object:Gem::Version
|
|
149
163
|
version: '1.21'
|
|
164
|
+
- !ruby/object:Gem::Dependency
|
|
165
|
+
name: rubocop-yard
|
|
166
|
+
requirement: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '0'
|
|
171
|
+
type: :development
|
|
172
|
+
prerelease: false
|
|
173
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
174
|
+
requirements:
|
|
175
|
+
- - ">="
|
|
176
|
+
- !ruby/object:Gem::Version
|
|
177
|
+
version: '0'
|
|
150
178
|
- !ruby/object:Gem::Dependency
|
|
151
179
|
name: simplecov
|
|
152
180
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -189,6 +217,20 @@ dependencies:
|
|
|
189
217
|
- - "~>"
|
|
190
218
|
- !ruby/object:Gem::Version
|
|
191
219
|
version: '3.18'
|
|
220
|
+
- !ruby/object:Gem::Dependency
|
|
221
|
+
name: yard
|
|
222
|
+
requirement: !ruby/object:Gem::Requirement
|
|
223
|
+
requirements:
|
|
224
|
+
- - "~>"
|
|
225
|
+
- !ruby/object:Gem::Version
|
|
226
|
+
version: '0.9'
|
|
227
|
+
type: :development
|
|
228
|
+
prerelease: false
|
|
229
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
230
|
+
requirements:
|
|
231
|
+
- - "~>"
|
|
232
|
+
- !ruby/object:Gem::Version
|
|
233
|
+
version: '0.9'
|
|
192
234
|
description: A basic adaptor to send HTTP requests and parse the responses. Intended
|
|
193
235
|
to bootstrap the quick writing of Adaptors for specific APIs, without having to
|
|
194
236
|
write the same old JSON request and processing time and time again.
|
|
@@ -201,7 +243,9 @@ files:
|
|
|
201
243
|
- ".env.example"
|
|
202
244
|
- ".rspec"
|
|
203
245
|
- ".rubocop.yml"
|
|
246
|
+
- ".yardopts"
|
|
204
247
|
- CHANGELOG.md
|
|
248
|
+
- CLAUDE.md
|
|
205
249
|
- CODE_OF_CONDUCT.md
|
|
206
250
|
- Gemfile
|
|
207
251
|
- Gemfile.lock
|
|
@@ -242,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
242
286
|
- !ruby/object:Gem::Version
|
|
243
287
|
version: '0'
|
|
244
288
|
requirements: []
|
|
245
|
-
rubygems_version: 4.0.
|
|
289
|
+
rubygems_version: 4.0.6
|
|
246
290
|
specification_version: 4
|
|
247
291
|
summary: A basic adaptor to send HTTP requests and parse the responses.
|
|
248
292
|
test_files: []
|