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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +281 -0
  5. data/lib/reactor_sdk/authentication.rb +137 -0
  6. data/lib/reactor_sdk/client.rb +186 -0
  7. data/lib/reactor_sdk/configuration.rb +102 -0
  8. data/lib/reactor_sdk/connection.rb +342 -0
  9. data/lib/reactor_sdk/endpoints/app_configurations.rb +42 -0
  10. data/lib/reactor_sdk/endpoints/audit_events.rb +64 -0
  11. data/lib/reactor_sdk/endpoints/base_endpoint.rb +207 -0
  12. data/lib/reactor_sdk/endpoints/builds.rb +62 -0
  13. data/lib/reactor_sdk/endpoints/callbacks.rb +38 -0
  14. data/lib/reactor_sdk/endpoints/companies.rb +42 -0
  15. data/lib/reactor_sdk/endpoints/data_elements.rb +251 -0
  16. data/lib/reactor_sdk/endpoints/environments.rb +174 -0
  17. data/lib/reactor_sdk/endpoints/extension_package_usage_authorizations.rb +51 -0
  18. data/lib/reactor_sdk/endpoints/extension_packages.rb +63 -0
  19. data/lib/reactor_sdk/endpoints/extensions.rb +181 -0
  20. data/lib/reactor_sdk/endpoints/hosts.rb +101 -0
  21. data/lib/reactor_sdk/endpoints/libraries.rb +872 -0
  22. data/lib/reactor_sdk/endpoints/notes.rb +11 -0
  23. data/lib/reactor_sdk/endpoints/profiles.rb +14 -0
  24. data/lib/reactor_sdk/endpoints/properties.rb +123 -0
  25. data/lib/reactor_sdk/endpoints/revisions.rb +102 -0
  26. data/lib/reactor_sdk/endpoints/rule_components.rb +218 -0
  27. data/lib/reactor_sdk/endpoints/rules.rb +240 -0
  28. data/lib/reactor_sdk/endpoints/search.rb +23 -0
  29. data/lib/reactor_sdk/endpoints/secrets.rb +76 -0
  30. data/lib/reactor_sdk/error.rb +115 -0
  31. data/lib/reactor_sdk/library_comparison_builder.rb +74 -0
  32. data/lib/reactor_sdk/library_snapshot_builder.rb +66 -0
  33. data/lib/reactor_sdk/paginator.rb +92 -0
  34. data/lib/reactor_sdk/rate_limiter.rb +96 -0
  35. data/lib/reactor_sdk/reference_extractor.rb +34 -0
  36. data/lib/reactor_sdk/resource_metadata.rb +73 -0
  37. data/lib/reactor_sdk/resource_normalizer.rb +90 -0
  38. data/lib/reactor_sdk/resources/app_configuration.rb +20 -0
  39. data/lib/reactor_sdk/resources/audit_event.rb +45 -0
  40. data/lib/reactor_sdk/resources/base_resource.rb +181 -0
  41. data/lib/reactor_sdk/resources/build.rb +64 -0
  42. data/lib/reactor_sdk/resources/callback.rb +16 -0
  43. data/lib/reactor_sdk/resources/company.rb +38 -0
  44. data/lib/reactor_sdk/resources/comprehensive_data_element.rb +28 -0
  45. data/lib/reactor_sdk/resources/comprehensive_extension.rb +30 -0
  46. data/lib/reactor_sdk/resources/comprehensive_resource.rb +31 -0
  47. data/lib/reactor_sdk/resources/comprehensive_rule.rb +26 -0
  48. data/lib/reactor_sdk/resources/comprehensive_upstream_chain.rb +50 -0
  49. data/lib/reactor_sdk/resources/comprehensive_upstream_chain_entry.rb +34 -0
  50. data/lib/reactor_sdk/resources/data_element.rb +108 -0
  51. data/lib/reactor_sdk/resources/environment.rb +45 -0
  52. data/lib/reactor_sdk/resources/extension.rb +66 -0
  53. data/lib/reactor_sdk/resources/extension_package.rb +49 -0
  54. data/lib/reactor_sdk/resources/extension_package_usage_authorization.rb +26 -0
  55. data/lib/reactor_sdk/resources/host.rb +68 -0
  56. data/lib/reactor_sdk/resources/library.rb +67 -0
  57. data/lib/reactor_sdk/resources/library_comparison.rb +72 -0
  58. data/lib/reactor_sdk/resources/library_comparison_entry.rb +144 -0
  59. data/lib/reactor_sdk/resources/library_snapshot.rb +118 -0
  60. data/lib/reactor_sdk/resources/library_snapshot_extension_index.rb +70 -0
  61. data/lib/reactor_sdk/resources/library_snapshot_index.rb +169 -0
  62. data/lib/reactor_sdk/resources/library_with_resources.rb +194 -0
  63. data/lib/reactor_sdk/resources/note.rb +37 -0
  64. data/lib/reactor_sdk/resources/profile.rb +22 -0
  65. data/lib/reactor_sdk/resources/property.rb +44 -0
  66. data/lib/reactor_sdk/resources/revision.rb +156 -0
  67. data/lib/reactor_sdk/resources/rule.rb +44 -0
  68. data/lib/reactor_sdk/resources/rule_component.rb +101 -0
  69. data/lib/reactor_sdk/resources/search_results.rb +28 -0
  70. data/lib/reactor_sdk/resources/secret.rb +17 -0
  71. data/lib/reactor_sdk/resources/upstream_chain.rb +80 -0
  72. data/lib/reactor_sdk/resources/upstream_chain_entry.rb +55 -0
  73. data/lib/reactor_sdk/response_parser.rb +160 -0
  74. data/lib/reactor_sdk/version.rb +5 -0
  75. data/lib/reactor_sdk.rb +79 -0
  76. data/reactor_sdk.gemspec +70 -0
  77. data/sig/reactor_sdk.rbs +346 -0
  78. metadata +293 -0
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactorSDK
4
+ module Resources
5
+ class LibraryComparisonEntry
6
+ attr_reader :resource_id,
7
+ :resource_type,
8
+ :current_library_id,
9
+ :baseline_library_id,
10
+ :current_resource,
11
+ :baseline_resource,
12
+ :current_revision_id,
13
+ :baseline_revision_id,
14
+ :current_comprehensive_resource,
15
+ :baseline_comprehensive_resource
16
+
17
+ def initialize(
18
+ resource_id:,
19
+ resource_type:,
20
+ current_library_id:,
21
+ baseline_library_id:,
22
+ current_resource:,
23
+ baseline_resource:,
24
+ current_revision_id:,
25
+ baseline_revision_id:,
26
+ current_comprehensive_resource:,
27
+ baseline_comprehensive_resource:
28
+ )
29
+ @resource_id = resource_id
30
+ @resource_type = resource_type
31
+ @current_library_id = current_library_id
32
+ @baseline_library_id = baseline_library_id
33
+ @current_resource = current_resource
34
+ @baseline_resource = baseline_resource
35
+ @current_revision_id = current_revision_id
36
+ @baseline_revision_id = baseline_revision_id
37
+ @current_comprehensive_resource = current_comprehensive_resource
38
+ @baseline_comprehensive_resource = baseline_comprehensive_resource
39
+ end
40
+
41
+ def resource_name
42
+ return current_resource.name if current_resource.respond_to?(:name)
43
+ return baseline_resource.name if baseline_resource.respond_to?(:name)
44
+
45
+ nil
46
+ end
47
+
48
+ def status
49
+ return 'added' if added?
50
+ return 'removed' if removed?
51
+ return 'unchanged' if unchanged?
52
+
53
+ 'modified'
54
+ end
55
+
56
+ def added?
57
+ present_in_current? && !present_in_baseline?
58
+ end
59
+
60
+ def removed?
61
+ !present_in_current? && present_in_baseline?
62
+ end
63
+
64
+ def modified?
65
+ present_in_current? && present_in_baseline? && !unchanged?
66
+ end
67
+
68
+ def unchanged?
69
+ return false unless present_in_current? && present_in_baseline?
70
+
71
+ same_revision? || same_normalized_payload?
72
+ end
73
+
74
+ def changed?
75
+ !unchanged?
76
+ end
77
+
78
+ def present_in_current?
79
+ !current_resource.nil?
80
+ end
81
+
82
+ def present_in_baseline?
83
+ !baseline_resource.nil?
84
+ end
85
+
86
+ def current_normalized_payload
87
+ current_comprehensive_resource&.normalized_payload
88
+ end
89
+
90
+ def baseline_normalized_payload
91
+ baseline_comprehensive_resource&.normalized_payload
92
+ end
93
+
94
+ def current_normalized_json
95
+ current_comprehensive_resource&.normalized_json.to_s
96
+ end
97
+
98
+ def baseline_normalized_json
99
+ baseline_comprehensive_resource&.normalized_json.to_s
100
+ end
101
+
102
+ def changeset_document(position: nil)
103
+ document = {
104
+ path: changeset_path,
105
+ language: 'json',
106
+ old_content: baseline_normalized_json,
107
+ new_content: current_normalized_json,
108
+ metadata: changeset_metadata
109
+ }
110
+ document[:position] = position unless position.nil?
111
+ document
112
+ end
113
+
114
+ private
115
+
116
+ def same_revision?
117
+ return false if current_revision_id.nil? || baseline_revision_id.nil?
118
+
119
+ current_revision_id == baseline_revision_id
120
+ end
121
+
122
+ def same_normalized_payload?
123
+ current_normalized_payload == baseline_normalized_payload
124
+ end
125
+
126
+ def changeset_path
127
+ "reactor/#{resource_type}/#{resource_id}.json"
128
+ end
129
+
130
+ def changeset_metadata
131
+ {
132
+ resource_id: resource_id,
133
+ resource_type: resource_type,
134
+ resource_name: resource_name,
135
+ status: status,
136
+ current_library_id: current_library_id,
137
+ baseline_library_id: baseline_library_id,
138
+ current_revision_id: current_revision_id,
139
+ baseline_revision_id: baseline_revision_id
140
+ }.compact
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactorSDK
4
+ module Resources
5
+ class LibrarySnapshot
6
+ attr_reader :property_id,
7
+ :library,
8
+ :rules,
9
+ :data_elements,
10
+ :extensions,
11
+ :rule_components,
12
+ :rule_components_by_rule_id,
13
+ :resource_by_id
14
+
15
+ def initialize(property_id:, library:, rule_components_by_rule_id:)
16
+ @property_id = property_id
17
+ @library = library
18
+ @rules = Array(library.rules)
19
+ @data_elements = Array(library.data_elements)
20
+ @extensions = Array(library.extensions)
21
+ @index = LibrarySnapshotIndex.new(
22
+ rules: @rules,
23
+ data_elements: @data_elements,
24
+ extensions: @extensions,
25
+ rule_components_by_rule_id: rule_components_by_rule_id
26
+ )
27
+ @rule_components_by_rule_id = @index.rule_components_by_rule_id
28
+ @rule_components = @index.rule_components
29
+ @resource_by_id = @index.resource_by_id
30
+ end
31
+
32
+ def top_level_resources
33
+ @top_level_resources ||= @rules + @data_elements + @extensions
34
+ end
35
+
36
+ def all_resources
37
+ top_level_resources + @rule_components
38
+ end
39
+
40
+ def find_resource(resource_id)
41
+ @index.find_resource(resource_id)
42
+ end
43
+
44
+ def rule_components_for_rule(rule_or_id)
45
+ @index.rule_components_for_rule(rule_or_id)
46
+ end
47
+
48
+ def referenced_data_elements_for(data_element_or_id)
49
+ @index.referenced_data_elements_for(data_element_or_id)
50
+ end
51
+
52
+ def impacted_rules_for(data_element_or_id)
53
+ @index.impacted_rules_for(data_element_or_id)
54
+ end
55
+
56
+ def data_elements_for_extension(extension_or_id)
57
+ @index.data_elements_for_extension(extension_or_id)
58
+ end
59
+
60
+ def rule_components_for_extension(extension_or_id)
61
+ @index.rule_components_for_extension(extension_or_id)
62
+ end
63
+
64
+ def rules_for_extension(extension_or_id)
65
+ @index.rules_for_extension(extension_or_id)
66
+ end
67
+
68
+ def resource_revision_id(resource_or_id)
69
+ resource_id = extract_id(resource_or_id)
70
+ resource = find_resource(resource_id)
71
+ return nil if resource.nil?
72
+ return resource.revision_id if resource.respond_to?(:revision_id)
73
+ return @library.resource_index[resource_id] if @library.resource_index.key?(resource_id)
74
+ return resource.relationship_id('latest_revision') if resource.respond_to?(:relationship_id)
75
+
76
+ nil
77
+ end
78
+
79
+ def comprehensive_resource(resource_id, resource_type: nil)
80
+ resource = find_resource(resource_id)
81
+ return nil if resource.nil?
82
+
83
+ build_comprehensive_resource(resource, resource_type || resource.type)
84
+ end
85
+
86
+ private
87
+
88
+ def build_comprehensive_resource(resource, resource_type)
89
+ case resource_type
90
+ when 'rules'
91
+ ComprehensiveRule.new(
92
+ resource: resource,
93
+ rule_components: rule_components_for_rule(resource.id)
94
+ )
95
+ when 'data_elements'
96
+ ComprehensiveDataElement.new(
97
+ resource: resource,
98
+ referenced_data_elements: referenced_data_elements_for(resource.id),
99
+ impacted_rules: impacted_rules_for(resource.id)
100
+ )
101
+ when 'extensions'
102
+ ComprehensiveExtension.new(
103
+ resource: resource,
104
+ data_elements: data_elements_for_extension(resource.id),
105
+ rule_components: rule_components_for_extension(resource.id),
106
+ rules: rules_for_extension(resource.id)
107
+ )
108
+ end
109
+ end
110
+
111
+ def extract_id(resource_or_id)
112
+ return resource_or_id.id if resource_or_id.respond_to?(:id)
113
+
114
+ resource_or_id
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactorSDK
4
+ module Resources
5
+ class LibrarySnapshotExtensionIndex
6
+ def initialize(data_elements:, rule_components_by_rule_id:, find_resource:, sort_rule_components:)
7
+ @data_elements = Array(data_elements)
8
+ @rule_components_by_rule_id = rule_components_by_rule_id
9
+ @find_resource = find_resource
10
+ @sort_rule_components = sort_rule_components
11
+ @data_elements_by_extension = build_data_elements_index
12
+ @rule_components_by_extension = build_rule_components_index
13
+ @rules_by_extension = build_rules_index
14
+ end
15
+
16
+ def data_elements_for(extension_id)
17
+ @data_elements_by_extension.fetch(extension_id, [])
18
+ end
19
+
20
+ def rule_components_for(extension_id)
21
+ @rule_components_by_extension.fetch(extension_id, [])
22
+ end
23
+
24
+ def rules_for(extension_id)
25
+ @rules_by_extension.fetch(extension_id, [])
26
+ end
27
+
28
+ private
29
+
30
+ def build_data_elements_index
31
+ group_by_relationship(@data_elements, 'extension').transform_values do |items|
32
+ items.sort_by { |item| [item.name.to_s, item.id] }
33
+ end
34
+ end
35
+
36
+ def build_rule_components_index
37
+ group_by_relationship(@rule_components_by_rule_id.values.flatten, 'extension')
38
+ .transform_values { |items| @sort_rule_components.call(items) }
39
+ end
40
+
41
+ def build_rules_index
42
+ rules_by_extension = @rule_components_by_rule_id.each_with_object(new_set_index) do |rule_entry, index|
43
+ rule_id, components = rule_entry
44
+ rule = @find_resource.call(rule_id)
45
+ next if rule.nil?
46
+
47
+ components.each do |component|
48
+ extension_id = component.relationship_id('extension')
49
+ index[extension_id] << rule unless extension_id.nil?
50
+ end
51
+ end
52
+
53
+ rules_by_extension.transform_values do |items|
54
+ items.to_a.sort_by { |rule| [rule.name.to_s, rule.id] }
55
+ end
56
+ end
57
+
58
+ def group_by_relationship(resources, relationship_name)
59
+ resources.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |resource, grouped_resources|
60
+ relationship_id = resource.relationship_id(relationship_name)
61
+ grouped_resources[relationship_id] << resource unless relationship_id.nil?
62
+ end
63
+ end
64
+
65
+ def new_set_index
66
+ Hash.new { |hash, key| hash[key] = Set.new }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactorSDK
4
+ module Resources
5
+ class LibrarySnapshotIndex
6
+ attr_reader :resource_by_id, :rule_components, :rule_components_by_rule_id
7
+
8
+ def initialize(rules:, data_elements:, extensions:, rule_components_by_rule_id:)
9
+ @rules = Array(rules)
10
+ @data_elements = Array(data_elements)
11
+ @extensions = Array(extensions)
12
+ @rule_components_by_rule_id = normalize_rule_components(rule_components_by_rule_id)
13
+ @rule_components = @rule_components_by_rule_id.values.flatten
14
+ @resource_by_id = build_resource_index
15
+ @data_elements_by_name = build_data_element_name_index
16
+ @data_element_dependency_graph = build_data_element_dependency_graph
17
+ @reverse_data_element_graph = invert_graph(@data_element_dependency_graph)
18
+ @rule_dependency_graph = build_rule_dependency_graph
19
+ @rules_by_data_element = invert_graph(@rule_dependency_graph)
20
+ @extension_index = LibrarySnapshotExtensionIndex.new(
21
+ data_elements: @data_elements,
22
+ rule_components_by_rule_id: @rule_components_by_rule_id,
23
+ find_resource: method(:find_resource),
24
+ sort_rule_components: method(:sort_rule_components)
25
+ )
26
+ end
27
+
28
+ def find_resource(resource_id)
29
+ @resource_by_id[extract_id(resource_id)]
30
+ end
31
+
32
+ def rule_components_for_rule(rule_or_id)
33
+ @rule_components_by_rule_id.fetch(extract_id(rule_or_id), [])
34
+ end
35
+
36
+ def referenced_data_elements_for(data_element_or_id)
37
+ referenced_resource_ids(data_element_or_id).filter_map { |resource_id| find_resource(resource_id) }
38
+ end
39
+
40
+ def impacted_rules_for(data_element_or_id)
41
+ impacted_rule_ids_for(data_element_or_id)
42
+ .filter_map { |resource_id| find_resource(resource_id) }
43
+ .sort_by { |rule| [rule.name.to_s, rule.id] }
44
+ end
45
+
46
+ def data_elements_for_extension(extension_or_id)
47
+ @extension_index.data_elements_for(extract_id(extension_or_id))
48
+ end
49
+
50
+ def rule_components_for_extension(extension_or_id)
51
+ @extension_index.rule_components_for(extract_id(extension_or_id))
52
+ end
53
+
54
+ def rules_for_extension(extension_or_id)
55
+ @extension_index.rules_for(extract_id(extension_or_id))
56
+ end
57
+
58
+ private
59
+
60
+ def normalize_rule_components(rule_components_by_rule_id)
61
+ rule_components_by_rule_id.to_h do |rule_id, components|
62
+ [rule_id, sort_rule_components(Array(components))]
63
+ end
64
+ end
65
+
66
+ def sort_rule_components(components)
67
+ components.sort_by do |component|
68
+ [
69
+ sortable_number(component.respond_to?(:rule_order) ? component.rule_order : nil),
70
+ sortable_number(component.respond_to?(:order) ? component.order : nil),
71
+ component.id
72
+ ]
73
+ end
74
+ end
75
+
76
+ def sortable_number(value)
77
+ value.nil? ? Float::INFINITY : value.to_f
78
+ end
79
+
80
+ def build_resource_index
81
+ (@rules + @data_elements + @extensions + @rule_components).each_with_object({}) do |resource, index|
82
+ index[resource.id] ||= resource
83
+ end
84
+ end
85
+
86
+ def build_data_element_name_index
87
+ @data_elements.each_with_object({}) do |data_element, index|
88
+ next unless data_element.respond_to?(:name)
89
+ next if data_element.name.nil?
90
+
91
+ index[data_element.name] = data_element
92
+ end
93
+ end
94
+
95
+ def build_data_element_dependency_graph
96
+ @data_elements.to_h do |data_element|
97
+ names = ReactorSDK::ReferenceExtractor.extract_data_element_names(data_element)
98
+ [data_element.id, resolve_data_element_names(names, excluding: data_element.id)]
99
+ end
100
+ end
101
+
102
+ def build_rule_dependency_graph
103
+ @rules.to_h do |rule|
104
+ component_names = rule_components_for_rule(rule.id).flat_map do |component|
105
+ ReactorSDK::ReferenceExtractor.extract_data_element_names(component)
106
+ end
107
+ [rule.id, resolve_data_element_names(component_names)]
108
+ end
109
+ end
110
+
111
+ def invert_graph(graph)
112
+ graph.each_with_object(new_set_index) do |(from_id, to_ids), inverse|
113
+ Array(to_ids).each { |to_id| inverse[to_id] << from_id }
114
+ end.transform_values(&:to_a)
115
+ end
116
+
117
+ def referenced_resource_ids(data_element_or_id)
118
+ data_element_id = extract_id(data_element_or_id)
119
+ Array(@data_element_dependency_graph[data_element_id])
120
+ end
121
+
122
+ def impacted_rule_ids_for(data_element_or_id)
123
+ related_data_element_ids = transitive_data_element_ids(extract_id(data_element_or_id))
124
+
125
+ related_data_element_ids.each_with_object(Set.new) do |data_element_id, impacted_rule_ids|
126
+ Array(@rules_by_data_element[data_element_id]).each { |rule_id| impacted_rule_ids << rule_id }
127
+ end.to_a
128
+ end
129
+
130
+ def transitive_data_element_ids(data_element_id)
131
+ related_data_element_ids = Set[data_element_id]
132
+ queue = [data_element_id]
133
+
134
+ until queue.empty?
135
+ current = queue.shift
136
+
137
+ Array(@reverse_data_element_graph[current]).each do |dependent_id|
138
+ next if related_data_element_ids.include?(dependent_id)
139
+
140
+ related_data_element_ids << dependent_id
141
+ queue << dependent_id
142
+ end
143
+ end
144
+
145
+ related_data_element_ids
146
+ end
147
+
148
+ def new_set_index
149
+ Hash.new { |hash, key| hash[key] = Set.new }
150
+ end
151
+
152
+ def resolve_data_element_names(names, excluding: nil)
153
+ names.each_with_object(Set.new) do |name, resolved_ids|
154
+ target = @data_elements_by_name[name]
155
+ next if target.nil?
156
+ next if target.id == excluding
157
+
158
+ resolved_ids << target.id
159
+ end.to_a.sort
160
+ end
161
+
162
+ def extract_id(resource_or_id)
163
+ return resource_or_id.id if resource_or_id.respond_to?(:id)
164
+
165
+ resource_or_id
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # @file resources/library_with_resources.rb
5
+ # @description A richer Library resource returned by Libraries#find_with_resources.
6
+ #
7
+ # When fetching GET /libraries/:id?include=rules,data_elements,extensions
8
+ # Adobe returns the library alongside all its associated resources in the
9
+ # JSON:API included array. Each included resource carries a relationships
10
+ # hash containing its current revision ID.
11
+ #
12
+ # This class wraps that response and exposes:
13
+ # - All standard Library attributes (name, state, etc.)
14
+ # - rules — Array of Rule resources with revision_id attached
15
+ # - data_elements — Array of DataElement resources with revision_id attached
16
+ # - extensions — Array of Extension resources with revision_id attached
17
+ #
18
+ # The revision_id on each resource is the key the app uses for upstream
19
+ # resolution — when a resource does not exist in the target library, the
20
+ # app walks upstream (Development → Staging → Production) to find the
21
+ # nearest version and fetches that revision for comparison.
22
+ #
23
+ # This class is never instantiated directly — always created via
24
+ # Libraries#find_with_resources which passes the full API response.
25
+ #
26
+ # @domain Resources
27
+ # @see https://developer.adobe.com/experience-platform/documentation/tags/api/endpoints/libraries/
28
+ #
29
+
30
+ module ReactorSDK
31
+ module Resources
32
+ class LibraryWithResources < BaseResource
33
+ # @return [String] Display name of the library
34
+ attribute :name
35
+
36
+ # @return [String] Current workflow state
37
+ # One of: "development", "submitted", "approved", "rejected", "published"
38
+ attribute :state
39
+
40
+ # @return [Boolean] Whether the library has been published
41
+ attribute :published, as: :boolean
42
+
43
+ # @return [String] ISO8601 timestamp when the library was created
44
+ attribute :created_at
45
+
46
+ # @return [String] ISO8601 timestamp when the library was last updated
47
+ attribute :updated_at
48
+
49
+ # @return [String, nil] ISO8601 timestamp when the library was published
50
+ attribute :published_at
51
+
52
+ # @return [Array<ReactorSDK::Resources::Rule>] Rules in this library
53
+ # Each rule has a revision_id attribute attached from the relationship data
54
+ attr_reader :rules
55
+
56
+ # @return [Array<ReactorSDK::Resources::DataElement>] Data elements in this library
57
+ # Each data element has a revision_id attribute attached
58
+ attr_reader :data_elements
59
+
60
+ # @return [Array<ReactorSDK::Resources::Extension>] Extensions in this library
61
+ # Each extension has a revision_id attribute attached
62
+ attr_reader :extensions
63
+
64
+ ##
65
+ # Initializes the library with its included resources.
66
+ #
67
+ # Accepts the standard BaseResource arguments plus an included_resources
68
+ # hash that maps resource type to arrays of raw JSON:API resource hashes
69
+ # extracted from the API response included array.
70
+ #
71
+ # @param id [String] Adobe library ID
72
+ # @param type [String] JSON:API type ("libraries")
73
+ # @param attributes [Hash] Library attribute values
74
+ # @param meta [Hash] Optional metadata
75
+ # @param included_resources [Hash] Keyed by type — raw included resource arrays
76
+ # {
77
+ # "rules" => [ { "id" => "RL123", "type" => "rules",
78
+ # "attributes" => {...},
79
+ # "relationships" => { "latest_revision" => { "data" => { "id" => "RE123" } } } } ],
80
+ # "data_elements" => [ ... ],
81
+ # "extensions" => [ ... ]
82
+ # }
83
+ #
84
+ def initialize(
85
+ id:,
86
+ type:,
87
+ attributes: {},
88
+ meta: {},
89
+ included_resources: {}
90
+ )
91
+ super(id: id, type: type, attributes: attributes, meta: meta)
92
+ @rules = build_resources(included_resources['rules'], Resources::Rule)
93
+ @data_elements = build_resources(included_resources['data_elements'], Resources::DataElement)
94
+ @extensions = build_resources(included_resources['extensions'], Resources::Extension)
95
+ end
96
+
97
+ ##
98
+ # Returns true if the library is in a state where it can be built.
99
+ #
100
+ # @return [Boolean]
101
+ #
102
+ def buildable?
103
+ state == 'development'
104
+ end
105
+
106
+ ##
107
+ # Returns true if the library has been successfully published.
108
+ #
109
+ # @return [Boolean]
110
+ #
111
+ def published?
112
+ state == 'published'
113
+ end
114
+
115
+ ##
116
+ # Returns a flat index of all resources keyed by Adobe resource ID.
117
+ # Maps each resource ID to its current revision ID.
118
+ # Covers rules, data elements, and extensions in a single lookup.
119
+ #
120
+ # Used by the app to compare two libraries — call resource_index on
121
+ # both the source and target library, then compare revision IDs to
122
+ # find what changed, what was added, and what needs upstream resolution.
123
+ #
124
+ # @return [Hash] { "RL123" => "RE456", "DE789" => "RE012", ... }
125
+ #
126
+ def resource_index
127
+ (@rules + @data_elements + @extensions).each_with_object({}) do |resource, index|
128
+ index[resource.id] = resource.revision_id if resource.revision_id
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Returns all included resources as a flat array regardless of type.
134
+ #
135
+ # @return [Array<BaseResource>]
136
+ #
137
+ def all_resources
138
+ @rules + @data_elements + @extensions
139
+ end
140
+
141
+ ##
142
+ # @return [String] Human-readable representation
143
+ #
144
+ def inspect
145
+ '#<ReactorSDK::Resources::LibraryWithResources ' \
146
+ "id=#{id.inspect} " \
147
+ "name=#{name.inspect} " \
148
+ "state=#{state.inspect} " \
149
+ "rules=#{@rules.length} " \
150
+ "data_elements=#{@data_elements.length} " \
151
+ "extensions=#{@extensions.length}>"
152
+ end
153
+
154
+ private
155
+
156
+ ##
157
+ # Builds an array of typed resource objects from raw included resource hashes.
158
+ # Attaches revision_id to each resource extracted from its relationships.
159
+ #
160
+ # @param raw_resources [Array<Hash>, nil] Raw JSON:API resource hashes
161
+ # @param resource_class [Class] Resource class to instantiate
162
+ # @return [Array<BaseResource>] Typed resource objects with revision_id attached
163
+ #
164
+ def build_resources(raw_resources, resource_class)
165
+ Array(raw_resources).map do |raw|
166
+ resource = resource_class.new(
167
+ id: raw.fetch('id'),
168
+ type: raw.fetch('type'),
169
+ attributes: raw.fetch('attributes', {}),
170
+ meta: raw.fetch('meta', {}),
171
+ relationships: raw.fetch('relationships', {})
172
+ )
173
+ resource.instance_variable_set(
174
+ :@revision_id,
175
+ extract_revision_id(raw)
176
+ )
177
+ resource.singleton_class.attr_reader :revision_id
178
+ resource
179
+ end
180
+ end
181
+
182
+ ##
183
+ # Extracts the latest revision ID from a resource's relationships hash.
184
+ # Adobe stores it under relationships.latest_revision.data.id.
185
+ #
186
+ # @param raw [Hash] Raw JSON:API resource hash
187
+ # @return [String, nil] Revision ID or nil if not present
188
+ #
189
+ def extract_revision_id(raw)
190
+ raw.dig('relationships', 'latest_revision', 'data', 'id')
191
+ end
192
+ end
193
+ end
194
+ end