chimera_http_client 1.8.0 → 1.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b13d3c092d285089ec23e09429db2129e3095c398ed1e829800bbcd0574e5c49
4
- data.tar.gz: 8a646ce583338f381130879c86801349af06de6f2b32e19ab0a6a5d6677d8303
3
+ metadata.gz: ca5968573054108171952f955ef7974120a4d4725ed1ca34af29e3d40e831c61
4
+ data.tar.gz: 3f8adfc58275d048326c4cb25c93cc46d637195da9eb042e1c6388d705e3dc54
5
5
  SHA512:
6
- metadata.gz: b2813b2933fa8488973c345392fe879ac64b827180fa1d01dc872f83e13e23ebd4b9e95d9e3d7c17e457a9b73f293812d582a0fa9e22168a36abbb09439715cd
7
- data.tar.gz: 5ec92f4d71bc1925f2e8b3db89b50ec237b554f7ff4d3ac65edc13dd6fd6200aa56ed1aba060e85afd325512eadfeb81fcdb83471995ca015010bbdcea8356b6
6
+ metadata.gz: b00e1318e81f48ea6897e384062ab4e30b9ca83d1dc4549a22ecf8df9ce1940806c47fd04f8bf2f1109cd58922bee78605622fedc61bc74e5fb15d4be7dc0184
7
+ data.tar.gz: 494531a305d3bb571e98d9da1f6ad69330b64de19d29ed8d4cd64a3218fdc92bc3c2c499f7b88b829f714abc5f291654b09d364dff7577e83d6ff3080b68c6ae
data/README.markdown CHANGED
@@ -37,6 +37,8 @@ All information above is given for **Linux**.
37
37
 
38
38
  Setting the environment variable `ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS']` to `true` (or `'true'`) will provide more detailed error messages for logging and also add additional information to the Error JSON. It is recommended to use this only in development environments.
39
39
 
