forest_admin_datasource_rpc 1.16.10 → 1.17.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: c76facc94fe343ca29176a93b36f2a85b1d9e90f33c9d2e9a8300e0c1ff824c8
4
- data.tar.gz: f47529d4767faf8ec9aaf7fe3c3863e26c092a526fc1c99fc6596fd8931ff235
3
+ metadata.gz: 3eddd5bbee08cd80c2df623bc125cfd1c93b5995c7cba4f64b0b4a53a93f7708
4
+ data.tar.gz: 8a57971769da080a8e5a3b030cb7da212665a47054d7fb558d60bf8d0d09b688
5
5
  SHA512:
6
- metadata.gz: dd67fcda71f0307b651d1bb00c98ac03ac184ec9717f623394f3865680497220bc9558b10501aa9052ecf7ca7d94c0fac5f0486e5f1fc5c6f5e61ce0848a4351
7
- data.tar.gz: 2a6d7cd793499fffb01ab99c75f5fbec550aef76b71aeeb82dd582cbd1f0274ef4f40ccb1080c364c58687d9451eea7170d453e8fa7c570bf32c0ff0a2cbf6ab
6
+ metadata.gz: b4b009428c7020b88a55b2673e4f2fc34d11712cb7c710868a3e745f7b1d527d2f2df61e9fbd7a4bf1b70873dc2d5a6358fc85e575f4013f69e9d97b4d3d5ea9
7
+ data.tar.gz: f5052e0d5d2e38572dc15730404ab4ef6d2bdfa75b8733aadffa77566e3c7585a3ec8e779d077be6aa106c78ab762b65dae2adb7d245cc7d5569b4c580f4c5a0
@@ -5,20 +5,17 @@ require 'time'
5
5
 
6
6
  module ForestAdminDatasourceRpc
7
7
  module Utils
8
+ # Response wrapper for schema requests that need ETag
9
+ class SchemaResponse
10
+ attr_reader :body, :etag
11
+
12
+ def initialize(body, etag = nil)
13
+ @body = body
14
+ @etag = etag
15
+ end
16
+ end
17
+
8
18
  class RpcClient
9
- # RpcClient handles HTTP communication with the RPC Agent.
10
- #
11
- # Error Handling:
12
- # When the RPC agent returns an error, this client automatically maps HTTP status codes
13
- # to appropriate Forest Admin exception types. This ensures business errors from the
14
- # RPC agent are properly propagated to the datasource_rpc.
15
- #
16
- # To add support for a new error type:
17
- # 1. Add the status code and exception class to ERROR_STATUS_MAP
18
- # 2. (Optional) Add a default message to generate_default_message method
19
- # 3. Tests will automatically cover the new mapping
20
-
21
- # Map HTTP status codes to Forest Admin exception classes
22
19
  ERROR_STATUS_MAP = {
23
20
  400 => ForestAdminAgent::Http::Exceptions::ValidationError,
24
21
  401 => ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient,
@@ -28,12 +25,37 @@ module ForestAdminDatasourceRpc
28
25
  422 => ForestAdminAgent::Http::Exceptions::UnprocessableError
29
26
  }.freeze
30
27
 
28
+ DEFAULT_ERROR_MESSAGES = {
29
+ 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
30
+ 404 => 'Not Found', 409 => 'Conflict', 422 => 'Unprocessable Entity'
31
+ }.freeze
32
+
33
+ HTTP_NOT_MODIFIED = 304
34
+ NotModified = Class.new
35
+
31
36
  def initialize(api_url, auth_secret)
32
37
  @api_url = api_url
33
38
  @auth_secret = auth_secret
34
39
  end
35
40
 
36
- def call_rpc(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false)
41
+ # rubocop:disable Metrics/ParameterLists
42
+ def call_rpc(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false, if_none_match: nil)
43
+ response = make_request(endpoint, caller: caller, method: method, payload: payload,
44
+ symbolize_keys: symbolize_keys, if_none_match: if_none_match)
45
+ handle_response(response)
46
+ end
47
+
48
+ # rubocop:enable Metrics/ParameterLists
49
+
50
+ def fetch_schema(endpoint, if_none_match: nil)
51
+ response = make_request(endpoint, method: :get, symbolize_keys: true, if_none_match: if_none_match)
52
+ handle_response_with_etag(response)
53
+ end
54
+
55
+ private
56
+
57
+ # rubocop:disable Metrics/ParameterLists
58
+ def make_request(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false, if_none_match: nil)
37
59
  client = Faraday.new(url: @api_url) do |faraday|
38
60
  faraday.request :json
39
61
  faraday.response :json, parser_options: { symbolize_names: symbolize_keys }
@@ -51,22 +73,33 @@ module ForestAdminDatasourceRpc
51
73
  }
52
74
 
53
75
  headers['forest_caller'] = caller.to_json if caller
76
+ headers['If-None-Match'] = %("#{if_none_match}") if if_none_match
54
77
 
55
- response = client.send(method, endpoint, payload, headers)
56
-
57
- handle_response(response)
78
+ client.send(method, endpoint, payload, headers)
58
79
  end
59
-
60
- private
80
+ # rubocop:enable Metrics/ParameterLists
61
81
 
62
82
  def generate_signature(timestamp)
63
83
  OpenSSL::HMAC.hexdigest('SHA256', @auth_secret, timestamp)
64
84
  end
65
85
 
66
86
  def handle_response(response)
67
- raise_appropriate_error(response) unless response.success?
87
+ return response.body if response.success?
88
+ return NotModified if response.status == HTTP_NOT_MODIFIED
89
+
90
+ raise_appropriate_error(response)
91
+ end
92
+
93
+ def handle_response_with_etag(response)
94
+ return SchemaResponse.new(response.body, extract_etag(response)) if response.success?
95
+ return NotModified if response.status == HTTP_NOT_MODIFIED
68
96
 
69
- response.body
97
+ raise_appropriate_error(response)
98
+ end
99
+
100
+ def extract_etag(response)
101
+ etag = response.headers['ETag'] || response.headers['etag']
102
+ etag&.gsub(/\A"?|"?\z/, '')
70
103
  end
71
104
 
72
105
  def raise_appropriate_error(response)
@@ -89,37 +122,18 @@ module ForestAdminDatasourceRpc
89
122
  end
90
123
 
91
124
  def generate_default_message(status, url)
92
- default_messages = {
93
- 400 => "Bad Request: #{url}",
94
- 401 => "Unauthorized: #{url}",
95
- 403 => "Forbidden: #{url}",
96
- 404 => "Not Found: #{url}",
97
- 409 => "Conflict: #{url}",
98
- 422 => "Unprocessable Entity: #{url}"
99
- }
100
-
101
- default_messages[status] || "Unknown error (#{url})"
125
+ prefix = DEFAULT_ERROR_MESSAGES[status] || 'Unknown error'
126
+ "#{prefix}: #{url}"
102
127
  end
103
128
 
104
129
  def parse_error_body(response)
105
130
  body = response.body
106
-
107
- # If body is already a hash (Faraday parsed it as JSON)
108
131
  return symbolize_error_keys(body) if body.is_a?(Hash)
132
+ return { message: 'Unknown error' } unless body.is_a?(String) && !body.empty?
109
133
 
110
- # Try to parse as JSON if it's a string
111
- if body.is_a?(String) && !body.empty?
112
- begin
113
- parsed = JSON.parse(body)
114
- return symbolize_error_keys(parsed)
115
- rescue JSON::ParserError
116
- # If parsing fails, return the body as the message
117
- return { message: body }
118
- end
119
- end
120
-
121
- # Fallback for empty or unexpected body types
122
- { message: 'Unknown error' }
134
+ symbolize_error_keys(JSON.parse(body))
135
+ rescue JSON::ParserError
136
+ { message: body }
123
137
  end
124
138
 
125
139
  def symbolize_error_keys(hash)
@@ -1,3 +1,3 @@
1
1
  module ForestAdminDatasourceRpc
2
- VERSION = "1.16.10"
2
+ VERSION = "1.17.0"
3
3
  end
@@ -13,10 +13,15 @@ module ForestAdminDatasourceRpc
13
13
  auth_secret = options[:auth_secret] || ForestAdminAgent::Facades::Container.cache(:auth_secret)
14
14
  ForestAdminAgent::Facades::Container.logger.log('Info', "Getting schema from RPC agent on #{uri}.")
15
15
 
16
+ schema = nil
17
+ cached_etag = nil
18
+
16
19
  begin
17
20
  rpc_client = Utils::RpcClient.new(uri, auth_secret)
18
- schema = rpc_client.call_rpc('/forest/rpc-schema', method: :get, symbolize_keys: true)
19
- last_hash_schema = Digest::SHA1.hexdigest(schema.to_h.to_s)
21
+ response = rpc_client.fetch_schema('/forest/rpc-schema')
22
+ schema = response.body
23
+ # Use the ETag header for conditional requests
24
+ cached_etag = response.etag
20
25
  rescue Faraday::ConnectionFailed => e
21
26
  ForestAdminAgent::Facades::Container.logger.log(
22
27
  'Error',
@@ -45,11 +50,17 @@ module ForestAdminDatasourceRpc
45
50
  else
46
51
  sse = Utils::SseClient.new("#{uri}/forest/sse", auth_secret) do
47
52
  ForestAdminAgent::Facades::Container.logger.log('Info', 'RPC server stopped, checking schema...')
48
- new_schema = rpc_client.call_rpc('/forest/rpc-schema', method: :get, symbolize_keys: true)
49
53
 
50
- if last_hash_schema == Digest::SHA1.hexdigest(new_schema.to_h.to_s)
51
- ForestAdminAgent::Facades::Container.logger.log('Debug', '[RPCDatasource] Schema has not changed')
54
+ # Send If-None-Match header to check if schema has changed (304 optimization)
55
+ result = rpc_client.fetch_schema('/forest/rpc-schema', if_none_match: cached_etag)
56
+
57
+ # If we get NotModified, schema hasn't changed
58
+ if result == Utils::RpcClient::NotModified
59
+ ForestAdminAgent::Facades::Container.logger.log('Debug', '[RPCDatasource] Schema has not changed (304)')
52
60
  else
61
+ # Schema has changed, update the cached ETag and schema, then reload
62
+ cached_etag = result.etag
63
+ ForestAdminAgent::Facades::Container.logger.log('Info', '[RPCDatasource] Schema changed, reloading agent...')
53
64
  ForestAdminAgent::Builder::AgentFactory.instance.reload!
54
65
  end
55
66
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_datasource_rpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.10
4
+ version: 1.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2025-12-09 00:00:00.000000000 Z
12
+ date: 2025-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: base64