reactor_sdk 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +281 -0
- data/lib/reactor_sdk/authentication.rb +137 -0
- data/lib/reactor_sdk/client.rb +186 -0
- data/lib/reactor_sdk/configuration.rb +102 -0
- data/lib/reactor_sdk/connection.rb +342 -0
- data/lib/reactor_sdk/endpoints/app_configurations.rb +42 -0
- data/lib/reactor_sdk/endpoints/audit_events.rb +64 -0
- data/lib/reactor_sdk/endpoints/base_endpoint.rb +207 -0
- data/lib/reactor_sdk/endpoints/builds.rb +62 -0
- data/lib/reactor_sdk/endpoints/callbacks.rb +38 -0
- data/lib/reactor_sdk/endpoints/companies.rb +42 -0
- data/lib/reactor_sdk/endpoints/data_elements.rb +251 -0
- data/lib/reactor_sdk/endpoints/environments.rb +174 -0
- data/lib/reactor_sdk/endpoints/extension_package_usage_authorizations.rb +51 -0
- data/lib/reactor_sdk/endpoints/extension_packages.rb +63 -0
- data/lib/reactor_sdk/endpoints/extensions.rb +181 -0
- data/lib/reactor_sdk/endpoints/hosts.rb +101 -0
- data/lib/reactor_sdk/endpoints/libraries.rb +872 -0
- data/lib/reactor_sdk/endpoints/notes.rb +11 -0
- data/lib/reactor_sdk/endpoints/profiles.rb +14 -0
- data/lib/reactor_sdk/endpoints/properties.rb +123 -0
- data/lib/reactor_sdk/endpoints/revisions.rb +102 -0
- data/lib/reactor_sdk/endpoints/rule_components.rb +218 -0
- data/lib/reactor_sdk/endpoints/rules.rb +240 -0
- data/lib/reactor_sdk/endpoints/search.rb +23 -0
- data/lib/reactor_sdk/endpoints/secrets.rb +76 -0
- data/lib/reactor_sdk/error.rb +115 -0
- data/lib/reactor_sdk/library_comparison_builder.rb +74 -0
- data/lib/reactor_sdk/library_snapshot_builder.rb +66 -0
- data/lib/reactor_sdk/paginator.rb +92 -0
- data/lib/reactor_sdk/rate_limiter.rb +96 -0
- data/lib/reactor_sdk/reference_extractor.rb +34 -0
- data/lib/reactor_sdk/resource_metadata.rb +73 -0
- data/lib/reactor_sdk/resource_normalizer.rb +90 -0
- data/lib/reactor_sdk/resources/app_configuration.rb +20 -0
- data/lib/reactor_sdk/resources/audit_event.rb +45 -0
- data/lib/reactor_sdk/resources/base_resource.rb +181 -0
- data/lib/reactor_sdk/resources/build.rb +64 -0
- data/lib/reactor_sdk/resources/callback.rb +16 -0
- data/lib/reactor_sdk/resources/company.rb +38 -0
- data/lib/reactor_sdk/resources/comprehensive_data_element.rb +28 -0
- data/lib/reactor_sdk/resources/comprehensive_extension.rb +30 -0
- data/lib/reactor_sdk/resources/comprehensive_resource.rb +31 -0
- data/lib/reactor_sdk/resources/comprehensive_rule.rb +26 -0
- data/lib/reactor_sdk/resources/comprehensive_upstream_chain.rb +50 -0
- data/lib/reactor_sdk/resources/comprehensive_upstream_chain_entry.rb +34 -0
- data/lib/reactor_sdk/resources/data_element.rb +108 -0
- data/lib/reactor_sdk/resources/environment.rb +45 -0
- data/lib/reactor_sdk/resources/extension.rb +66 -0
- data/lib/reactor_sdk/resources/extension_package.rb +49 -0
- data/lib/reactor_sdk/resources/extension_package_usage_authorization.rb +26 -0
- data/lib/reactor_sdk/resources/host.rb +68 -0
- data/lib/reactor_sdk/resources/library.rb +67 -0
- data/lib/reactor_sdk/resources/library_comparison.rb +72 -0
- data/lib/reactor_sdk/resources/library_comparison_entry.rb +144 -0
- data/lib/reactor_sdk/resources/library_snapshot.rb +118 -0
- data/lib/reactor_sdk/resources/library_snapshot_extension_index.rb +70 -0
- data/lib/reactor_sdk/resources/library_snapshot_index.rb +169 -0
- data/lib/reactor_sdk/resources/library_with_resources.rb +194 -0
- data/lib/reactor_sdk/resources/note.rb +37 -0
- data/lib/reactor_sdk/resources/profile.rb +22 -0
- data/lib/reactor_sdk/resources/property.rb +44 -0
- data/lib/reactor_sdk/resources/revision.rb +156 -0
- data/lib/reactor_sdk/resources/rule.rb +44 -0
- data/lib/reactor_sdk/resources/rule_component.rb +101 -0
- data/lib/reactor_sdk/resources/search_results.rb +28 -0
- data/lib/reactor_sdk/resources/secret.rb +17 -0
- data/lib/reactor_sdk/resources/upstream_chain.rb +80 -0
- data/lib/reactor_sdk/resources/upstream_chain_entry.rb +55 -0
- data/lib/reactor_sdk/response_parser.rb +160 -0
- data/lib/reactor_sdk/version.rb +5 -0
- data/lib/reactor_sdk.rb +79 -0
- data/reactor_sdk.gemspec +70 -0
- data/sig/reactor_sdk.rbs +346 -0
- metadata +293 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# @file endpoints/rules.rb
|
|
5
|
+
# @description Endpoint group for Adobe Launch Rule resources.
|
|
6
|
+
#
|
|
7
|
+
# Rules define the logic Adobe Launch executes. They consist of
|
|
8
|
+
# conditions (when to run) and actions (what to do). Rules belong
|
|
9
|
+
# to a property and are versioned — the revisions endpoint provides
|
|
10
|
+
# point-in-time snapshots used by the diff engine.
|
|
11
|
+
#
|
|
12
|
+
# Important: rules must be revised before they can be added to a library.
|
|
13
|
+
# Call revise(rule_id) after creating or updating a rule.
|
|
14
|
+
#
|
|
15
|
+
# @domain Endpoints
|
|
16
|
+
# @see https://developer.adobe.com/experience-platform/documentation/tags/api/endpoints/rules/
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
module ReactorSDK
|
|
20
|
+
module Endpoints
|
|
21
|
+
class Rules < BaseEndpoint
|
|
22
|
+
##
|
|
23
|
+
# Lists all rules for a given property.
|
|
24
|
+
# Follows pagination automatically — returns all rules.
|
|
25
|
+
#
|
|
26
|
+
# @param property_id [String] Adobe property ID
|
|
27
|
+
# @return [Array<ReactorSDK::Resources::Rule>]
|
|
28
|
+
# @raise [ReactorSDK::ResourceNotFoundError] if the property does not exist
|
|
29
|
+
#
|
|
30
|
+
def list_for_property(property_id)
|
|
31
|
+
list_resources("/properties/#{property_id}/rules", Resources::Rule)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Retrieves a single rule by its Adobe ID.
|
|
36
|
+
#
|
|
37
|
+
# @param rule_id [String] Adobe rule ID (format: "RL" + hex string)
|
|
38
|
+
# @return [ReactorSDK::Resources::Rule]
|
|
39
|
+
# @raise [ReactorSDK::ResourceNotFoundError] if the rule does not exist
|
|
40
|
+
#
|
|
41
|
+
def find(rule_id)
|
|
42
|
+
fetch_resource("/rules/#{rule_id}", Resources::Rule)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Retrieves the property that owns a rule.
|
|
47
|
+
#
|
|
48
|
+
# @param rule_id [String] Adobe rule ID
|
|
49
|
+
# @return [ReactorSDK::Resources::Property]
|
|
50
|
+
#
|
|
51
|
+
def property(rule_id)
|
|
52
|
+
fetch_resource("/rules/#{rule_id}/property", Resources::Property)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Lists the libraries containing a rule.
|
|
57
|
+
#
|
|
58
|
+
# @param rule_id [String] Adobe rule ID
|
|
59
|
+
# @return [Array<ReactorSDK::Resources::Library>]
|
|
60
|
+
#
|
|
61
|
+
def libraries(rule_id)
|
|
62
|
+
list_resources("/rules/#{rule_id}/libraries", Resources::Library)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Resolves the rule across the ordered upstream library chain.
|
|
67
|
+
#
|
|
68
|
+
# This is a convenience wrapper around Libraries#upstream_chain_for_resource
|
|
69
|
+
# so rule workflows can stay resource-centric at the call site.
|
|
70
|
+
#
|
|
71
|
+
# @param rule_or_id [String, ReactorSDK::Resources::Rule]
|
|
72
|
+
# @param library_id [String] Adobe library ID used as the comparison root
|
|
73
|
+
# @param property_id [String] Adobe property ID containing the library chain
|
|
74
|
+
# @return [ReactorSDK::Resources::UpstreamChain]
|
|
75
|
+
#
|
|
76
|
+
def upstream_chain(rule_or_id, library_id:, property_id:)
|
|
77
|
+
libraries_endpoint.upstream_chain_for_resource(
|
|
78
|
+
rule_or_id,
|
|
79
|
+
library_id: library_id,
|
|
80
|
+
property_id: property_id,
|
|
81
|
+
resource_type: 'rules'
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Fetches the rule from a library-context review snapshot together with
|
|
87
|
+
# its associated rule components and normalized review payload.
|
|
88
|
+
#
|
|
89
|
+
# @param rule_id [String]
|
|
90
|
+
# @param library_id [String]
|
|
91
|
+
# @param property_id [String]
|
|
92
|
+
# @return [ReactorSDK::Resources::ComprehensiveRule]
|
|
93
|
+
#
|
|
94
|
+
def find_comprehensive(rule_id, library_id:, property_id:)
|
|
95
|
+
snapshot = libraries_endpoint.find_snapshot(library_id, property_id: property_id)
|
|
96
|
+
comprehensive = snapshot.comprehensive_resource(rule_id, resource_type: 'rules')
|
|
97
|
+
unless comprehensive
|
|
98
|
+
raise ReactorSDK::ResourceNotFoundError,
|
|
99
|
+
"Rule #{rule_id} was not found in library #{library_id}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
comprehensive
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Resolves the rule across the ordered upstream chain and returns
|
|
107
|
+
# comprehensive review objects for the target and upstream entries.
|
|
108
|
+
#
|
|
109
|
+
# @param rule_or_id [String, ReactorSDK::Resources::Rule]
|
|
110
|
+
# @param library_id [String]
|
|
111
|
+
# @param property_id [String]
|
|
112
|
+
# @return [ReactorSDK::Resources::ComprehensiveUpstreamChain]
|
|
113
|
+
#
|
|
114
|
+
def comprehensive_upstream_chain(rule_or_id, library_id:, property_id:)
|
|
115
|
+
libraries_endpoint.comprehensive_upstream_chain_for_resource(
|
|
116
|
+
rule_or_id,
|
|
117
|
+
library_id: library_id,
|
|
118
|
+
property_id: property_id,
|
|
119
|
+
resource_type: 'rules'
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Retrieves the origin revision head for a rule.
|
|
125
|
+
#
|
|
126
|
+
# @param rule_id [String] Adobe rule ID
|
|
127
|
+
# @return [ReactorSDK::Resources::Rule]
|
|
128
|
+
#
|
|
129
|
+
def origin(rule_id)
|
|
130
|
+
fetch_resource("/rules/#{rule_id}/origin", Resources::Rule)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# Creates a new rule within a property.
|
|
135
|
+
#
|
|
136
|
+
# @param property_id [String] Adobe property ID
|
|
137
|
+
# @param name [String] Display name for the rule
|
|
138
|
+
# @param enabled [Boolean] Whether the rule is enabled (default: true)
|
|
139
|
+
# @return [ReactorSDK::Resources::Rule] The newly created rule
|
|
140
|
+
# @raise [ReactorSDK::UnprocessableEntityError] if attributes are invalid
|
|
141
|
+
#
|
|
142
|
+
def create(property_id:, name:, enabled: true)
|
|
143
|
+
create_resource(
|
|
144
|
+
"/properties/#{property_id}/rules",
|
|
145
|
+
'rules',
|
|
146
|
+
Resources::Rule,
|
|
147
|
+
attributes: { name: name, enabled: enabled }
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
# Updates an existing rule.
|
|
153
|
+
#
|
|
154
|
+
# @param rule_id [String] Adobe rule ID
|
|
155
|
+
# @param attributes [Hash] Fields to update (e.g. { name: "New Name" })
|
|
156
|
+
# @return [ReactorSDK::Resources::Rule] The updated rule
|
|
157
|
+
# @raise [ReactorSDK::ResourceNotFoundError] if the rule does not exist
|
|
158
|
+
#
|
|
159
|
+
def update(rule_id, attributes)
|
|
160
|
+
update_resource("/rules/#{rule_id}", rule_id, 'rules', Resources::Rule, attributes: attributes)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
##
|
|
164
|
+
# Revises a rule so it can be added to a library.
|
|
165
|
+
#
|
|
166
|
+
# Adobe Launch requires every resource to be explicitly revised before
|
|
167
|
+
# it can be added to a library. A newly created or updated rule cannot
|
|
168
|
+
# be added to a library until revised.
|
|
169
|
+
#
|
|
170
|
+
# Always call revise after create or update, before libraries.add_rules.
|
|
171
|
+
#
|
|
172
|
+
# @param rule_id [String] Adobe rule ID
|
|
173
|
+
# @return [ReactorSDK::Resources::Rule] The revised rule
|
|
174
|
+
# @raise [ReactorSDK::ResourceNotFoundError] if the rule does not exist
|
|
175
|
+
#
|
|
176
|
+
def revise(rule_id)
|
|
177
|
+
update_resource(
|
|
178
|
+
"/rules/#{rule_id}",
|
|
179
|
+
rule_id,
|
|
180
|
+
'rules',
|
|
181
|
+
Resources::Rule,
|
|
182
|
+
attributes: {},
|
|
183
|
+
meta: { action: 'revise' }
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
##
|
|
188
|
+
# Deletes a rule permanently.
|
|
189
|
+
#
|
|
190
|
+
# @param rule_id [String] Adobe rule ID
|
|
191
|
+
# @return [nil]
|
|
192
|
+
# @raise [ReactorSDK::ResourceNotFoundError] if the rule does not exist
|
|
193
|
+
#
|
|
194
|
+
def delete(rule_id)
|
|
195
|
+
delete_resource("/rules/#{rule_id}")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
# Creates a note on a rule.
|
|
200
|
+
#
|
|
201
|
+
# @param rule_id [String] Adobe rule ID
|
|
202
|
+
# @param text [String] Note body text
|
|
203
|
+
# @return [ReactorSDK::Resources::Note]
|
|
204
|
+
#
|
|
205
|
+
def create_note(rule_id, text)
|
|
206
|
+
create_note_for_path("/rules/#{rule_id}/notes", text)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
##
|
|
210
|
+
# Lists notes attached directly to a rule.
|
|
211
|
+
#
|
|
212
|
+
# @param rule_id [String]
|
|
213
|
+
# @return [Array<ReactorSDK::Resources::Note>]
|
|
214
|
+
#
|
|
215
|
+
def list_notes(rule_id)
|
|
216
|
+
list_notes_for_path("/rules/#{rule_id}/notes")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
##
|
|
220
|
+
# Lists rule components associated with the rule's component notes route.
|
|
221
|
+
#
|
|
222
|
+
# @param rule_id [String]
|
|
223
|
+
# @return [Array<ReactorSDK::Resources::RuleComponent>]
|
|
224
|
+
#
|
|
225
|
+
def rule_component_notes(rule_id)
|
|
226
|
+
list_resources("/rules/#{rule_id}/rule_component_notes", Resources::RuleComponent)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def libraries_endpoint
|
|
232
|
+
@libraries_endpoint ||= Endpoints::Libraries.new(
|
|
233
|
+
connection: @connection,
|
|
234
|
+
paginator: @paginator,
|
|
235
|
+
parser: @parser
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactorSDK
|
|
4
|
+
module Endpoints
|
|
5
|
+
class Search < BaseEndpoint
|
|
6
|
+
def perform(query:, from: nil, size: nil, sort: nil, resource_types: nil)
|
|
7
|
+
payload = { query: query }
|
|
8
|
+
payload[:from] = from unless from.nil?
|
|
9
|
+
payload[:size] = size unless size.nil?
|
|
10
|
+
payload[:sort] = sort unless sort.nil?
|
|
11
|
+
payload[:resource_types] = resource_types unless resource_types.nil?
|
|
12
|
+
|
|
13
|
+
response = @connection.post('/search', payload)
|
|
14
|
+
Resources::SearchResults.new(
|
|
15
|
+
results: @parser.parse_many_auto(response['data']),
|
|
16
|
+
meta: response.fetch('meta', {})
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
alias query perform
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactorSDK
|
|
4
|
+
module Endpoints
|
|
5
|
+
class Secrets < BaseEndpoint
|
|
6
|
+
def list_for_property(property_id)
|
|
7
|
+
list_resources("/properties/#{property_id}/secrets", Resources::Secret)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_for_environment(environment_id)
|
|
11
|
+
list_resources("/environments/#{environment_id}/secrets", Resources::Secret)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find(secret_id)
|
|
15
|
+
fetch_resource("/secrets/#{secret_id}", Resources::Secret)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create(property_id:, environment_id:, attributes:)
|
|
19
|
+
create_resource(
|
|
20
|
+
"/properties/#{property_id}/secrets",
|
|
21
|
+
'secrets',
|
|
22
|
+
Resources::Secret,
|
|
23
|
+
attributes: attributes,
|
|
24
|
+
relationships: {
|
|
25
|
+
environment: {
|
|
26
|
+
data: { id: environment_id, type: 'environments' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_or_retry(secret_id, type_of:, action:)
|
|
33
|
+
update_resource(
|
|
34
|
+
"/secrets/#{secret_id}",
|
|
35
|
+
secret_id,
|
|
36
|
+
'secrets',
|
|
37
|
+
Resources::Secret,
|
|
38
|
+
attributes: { type_of: type_of },
|
|
39
|
+
meta: { action: action }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test(secret_id, type_of:)
|
|
44
|
+
test_or_retry(secret_id, type_of: type_of, action: 'test')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def retry(secret_id, type_of:)
|
|
48
|
+
test_or_retry(secret_id, type_of: type_of, action: 'retry')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def delete(secret_id)
|
|
52
|
+
delete_resource("/secrets/#{secret_id}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def data_elements(secret_id)
|
|
56
|
+
list_resources("/secrets/#{secret_id}/data_elements", Resources::DataElement)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def environment(secret_id)
|
|
60
|
+
fetch_resource("/secrets/#{secret_id}/environment", Resources::Environment)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def property(secret_id)
|
|
64
|
+
fetch_resource("/secrets/#{secret_id}/property", Resources::Property)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def list_notes(secret_id)
|
|
68
|
+
list_notes_for_path("/secrets/#{secret_id}/notes")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_note(secret_id, text)
|
|
72
|
+
create_note_for_path("/secrets/#{secret_id}/notes", text)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# @file error.rb
|
|
5
|
+
# @description Typed error hierarchy for the ReactorSDK gem.
|
|
6
|
+
#
|
|
7
|
+
# All errors inherit from ReactorSDK::Error so callers can rescue
|
|
8
|
+
# broadly or narrowly depending on their needs.
|
|
9
|
+
#
|
|
10
|
+
# Rescue broadly:
|
|
11
|
+
# rescue ReactorSDK::Error => e
|
|
12
|
+
#
|
|
13
|
+
# Rescue specifically:
|
|
14
|
+
# rescue ReactorSDK::RateLimitError => e
|
|
15
|
+
# sleep e.retry_after
|
|
16
|
+
# retry
|
|
17
|
+
#
|
|
18
|
+
# @domain Infrastructure
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
module ReactorSDK
|
|
22
|
+
##
|
|
23
|
+
# Base class for all ReactorSDK errors.
|
|
24
|
+
# Every other error in this file inherits from this class.
|
|
25
|
+
#
|
|
26
|
+
class Error < StandardError
|
|
27
|
+
# @return [Integer, nil] HTTP status code from the API response
|
|
28
|
+
attr_reader :status
|
|
29
|
+
|
|
30
|
+
# @return [Exception, nil] The original exception that caused this one
|
|
31
|
+
attr_reader :cause
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @param message [String] Human-readable description of what went wrong
|
|
35
|
+
# @param status [Integer, nil] HTTP status code from the API response
|
|
36
|
+
# @param cause [Exception, nil] Underlying exception if this wraps another error
|
|
37
|
+
#
|
|
38
|
+
def initialize(message, status: nil, cause: nil)
|
|
39
|
+
super(message)
|
|
40
|
+
@status = status
|
|
41
|
+
@cause = cause
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Raised when Adobe IMS token fetch or refresh fails.
|
|
47
|
+
# Most commonly caused by an incorrect client_id or client_secret.
|
|
48
|
+
#
|
|
49
|
+
class AuthenticationError < Error; end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Raised when the token is valid but lacks permission for the resource.
|
|
53
|
+
# Most commonly caused by the org not having access to a property.
|
|
54
|
+
#
|
|
55
|
+
class AuthorizationError < Error; end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Raised when the requested Adobe resource does not exist (HTTP 404).
|
|
59
|
+
#
|
|
60
|
+
class ResourceNotFoundError < Error; end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Raised when a request payload fails Adobe's validation (HTTP 422).
|
|
64
|
+
# Check validation_errors for field-level details returned by Adobe.
|
|
65
|
+
#
|
|
66
|
+
class UnprocessableEntityError < Error
|
|
67
|
+
# @return [Array<Hash>] Validation error objects returned by the Adobe API
|
|
68
|
+
attr_reader :validation_errors
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# @param message [String] Error description
|
|
72
|
+
# @param validation_errors [Array<Hash>] Adobe API validation error objects
|
|
73
|
+
# @param opts [Hash] Passed through to ReactorSDK::Error
|
|
74
|
+
#
|
|
75
|
+
def initialize(message, validation_errors: [], **)
|
|
76
|
+
super(message, **)
|
|
77
|
+
@validation_errors = validation_errors
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Raised when the rate limit is hit after all retries are exhausted (HTTP 429).
|
|
83
|
+
# The retry_after value is taken from Adobe's Retry-After response header.
|
|
84
|
+
#
|
|
85
|
+
class RateLimitError < Error
|
|
86
|
+
# @return [Integer, nil] Seconds to wait before the next request
|
|
87
|
+
attr_reader :retry_after
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# @param message [String] Error description
|
|
91
|
+
# @param retry_after [Integer, nil] Seconds until rate limit resets
|
|
92
|
+
# @param opts [Hash] Passed through to ReactorSDK::Error
|
|
93
|
+
#
|
|
94
|
+
def initialize(message, retry_after: nil, **)
|
|
95
|
+
super(message, **)
|
|
96
|
+
@retry_after = retry_after
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Raised when Adobe returns a 5xx server error after all retries exhausted.
|
|
102
|
+
#
|
|
103
|
+
class ServerError < Error; end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Raised when an HTTP response body cannot be parsed as valid JSON.
|
|
107
|
+
#
|
|
108
|
+
class ParseError < Error; end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Raised when a required configuration value is missing or blank.
|
|
112
|
+
# Caught at client initialization time — never mid-request.
|
|
113
|
+
#
|
|
114
|
+
class ConfigurationError < Error; end
|
|
115
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactorSDK
|
|
4
|
+
class LibraryComparisonBuilder
|
|
5
|
+
RESOURCE_TYPE_ORDER = {
|
|
6
|
+
'rules' => 0,
|
|
7
|
+
'data_elements' => 1,
|
|
8
|
+
'extensions' => 2
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(snapshot_loader:)
|
|
12
|
+
@snapshot_loader = snapshot_loader
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build(current_library_id, baseline_library_id:, property_id:)
|
|
16
|
+
current_snapshot = @snapshot_loader.call(current_library_id, property_id: property_id)
|
|
17
|
+
baseline_snapshot = @snapshot_loader.call(baseline_library_id, property_id: property_id)
|
|
18
|
+
|
|
19
|
+
Resources::LibraryComparison.new(
|
|
20
|
+
current_library_id: current_library_id,
|
|
21
|
+
baseline_library_id: baseline_library_id,
|
|
22
|
+
property_id: property_id,
|
|
23
|
+
current_snapshot: current_snapshot,
|
|
24
|
+
baseline_snapshot: baseline_snapshot,
|
|
25
|
+
entries: build_entries(current_snapshot, baseline_snapshot)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_entries(current_snapshot, baseline_snapshot)
|
|
32
|
+
comparable_resource_ids(current_snapshot, baseline_snapshot)
|
|
33
|
+
.map { |resource_id| build_entry(current_snapshot, baseline_snapshot, resource_id) }
|
|
34
|
+
.sort_by { |entry| entry_sort_key(entry) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def comparable_resource_ids(current_snapshot, baseline_snapshot)
|
|
38
|
+
(current_snapshot.top_level_resources.map(&:id) + baseline_snapshot.top_level_resources.map(&:id)).uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_entry(current_snapshot, baseline_snapshot, resource_id)
|
|
42
|
+
current_resource = current_snapshot.find_resource(resource_id)
|
|
43
|
+
baseline_resource = baseline_snapshot.find_resource(resource_id)
|
|
44
|
+
resource_type = current_resource&.type || baseline_resource&.type
|
|
45
|
+
|
|
46
|
+
Resources::LibraryComparisonEntry.new(
|
|
47
|
+
resource_id: resource_id,
|
|
48
|
+
resource_type: resource_type,
|
|
49
|
+
current_library_id: current_snapshot.library.id,
|
|
50
|
+
baseline_library_id: baseline_snapshot.library.id,
|
|
51
|
+
current_resource: current_resource,
|
|
52
|
+
baseline_resource: baseline_resource,
|
|
53
|
+
current_revision_id: current_snapshot.resource_revision_id(resource_id),
|
|
54
|
+
baseline_revision_id: baseline_snapshot.resource_revision_id(resource_id),
|
|
55
|
+
current_comprehensive_resource: comprehensive_resource_for(current_snapshot, resource_id, resource_type),
|
|
56
|
+
baseline_comprehensive_resource: comprehensive_resource_for(baseline_snapshot, resource_id, resource_type)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def comprehensive_resource_for(snapshot, resource_id, resource_type)
|
|
61
|
+
return nil if resource_type.nil?
|
|
62
|
+
|
|
63
|
+
snapshot.comprehensive_resource(resource_id, resource_type: resource_type)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def entry_sort_key(entry)
|
|
67
|
+
[
|
|
68
|
+
RESOURCE_TYPE_ORDER.fetch(entry.resource_type, RESOURCE_TYPE_ORDER.length),
|
|
69
|
+
entry.resource_name.to_s,
|
|
70
|
+
entry.resource_id
|
|
71
|
+
]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactorSDK
|
|
4
|
+
class LibrarySnapshotBuilder
|
|
5
|
+
def initialize(library_loader:, revisions_endpoint:, rule_components_endpoint:)
|
|
6
|
+
@library_loader = library_loader
|
|
7
|
+
@revisions_endpoint = revisions_endpoint
|
|
8
|
+
@rule_components_endpoint = rule_components_endpoint
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build(library_id, property_id:)
|
|
12
|
+
library = @library_loader.call(library_id)
|
|
13
|
+
|
|
14
|
+
Resources::LibrarySnapshot.new(
|
|
15
|
+
property_id: property_id,
|
|
16
|
+
library: library,
|
|
17
|
+
rule_components_by_rule_id: build_rule_components_index(library)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_rule_components_index(library)
|
|
24
|
+
Array(library.rules).to_h do |rule|
|
|
25
|
+
[rule.id, build_rule_components_snapshot(rule)]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_rule_components_snapshot(rule)
|
|
30
|
+
current_components = @rule_components_endpoint.list_for_rule(rule.id)
|
|
31
|
+
rule_component_ids = point_in_time_rule_component_ids(rule)
|
|
32
|
+
return current_components if rule_component_ids.empty?
|
|
33
|
+
|
|
34
|
+
current_components_by_id = current_components.to_h do |component|
|
|
35
|
+
[component.id, component]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
rule_component_ids.filter_map do |component_id|
|
|
39
|
+
current_components_by_id[component_id] || find_rule_component(component_id)
|
|
40
|
+
end.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def point_in_time_rule_component_ids(rule)
|
|
44
|
+
revision_id = current_revision_id_for(rule)
|
|
45
|
+
return [] if revision_id.nil?
|
|
46
|
+
|
|
47
|
+
revision = @revisions_endpoint.find(revision_id)
|
|
48
|
+
Array(revision.entity_relationships.dig('rule_components', 'data')).filter_map { |item| item['id'] }.uniq
|
|
49
|
+
rescue ReactorSDK::Error
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def current_revision_id_for(rule)
|
|
54
|
+
return rule.revision_id if rule.respond_to?(:revision_id)
|
|
55
|
+
return rule.relationship_id('latest_revision') if rule.respond_to?(:relationship_id)
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def find_rule_component(rule_component_id)
|
|
61
|
+
@rule_components_endpoint.find(rule_component_id)
|
|
62
|
+
rescue ReactorSDK::ResourceNotFoundError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# @file paginator.rb
|
|
5
|
+
# @description Handles cursor-based pagination for Reactor API list endpoints.
|
|
6
|
+
#
|
|
7
|
+
# The Reactor API paginates all list responses using JSON:API cursor links.
|
|
8
|
+
# Each response includes a `links` object with a `next` URL. This class
|
|
9
|
+
# follows that cursor until it is absent, collecting all records into a
|
|
10
|
+
# single flat array.
|
|
11
|
+
#
|
|
12
|
+
# IMPORTANT: Always use this paginator for list endpoints.
|
|
13
|
+
# Never make a single GET to a list endpoint — Adobe defaults to 25 records
|
|
14
|
+
# per page and silently truncates results without any warning or error.
|
|
15
|
+
# A property with 100 rules will return only 25 without pagination.
|
|
16
|
+
#
|
|
17
|
+
# @domain Infrastructure
|
|
18
|
+
# @depends ReactorSDK::Connection
|
|
19
|
+
#
|
|
20
|
+
# @example Fetch all rules for a property
|
|
21
|
+
# paginator = ReactorSDK::Paginator.new(connection)
|
|
22
|
+
# records = paginator.all("/properties/PR123/rules")
|
|
23
|
+
# # => Array of all rule hashes across every page
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
module ReactorSDK
|
|
27
|
+
class Paginator
|
|
28
|
+
# Number of records to request per page.
|
|
29
|
+
# Adobe's maximum is 100 — always request the maximum to minimise
|
|
30
|
+
# the number of API calls made.
|
|
31
|
+
DEFAULT_PAGE_SIZE = 100
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @param connection [ReactorSDK::Connection] Authenticated HTTP connection
|
|
35
|
+
#
|
|
36
|
+
def initialize(connection)
|
|
37
|
+
@connection = connection
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Fetches every record from a paginated list endpoint.
|
|
42
|
+
#
|
|
43
|
+
# Follows the `links.next` cursor in each response until it is absent,
|
|
44
|
+
# then returns all collected records as a single flat array.
|
|
45
|
+
#
|
|
46
|
+
# An optional block can be provided to process each record as it arrives
|
|
47
|
+
# rather than waiting for all pages to complete — useful for large datasets.
|
|
48
|
+
#
|
|
49
|
+
# @param path [String] Relative API path (e.g. "/properties/PR123/rules")
|
|
50
|
+
# @param params [Hash] Additional query parameters to merge into the request
|
|
51
|
+
# @yield [Hash] Each raw JSON:API record hash as it is fetched (optional)
|
|
52
|
+
# @return [Array<Hash>] All records across every page as a flat array
|
|
53
|
+
# @raise [ReactorSDK::Error] on any non-2xx response during pagination
|
|
54
|
+
#
|
|
55
|
+
def all(path, params: {})
|
|
56
|
+
records = []
|
|
57
|
+
next_url = build_initial_url(path, params)
|
|
58
|
+
|
|
59
|
+
while next_url
|
|
60
|
+
response = @connection.get(next_url)
|
|
61
|
+
data = Array(response&.fetch('data', []))
|
|
62
|
+
|
|
63
|
+
data.each do |record|
|
|
64
|
+
yield record if block_given?
|
|
65
|
+
records << record
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
next_url = response&.dig('links', 'next')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
records
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Builds the initial request URL with the page size parameter appended.
|
|
78
|
+
# Merges any caller-supplied params so they are included on every page.
|
|
79
|
+
#
|
|
80
|
+
# @param path [String] Base API path without query string
|
|
81
|
+
# @param params [Hash] Caller-supplied query parameters
|
|
82
|
+
# @return [String] Full path with query string
|
|
83
|
+
#
|
|
84
|
+
def build_initial_url(path, params)
|
|
85
|
+
query_params = { 'page[size]' => DEFAULT_PAGE_SIZE }
|
|
86
|
+
.merge(params.transform_keys(&:to_s))
|
|
87
|
+
|
|
88
|
+
query_string = URI.encode_www_form(query_params)
|
|
89
|
+
"#{path}?#{query_string}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|