40
+ Setting `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']` to a JSON object string will merge those headers into every `Connection`/`Queue` in the process, e.g. `export CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS='{"X-Service-Name":"orders-api"}'`. This is meant for deployment/ops-level defaults (like identifying which service is calling), set once outside application code. See [Custom headers](#custom-headers) below.
41
+
40
42
  ## Table of Contents
41
43
 
42
44
  <!-- TOC depthFrom:1 depthTo:4 withLinks:1 updateOnSave:0 orderedList:0 -->
@@ -51,6 +53,8 @@ Setting the environment variable `ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS']` to `t
51
53
  - [Mandatory initialization parameter `base_url`](#mandatory-initialization-parameter-base_url)
52
54
  - [Optional initialization parameters](#optional-initialization-parameters)
53
55
  - [Custom deserializers](#custom-deserializers)
56
+ - [Custom headers](#custom-headers)
57
+ - [Custom serializer](#custom-serializer)
54
58
  - [Monitoring, metrics, instrumentation](#monitoring-metrics-instrumentation)
55
59
  - [Request methods](#request-methods)
56
60
  - [Mandatory request parameter `endpoint`](#mandatory-request-parameter-endpoint)
@@ -100,10 +104,12 @@ The optional parameters are:
100
104
 
101
105
  * `cache` - an instance of your cache solution, can be overwritten in any request
102
106
  * `deserializers` - custom methods to deserialize the response body, below more details
107
+ * `headers` - override/extend the default request headers (`{ "Content-Type" => "application/json" }`), can be overwritten or merged with per-request headers, below more details
103
108
  * `logger` - an instance of a logger class that implements `#info`, `#warn` and `#error` methods
104
109
  * `monitor` - to collect metrics about requests, the basis for your instrumentation needs
105
110
  * `retries` - the number of times a failed idempotent request is retried, can be overwritten in any request, the default is `0` (no retries)
106
111
  * `retry_delay` - the base delay in seconds between retries, can be overwritten in any request, the default is `1`
112
+ * `serializer` - override how a Hash/Array request body is turned into a request, can be overwritten in any request, below more details
107
113
  * `timeout` - the timeout for all requests, can be overwritten in any request, the default are 3 seconds
108
114
  * `user_agent` - if you would like your calls to identify with a specific user agent
109
115
  * `verbose` - the default is `false`, set it to true while debugging issues
@@ -118,7 +124,64 @@ A Deserializer has to be an object on which the method `call` with the parameter
118
124
 
119
125
  custom_deserializer.call(body)
120
126
 
121
- where `body` is the response body (in the default case a JSON object). The class `Deserializer` contains the default objects that are used. They might help you creating your own. Don't forget to make requests with another header than the default `"Content-Type" => "application/json"`, when the API you connect to does not support JSON.
127
+ where `body` is the response body (in the default case a JSON object). The class `Deserializer` contains the default objects that are used. They might help you creating your own. If the API you connect to does not support JSON, set `headers` (see [Custom headers](#custom-headers) below) once on the `Connection` instead of repeating it on every request - and see [Custom serializer](#custom-serializer) below for the equivalent on the request-body side.
128
+
129
+ ##### Custom headers
130
+
131
+ Every request sends `{ "Content-Type" => "application/json" }` plus a `User-Agent` by default. Four layers apply on top of each other (each one merges in, overriding only the keys it sets, so you never have to restate headers you're not changing):
132
+
133
+ 1. the built-in default above (also available as `ChimeraHttpClient::Base::DEFAULT_HEADERS`)
134
+ 2. `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']`, a JSON object (see [ENV variables](#env-variables) above)
135
+ 3. `headers` passed to `Connection.new`/`Queue.new` - the connection's own default
136
+ 4. `headers` passed to an individual request - already documented above, unchanged
137
+
138
+ ```ruby
139
+ # connecting to a non-JSON API: override just Content-Type, everything else (User-Agent, ...) still applies
140
+ connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', headers: { "Content-Type" => "application/xml" })
141
+ ```
142
+
143
+ A common use for the connection-level `headers` option is a request/correlation id that should be attached to every call made for one unit of work (a background job, one inbound request being served), without every call site needing to know about it:
144
+
145
+ ```ruby
146
+ connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', headers: { "X-Request-Id" => job_id })
147
+ ```
148
+
149
+ For something that's the same for every request in the whole process instead - e.g. identifying which of your services is calling, in a service-to-service setting - set it once via `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']` at the deployment level rather than in application code:
150
+
151
+ ```bash
152
+ export CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS='{"X-Service-Name":"orders-api"}'
153
+ ```
154
+
155
+ ##### Custom serializer
156
+
157
+ A `Hash` or `Array` `body` is automatically serialized before the request is sent - by default with `body.to_json`, so this now just works:
158
+
159
+ ```ruby
160
+ connection.post!('users', body: { name: "Andy" }) # no more body.to_json needed
161
+ ```
162
+
163
+ A `String` body (e.g. one you already serialized yourself) is always passed through completely unchanged - so any existing code still doing `body: body.to_json` keeps working exactly as before.
164
+
165
+ To use a different format, pass `serializer` to `Connection.new`/`Queue.new`, or to an individual request. A Serializer has to be an object on which the method `call` with the parameter `body` can be called:
166
+
167
+ custom_serializer.call(body)
168
+
169
+ ```ruby
170
+ # talking to an XML API instead
171
+ connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', serializer: ->(body) { body.to_xml })
172
+ ```
173
+
174
+ The serializer normally returns a `String`, but it can also return the `Hash` unconverted - Typhoeus (via Ethon/libcurl) then form-encodes it natively as `application/x-www-form-urlencoded`, or as real multipart if a value looks file-shaped:
175
+
176
+ ```ruby
177
+ # posting a plain form instead of JSON
178
+ connection.post(
179
+ 'login',
180
+ body: { username: "andy", password: "secret" },
181
+ serializer: ->(body) { body }, # hand the Hash to Typhoeus/Ethon unconverted
182
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" }
183
+ )
184
+ ```
122
185
 
123
186
  ##### Monitoring, metrics, instrumentation
124
187
 
@@ -179,6 +242,7 @@ All request methods expect a mandatory `endpoint` and an optional hash as parame
179
242
  * `cache` - optionally overwrite the cache store set in `Connection` in any request
180
243
  * `retries` - optionally overwrite the number of retries set in `Connection` for this request
181
244
  * `retry_delay` - optionally overwrite the retry delay set in `Connection` for this request
245
+ * `serializer` - optionally overwrite the body serializer set in `Connection` for this request
182
246
  * `monitoring_context` - pass additional information you want to collect with your instrumentation `monitor`
183
247
 
184
248
  Example:
@@ -313,7 +377,7 @@ class Users
313
377
  # CREATE a new user by sending attributes in a JSON body and instantiate the new User
314
378
  #
315
379
  def create(body:)
316
- response = connection.post!('users', body: body.to_json) # body.to_json (!!)
380
+ response = connection.post!('users', body: body) # a Hash body is serialized to JSON automatically
317
381
 
318
382
  user = response.parsed_body
319
383
  User.new(id: user['id'], name: user['name'], email: user['email'])
@@ -348,7 +412,7 @@ To create and fetch a user from a remote service with the `Users` wrapper listed
348
412
 
349
413
  Usually it does not have to be used directly. It is the class that executes the `Typhoeus::Requests`, raises `Errors` on failing and returns `Response` objects on successful calls.
350
414
 
351
- The `body` which it receives from the `Connection` class has to be in the in the (serialized) form in which the endpoint expects it. Usually this means you have to pass a JSON string to the `body` (it will **not** be serialized automatically).
415
+ By the time `Request` receives `body`, it's already in the (serialized) form the endpoint expects: `Connection`/`Queue` auto-serialize a `Hash`/`Array` body to JSON (or via a custom `serializer`, see [Custom serializer](#custom-serializer)) before it ever reaches `Request`. A `String` body is passed through unchanged.
352
416
 
353
417
  ## The Response class
354
418
 
data/TODO.markdown CHANGED
@@ -22,6 +22,12 @@ _none known_
22
22
  * [x] ~~use custom deserializer in #parsed_body instead of default JSON parsing~~
23
23
  * [x] ~~add example to README~~
24
24
 
25
+ ### ~~Custom serializer~~
26
+
27
+ * [x] ~~allow to pass custom serializer~~
28
+ * [x] ~~use custom serializer with body instead of default JSON serialization ~~
29
+ * [x] ~~add example to README~~
30
+
25
31
  ### Queueing / running in parallel
26
32
 
27
33
  * [x] ~~allow to queue multiple requests~~
@@ -79,12 +85,12 @@ _none known_
79
85
  * [ ] enable to pass on_headers, on_body, on_complete procs
80
86
  * [ ] add example to README
81
87
 
82
- ### HTTP Headers
88
+ ### ~~HTTP Headers~~
83
89
 
84
- * [ ] make Connection#default_headers configurable
85
- * [ ] allow to set default_headers via ENV vars
86
- * [ ] give example how to use default_headers (e.g. request_id)
87
- * [ ] explain in README how to benefit from this gem in a given setting
90
+ * [x] ~~make Connection#default_headers configurable~~
91
+ * [x] ~~allow to set default_headers via ENV vars~~
92
+ * [x] ~~give example how to use default_headers (e.g. request_id)~~
93
+ * [x] ~~explain in README how to benefit from this gem in a given setting~~
88
94
 
89
95
  ### ~~Caching~~
90
96
 
@@ -1,16 +1,20 @@
1
1
  module ChimeraHttpClient
2
2
  class Base
3
3
  USER_AGENT = "ChimeraHttpClient (by mediafinger)".freeze
4
+ DEFAULT_HEADERS = { "Content-Type" => "application/json" }.freeze
5
+ DEFAULT_HEADERS_ENV_VAR = "CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS".freeze
4
6
 
5
7
  def initialize(options = {})
6
8
  fail(ChimeraHttpClient::ParameterMissingError, "base_url expected, but not given") if options[:base_url].nil?
7
9
 
8
10
  @base_url = options.fetch(:base_url)
9
11
  @deserializer = default_deserializer.merge(options.fetch(:deserializer, {}))
12
+ @serializer = options.fetch(:serializer, ::ChimeraHttpClient::Serializer.json)
10
13
  @logger = options[:logger]
11
14
  @monitor = options[:monitor]
12
15
  @timeout = options[:timeout]
13
16
  @cache = options[:cache]
17
+ @headers = DEFAULT_HEADERS.merge(headers_from_env).merge(options[:headers] || {})
14
18
  @user_agent = options.fetch(:user_agent, USER_AGENT)
15
19
  @verbose = options.fetch(:verbose, false)
16
20
  @retries = options[:retries] || 0
@@ -33,10 +37,13 @@ module ChimeraHttpClient
33
37
  def extract_body(options)
34
38
  body = options.delete(:body)
35
39
  body_optional = options.delete(:body_optional)
40
+ serializer = options.delete(:serializer) || @serializer
36
41
 
37
42
  fail(ChimeraHttpClient::ParameterMissingError, "body expected, but not given") if body.nil? && !body_optional
38
43
 
39
- body
44
+ return body unless body.is_a?(Hash) || body.is_a?(Array)
45
+
46
+ serializer.call(body)
40
47
  end
41
48
 
42
49
  def extract_headers(options, headers)
@@ -45,7 +52,16 @@ module ChimeraHttpClient
45
52
  end
46
53
 
47
54
  def default_headers
48
- { "Content-Type" => "application/json", "User-Agent" => @user_agent }
55
+ @headers.merge("User-Agent" => @user_agent)
56
+ end
57
+
58
+ def headers_from_env
59
+ raw = ENV[DEFAULT_HEADERS_ENV_VAR].to_s
60
+ return {} if raw.strip.empty?
61
+
62
+ JSON.parse(raw)
63
+ rescue JSON::ParserError => e
64
+ raise(ChimeraHttpClient::ParameterMissingError, "ENV['#{DEFAULT_HEADERS_ENV_VAR}'] is not valid JSON: #{e.message}")
49
65
  end
50
66
 
51
67
  def default_deserializer
@@ -0,0 +1,23 @@
1
+ # The default JSON serializer for request bodies.
2
+ #
3
+ # To use a custom serializer, pass it as a param to Connection.new or Queue.new:
4
+ # `serializer: your_serializer`
5
+ # or override it for a single request: `connection.post(endpoint, body: {...}, serializer: your_serializer)`
6
+ #
7
+ # A Serializer has to be an object on which the method `call` with the parameter `body` can be called:
8
+ # `custom_serializer.call(body)`
9
+ # It is only ever invoked for a Hash or Array body - anything else (e.g. an already-serialized
10
+ # String) is passed to the request unchanged. It normally returns a String, but may also return
11
+ # a Hash - Typhoeus/Ethon will then form-encode it natively (application/x-www-form-urlencoded,
12
+ # or multipart if a value looks file-shaped), so an identity serializer (`->(body) { body }`) is
13
+ # a valid way to opt into that instead of JSON.
14
+ #
15
+ module ChimeraHttpClient
16
+ class Serializer
17
+ class << self
18
+ def json
19
+ proc { |body| body.to_json }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module ChimeraHttpClient
2
- VERSION = "1.8.0".freeze
2
+ VERSION = "1.9.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chimera_http_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Finger
@@ -292,6 +292,7 @@ files:
292
292
  - lib/chimera_http_client/queue.rb
293
293
  - lib/chimera_http_client/request.rb
294
294
  - lib/chimera_http_client/response.rb
295
+ - lib/chimera_http_client/serializer.rb
295
296
  - lib/chimera_http_client/version.rb
296
297
  homepage: https://github.com/mediafinger/chimera_http_client
297
298
  licenses: