extraction_metadata_changes 0.0.1a

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7e5c61dc16159a4a8c989eb63689b147af5c70c35a3a96bb15d0d320374d914
4
+ data.tar.gz: bcf65732c185b576ccc4d15c10e6ffba1d2edab4d58cfa368433ea884858d973
5
+ SHA512:
6
+ metadata.gz: 7d6f2b597f0a240f16e72eef7d16bb25fa8d37a7d894b1dcd12e16b23da55706c131ea900b215ec726fd30790b02cf3f41cf5ef99e76c0487c1ad9fe1d69bcff
7
+ data.tar.gz: 3670c2f5811142be4940514162cc9320aa2d2fd3a6e3379bba601fbc7bb712c383eb1af6d9e59bf4b8cb9b772de692a43b9ce6d31119f8643f1b7763bf0204f5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Eduardo Martín Rojo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,83 @@
1
+ # extraction-metadata-changes
2
+ Client interface tool that talks with the metadata service to store and apply all
3
+ metadata modifications in a single transaction.
4
+
5
+ # How to use it
6
+
7
+ Add this line to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "extraction_metadata_changes"
11
+ ```
12
+
13
+ # Getting started
14
+
15
+ ## Building a modifications object
16
+
17
+ To build a set of metadata modifications, first we need to build a FactChanges object:
18
+
19
+ ```ruby
20
+ > changes = FactChanges.new
21
+ ```
22
+
23
+ This object will store the modifications that we want to apply in a single transaction so
24
+ we can run different methods to create these modifications. The list of available
25
+ modifications is:
26
+
27
+ * Create/Delete assets
28
+
29
+ ```ruby
30
+ > changes.create_assets(["00000000-0000-0000-0000"])
31
+ > changes.create_assets(["?my_car", "?my_previous_car", "?your_car"])
32
+ > changes.delete_assets(["00000000-0000-0000-0000"])
33
+ ```
34
+
35
+ Created assets can be described by either an uuid or by using a *variable* notation. A variable is any string starting with '?' that will identify the created asset in subsequent modifications, so we can refer to it in a more meaningful way than with a uuid.
36
+
37
+ * Add/Remove properties
38
+
39
+ ```ruby
40
+ > changes.add("?my_car", "color", "Red")
41
+ > changes.remove_where("00000000-0000-0000-0000", "size", "Big")
42
+ ```
43
+
44
+ * Add/Remove relations
45
+
46
+ ``` ruby
47
+ > changes.add("?my_car", "quickerThan", "?your_car")
48
+ > changes.removeWhere("?my_car", "quickerThan", "?my_previous_car")
49
+ > changes.removeWhere("?my_previous_car", "quickerThan", "?my_car")
50
+ ```
51
+
52
+ * Create/Delete groups of assets
53
+
54
+ ```ruby
55
+ > changes.create_asset_groups(["?my_parking", "?a_parking_with_fees"])
56
+ > changes.delete_asset_groups(["00000000-0000-0000-0000"])
57
+ ```
58
+
59
+ * Add/Remove assets to/from groups
60
+
61
+ ```ruby
62
+ > changes.add_assets_to_group("?my_parking", ["?my_car"])
63
+ > changes.remove_assets_from_group("?a_parking_with_fees", ["?my_car"])
64
+ ```
65
+
66
+ * Specify errors that will avoid the transaction to apply
67
+
68
+ ```ruby
69
+ > changes.set_errors(["This set of modifications are wrong."])
70
+ ```
71
+
72
+ ## Applying the modifications
73
+
74
+ Once we have completed all changes we want to apply in a single transaction, we can run
75
+ the apply method:
76
+
77
+ ```ruby
78
+ > changes.apply(transaction_id)
79
+ ```
80
+
81
+ All modifications will be joined under a single transaction, so we need to provide a unique
82
+ identifier of this set of modifications. One way of doing this is keeping a separate table that will keep track of the transactions creation an using the id of this table as the transaction id for the metadata service. This transaction id will be the reference of the changes we have apply so we can roll it back later if we ever need it.
83
+ that we have apply.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Main module for the gem
4
+ module ExtractionMetadataChanges
5
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'google_hash'
5
+
6
+ module ExtractionMetadataChanges
7
+ # This class DisjointList allow us to establish a relation between a list and a one or
8
+ # more dependent lists where all lists behave as disjoint sets of elements where any
9
+ # element added to any of the list cannot exist in anyother list at the same time so all
10
+ # lists negate elements to each other.
11
+ #
12
+ class DisjointList
13
+ SEED_FOR_UNIQUE_IDS = Random.rand(1000)
14
+ MAX_DEEP_UNIQUE_ID = 3
15
+
16
+ include Enumerable
17
+
18
+ attr_accessor :location_for_unique_id
19
+ attr_accessor :disjoint_lists
20
+ attr_accessor :list
21
+ attr_reader :name
22
+
23
+ DISABLED_NAME = 'DISABLED'
24
+
25
+ def initialize(list)
26
+ @name = "object_id_#{object_id}"
27
+
28
+ # Replace with a normal hash if we want to stop using it
29
+ @location_for_unique_id = GoogleHashDenseLongToRuby.new
30
+
31
+ @list = []
32
+ @disjoint_lists = [self]
33
+ list.each { |o| add(o) }
34
+ end
35
+
36
+ def add_disjoint_list(disjoint_list)
37
+ disjoints = (disjoint_list.disjoint_lists - disjoint_lists)
38
+ disjoint_lists.concat(disjoints).uniq
39
+
40
+ disjoints.each do |disjoint|
41
+ _synchronize_with_list(disjoint)
42
+
43
+ disjoint.location_for_unique_id = location_for_unique_id
44
+ disjoint.disjoint_lists = disjoint_lists
45
+ end
46
+ end
47
+
48
+ def store_for(element)
49
+ _store_for(unique_id_for_element(element))
50
+ end
51
+
52
+ def _store_for(unique_id)
53
+ store_name = location_for_unique_id[unique_id]
54
+ return nil if store_name.nil? || store_name == DISABLED_NAME
55
+
56
+ @disjoint_lists.select { |l| l.name == store_name }.first
57
+ end
58
+
59
+ def enabled?(element)
60
+ !store_for(element).nil?
61
+ end
62
+
63
+ def disabled?(element)
64
+ disabled_key?(unique_id_for_element(element))
65
+ end
66
+
67
+ def disabled_key?(key)
68
+ @location_for_unique_id[key] == DISABLED_NAME
69
+ end
70
+
71
+ def enabled_in_other_list?(element)
72
+ enabled?(element) && !include?(element)
73
+ end
74
+
75
+ def include?(element)
76
+ include_key?(unique_id_for_element(element))
77
+ end
78
+
79
+ def include_key?(key)
80
+ @location_for_unique_id[key] == name
81
+ end
82
+
83
+ def remove(element)
84
+ unique_id = unique_id_for_element(element)
85
+ remove_from_raw_list_by_id(unique_id)
86
+ @location_for_unique_id.delete(unique_id)
87
+ end
88
+
89
+ def remove_from_raw_list_by_id(unique_id)
90
+ @list.delete_if do |a|
91
+ unique_id_for_element(a) == unique_id
92
+ end
93
+ end
94
+
95
+ def length
96
+ @list.length
97
+ end
98
+
99
+ def each(&block)
100
+ @list.each(&block)
101
+ end
102
+
103
+ def [](index)
104
+ @list[index]
105
+ end
106
+
107
+ def flatten
108
+ @list.flatten
109
+ end
110
+
111
+ def uniq!
112
+ @list.uniq!
113
+ end
114
+
115
+ def <<(element)
116
+ if element.is_a?(Array)
117
+ element.each { |e| add(e) }
118
+ else
119
+ add(element)
120
+ end
121
+ end
122
+
123
+ def concat(element)
124
+ if element.is_a?(Array)
125
+ element.each { |e| add(e) }
126
+ else
127
+ add(element)
128
+ end
129
+ end
130
+
131
+ def push(element)
132
+ add(element)
133
+ end
134
+
135
+ def add(element)
136
+ return concat_disjoint_list(element) if element.is_a?(
137
+ ExtractionMetadataChanges::DisjointList
138
+ )
139
+
140
+ if enabled_in_other_list?(element)
141
+ disable(element)
142
+ elsif include?(element)
143
+ # It is already in our list, so we do not add it again
144
+ return false
145
+ else
146
+ enable(element)
147
+ end
148
+ self
149
+ end
150
+
151
+ def sum_function_for(value)
152
+ value.hash
153
+ # Value to create checksum and seed
154
+ # XXhash.xxh32(value, SEED_FOR_UNIQUE_IDS)
155
+ end
156
+
157
+ def unique_id_for_element(element)
158
+ _unique_id_for_element(element, 0)
159
+ end
160
+
161
+ def concat_disjoint_list(disjoint_list)
162
+ disjoint_list.location_for_unique_id.keys.each do |key|
163
+ _disable(key) if disjoint_list.disabled_key?(key)
164
+ end
165
+ disjoint_list.to_a.each { |val| add(val) }
166
+ self
167
+ end
168
+
169
+ def merge(disjoint_list)
170
+ disjoint_list.location_for_unique_id.keys.each do |key|
171
+ _disable(key) if !disjoint_list.include_key?(key) || disjoint_list.disabled_key?(key)
172
+ end
173
+ disjoint_list.to_a.each { |val| add(val) }
174
+ self
175
+ end
176
+
177
+ def enable(element)
178
+ return if disabled?(element)
179
+
180
+ unique_id = unique_id_for_element(element)
181
+ # Is not in any of the lists so we can add it
182
+ if element.is_a?(Enumerable) && !element.is_a?(Hash)
183
+ @list.concat(element)
184
+ else
185
+ @list.push(element)
186
+ end
187
+ @location_for_unique_id[unique_id] = name
188
+ end
189
+
190
+ def disable(element)
191
+ _disable(unique_id_for_element(element))
192
+ end
193
+
194
+ protected
195
+
196
+ def _synchronize_with_list(disjoint_list)
197
+ disjoint_list.location_for_unique_id.keys.each do |key|
198
+ unless location_for_unique_id[key] == DISABLED_NAME
199
+ # If my disjoint lists do not have the element
200
+ if location_for_unique_id[key].nil?
201
+ location_for_unique_id[key] = disjoint_list.location_for_unique_id[key]
202
+ _disable(key) if location_for_unique_id[key] == DISABLED_NAME
203
+ elsif location_for_unique_id[key] != disjoint_list.location_for_unique_id[key]
204
+ # If my lists have the element alredy
205
+ _disable(key)
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ def _disable(unique_id)
212
+ store = _store_for(unique_id)
213
+ store&.remove_from_raw_list_by_id(unique_id)
214
+ location_for_unique_id[unique_id] = DISABLED_NAME
215
+ end
216
+
217
+ def _unique_id_for_element(element, deep = 0)
218
+ return sum_function_for(SecureRandom.uuid) if deep == MAX_DEEP_UNIQUE_ID
219
+
220
+ if element.is_a?(String)
221
+ sum_function_for(element)
222
+ elsif element.respond_to?(:uuid) && !element.uuid.nil?
223
+ sum_function_for(element.uuid)
224
+ elsif element.respond_to?(:id) && !element.id.nil?
225
+ sum_function_for("#{element.class}_#{element.id}")
226
+ elsif element.is_a?(Hash)
227
+ if element.key?(:uuid) && !element[:uuid].nil?
228
+ sum_function_for(element[:uuid])
229
+ elsif element.key?(:predicate)
230
+ _unique_id_for_fact(element)
231
+ else
232
+ sum_function_for(element.keys.dup.concat(element.values.map do |val|
233
+ _unique_id_for_element(val, deep + 1)
234
+ end).join(''))
235
+ end
236
+ elsif element.is_a?(Enumerable)
237
+ sum_function_for(element.map { |o| _unique_id_for_element(o, deep + 1) }.join(''))
238
+ else
239
+ sum_function_for(element.to_s)
240
+ end
241
+ end
242
+
243
+ def _unique_id_for_fact(element)
244
+ sum_function_for([
245
+ (element[:asset_id] || element[:asset].id || element[:asset].object_id),
246
+ element[:predicate],
247
+ (element[:object] || element[:object_asset_id] || element[:object_asset].id ||
248
+ element[:object_asset].object_id)
249
+ ].join('_'))
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,652 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'extraction_token_util'
5
+
6
+ module ExtractionMetadataChanges
7
+ #
8
+ # Any change of metadata is composed of small modifications related with each other
9
+ # that can be applied in a transactional way so they can be cancelled, rolledback, etc.
10
+ #
11
+ # *About DisjointList*
12
+ #
13
+ # To be able to merge different configurations of changes, we need a way to keep track
14
+ # on the properties and values that have been added and removed to keep the modifications
15
+ # consistent, to avoid performing the operation twice, or in a wrong way.
16
+ #
17
+ # For instance, in these operations:
18
+ #
19
+ # > changes.add('?my_car', 'color', 'Red')
20
+ # > changes.remove_where('?my_car', 'color', 'Red')
21
+ #
22
+ # This modifications have opposite meaning so they do not apply at all, but if we add this:
23
+ #
24
+ # > changes.add('?my_car', 'color', 'Bright')
25
+ #
26
+ # It can apply because it refers to a different value. All this logic about when a property
27
+ # should not be part of the final transaction is performed using the DisjointList class,
28
+ # by establishing a disjoint relations between opposite lists (added properties,
29
+ # removed properties, etc...)
30
+ #
31
+ #
32
+ class FactChanges
33
+ attr_accessor :facts_to_destroy, :facts_to_add, :assets_to_create, :assets_to_destroy,
34
+ :assets_to_add, :assets_to_remove, :wildcards, :instances_from_uuid,
35
+ :asset_groups_to_create, :asset_groups_to_destroy, :errors_added,
36
+ :already_added_to_list, :instances_by_unique_id,
37
+ :facts_to_set_to_remote
38
+
39
+ attr_accessor :operations
40
+
41
+ def initialize(json = nil)
42
+ @assets_updated = []
43
+ reset
44
+ parse_json(json) if json
45
+ end
46
+
47
+ def parsing_valid?
48
+ @parsing_valid
49
+ end
50
+
51
+ def reset
52
+ @parsing_valid = false
53
+ @errors_added = []
54
+
55
+ @facts_to_set_to_remote = []
56
+
57
+ build_disjoint_lists(:facts_to_add, :facts_to_destroy)
58
+ build_disjoint_lists(:assets_to_create, :assets_to_destroy)
59
+ build_disjoint_lists(:asset_groups_to_create, :asset_groups_to_destroy)
60
+ build_disjoint_lists(:assets_to_add, :assets_to_remove)
61
+
62
+ @instances_from_uuid = GoogleHashDenseRubyToRuby.new
63
+ @wildcards = GoogleHashDenseRubyToRuby.new
64
+ end
65
+
66
+ def build_disjoint_lists(list, opposite)
67
+ list1 = ExtractionMetadataChanges::DisjointList.new([])
68
+ list2 = ExtractionMetadataChanges::DisjointList.new([])
69
+
70
+ list1.add_disjoint_list(list2)
71
+
72
+ send("#{list}=", list1)
73
+ send("#{opposite}=", list2)
74
+ end
75
+
76
+ def asset_group_asset_to_h(asset_group_asset_str)
77
+ obj = asset_group_asset_str.each_with_object({}) do |o, memo|
78
+ key = o[:asset_group]&.uuid || nil
79
+ memo[key] = [] unless memo[key]
80
+ memo[key].push(o[:asset].uuid)
81
+ end
82
+ obj.map do |k, v|
83
+ [k, v]
84
+ end
85
+ end
86
+
87
+ def to_h
88
+ {
89
+ 'set_errors': @errors_added,
90
+ 'create_assets': @assets_to_create.map(&:uuid),
91
+ 'create_asset_groups': @asset_groups_to_create.map(&:uuid),
92
+ 'delete_asset_groups': @asset_groups_to_destroy.map(&:uuid),
93
+ 'delete_assets': @assets_to_destroy.map(&:uuid),
94
+ 'add_facts': @facts_to_add.map do |f|
95
+ [
96
+ f[:asset].nil? ? nil : f[:asset].uuid,
97
+ f[:predicate],
98
+ (f[:object] || f[:object_asset].uuid)
99
+ ]
100
+ end,
101
+ 'remove_facts': @facts_to_destroy.map do |f|
102
+ if f[:id]
103
+ fact = Fact.find(f[:id])
104
+ [fact.asset.uuid, fact.predicate, fact.object_value_or_uuid]
105
+ else
106
+ [
107
+ f[:asset].nil? ? nil : f[:asset].uuid,
108
+ f[:predicate],
109
+ (f[:object] || f[:object_asset].uuid)
110
+ ]
111
+ end
112
+ end,
113
+ 'add_assets': asset_group_asset_to_h(@assets_to_add),
114
+ 'remove_assets': asset_group_asset_to_h(@assets_to_remove)
115
+ }.reject { |_k, v| v.empty? }
116
+ end
117
+
118
+ def to_json(*_args)
119
+ JSON.pretty_generate(to_h)
120
+ end
121
+
122
+ def parse_json(json)
123
+ obj = JSON.parse(json)
124
+ %w[set_errors create_assets create_asset_groups delete_asset_groups
125
+ remove_facts add_facts delete_assets add_assets remove_assets].each do |action_type|
126
+ send(action_type, obj[action_type]) if obj[action_type]
127
+ end
128
+ @parsing_valid = true
129
+ end
130
+
131
+ def values_for_predicate(asset, predicate)
132
+ actual_values = asset.facts.with_predicate(predicate).map(&:object)
133
+ values_to_add = facts_to_add.map do |f|
134
+ f[:object] if (f[:asset] == asset) && (f[:predicate] == predicate)
135
+ end.compact
136
+ values_to_destroy = facts_to_destroy.map do |f|
137
+ f[:object] if (f[:asset] == asset) && (f[:predicate] == predicate)
138
+ end.compact
139
+ (actual_values + values_to_add - values_to_destroy)
140
+ end
141
+
142
+ def _build_fact_attributes(asset, predicate, object, options = {})
143
+ t = [asset, predicate, object, options]
144
+ params = { asset: t[0], predicate: t[1], literal: !t[2].is_a?(Asset) }
145
+ params[:literal] ? params[:object] = t[2] : params[:object_asset] = t[2]
146
+ params = params.merge(t[3]) if t[3]
147
+ params
148
+ end
149
+
150
+ def add(asset, predicate, object, options = {})
151
+ asset = find_asset(asset)
152
+ object = options[:literal] == true ? literal_token(object) : find_asset(object)
153
+
154
+ fact = _build_fact_attributes(asset, predicate, object, options)
155
+
156
+ facts_to_add << fact if fact
157
+ end
158
+
159
+ def literal_token(str)
160
+ ExtractionTokenUtil.quote_if_uuid(str)
161
+ end
162
+
163
+ def add_facts(lists)
164
+ lists.each { |list| add(list[0], list[1], list[2]) }
165
+ self
166
+ end
167
+
168
+ def remove_facts(lists)
169
+ lists.each { |list| remove_where(list[0], list[1], list[2]) }
170
+ self
171
+ end
172
+
173
+ def add_remote(asset, predicate, object, options = {})
174
+ return unless asset && predicate && object
175
+
176
+ add(asset, predicate, object, options.merge({ is_remote?: true }))
177
+ end
178
+
179
+ def replace_remote_relation(asset, predicate, object_asset, options = {})
180
+ replace_remote(asset, predicate, object_asset, options.merge({ literal: false }))
181
+ end
182
+
183
+ def replace_remote_property(asset, predicate, value, options = {})
184
+ replace_remote(asset, predicate, value, options.merge({ literal: true }))
185
+ end
186
+
187
+ def replace_remote(asset, predicate, object, options = {})
188
+ return unless asset && predicate && object
189
+
190
+ asset.facts.with_predicate(predicate).each do |fact|
191
+ # The value is updated from the remote instance so we remove the previous value
192
+ remove(fact)
193
+ # In any case they will be set as Remote, even if they are not removed in this update
194
+ facts_to_set_to_remote << fact
195
+ end
196
+ add_remote(asset, predicate, object, options)
197
+ end
198
+
199
+ def remove(fact)
200
+ return if fact.nil?
201
+
202
+ if fact.is_a?(Enumerable)
203
+ facts_to_destroy << fact.map { |o| o.attributes.symbolize_keys }
204
+ elsif fact.is_a?(Fact)
205
+ facts_to_destroy << fact.attributes.symbolize_keys if fact
206
+ end
207
+ end
208
+
209
+ def remove_where(subject, predicate, object)
210
+ subject = find_asset(subject)
211
+ object = find_asset(object)
212
+
213
+ fact = _build_fact_attributes(subject, predicate, object)
214
+
215
+ facts_to_destroy << fact if fact
216
+ end
217
+
218
+ def errors?
219
+ to_h.key?(:set_errors)
220
+ end
221
+
222
+ def merge_hash(hash1, hash2)
223
+ hash2.keys.each do |k|
224
+ hash1[k] = hash2[k]
225
+ end
226
+ hash1
227
+ end
228
+
229
+ def merge(fact_changes)
230
+ if fact_changes
231
+ # To keep track of already added object after merging with another fact changes object
232
+ # _add_already_added_from_other_object(fact_changes)
233
+ errors_added.concat(fact_changes.errors_added)
234
+ asset_groups_to_create.concat(fact_changes.asset_groups_to_create)
235
+ assets_to_create.concat(fact_changes.assets_to_create)
236
+ facts_to_add.concat(fact_changes.facts_to_add)
237
+ assets_to_add.concat(fact_changes.assets_to_add)
238
+ assets_to_remove.concat(fact_changes.assets_to_remove)
239
+ facts_to_destroy.concat(fact_changes.facts_to_destroy)
240
+ assets_to_destroy.concat(fact_changes.assets_to_destroy)
241
+ asset_groups_to_destroy.concat(fact_changes.asset_groups_to_destroy)
242
+ merge_hash(instances_from_uuid, fact_changes.instances_from_uuid)
243
+ merge_hash(wildcards, fact_changes.wildcards)
244
+ end
245
+ self
246
+ end
247
+
248
+ def apply(step, with_operations = true)
249
+ _handle_errors(step) unless errors_added.empty?
250
+ ActiveRecord::Base.transaction do |_t|
251
+ # We need step to have an allocated id to be able to link it with the operations
252
+ # so we have to create a new record if is not already stored
253
+ step.save unless step.persisted?
254
+
255
+ # Callbacks execution
256
+ _on_apply(step) if respond_to?(:_on_apply)
257
+
258
+ _set_remote_facts(facts_to_set_to_remote)
259
+
260
+ # Creates the facts and generate from it the list of operations
261
+ operations = [
262
+ _create_asset_groups(step, asset_groups_to_create, with_operations),
263
+ _create_assets(step, assets_to_create, with_operations),
264
+ _add_assets(step, assets_to_add, with_operations),
265
+ _remove_assets(step, assets_to_remove, with_operations),
266
+ _remove_facts(step, facts_to_destroy, with_operations),
267
+ _detach_assets(step, assets_to_destroy, with_operations),
268
+ _detach_asset_groups(step, asset_groups_to_destroy, with_operations),
269
+ _create_facts(step, facts_to_add, with_operations)
270
+ ].flatten.compact
271
+
272
+ # Writes all operations in a single call
273
+ unless operations.empty?
274
+ Operation.import(operations)
275
+ @operations = operations
276
+ end
277
+ step.save if step.changed?
278
+ _handle_errors(step) unless errors_added.empty?
279
+ reset
280
+ end
281
+ end
282
+
283
+ def assets_updated
284
+ return [] unless @operations
285
+
286
+ @assets_updated = Asset.where(id: @operations.pluck(:asset_id).uniq).distinct
287
+ end
288
+
289
+ def assets_for_printing
290
+ return [] unless @operations
291
+
292
+ asset_ids = @operations.select do |operation|
293
+ (operation.action_type == 'createAssets')
294
+ end.pluck(:object).uniq
295
+
296
+ ready_for_print_ids = @operations.select do |operation|
297
+ ((operation.action_type == 'addFacts') &&
298
+ (operation.predicate == 'is') &&
299
+ (operation.object == 'readyForPrint'))
300
+ end.map(&:asset).compact.uniq.map(&:uuid)
301
+
302
+ ids_for_print = asset_ids.concat(ready_for_print_ids).flatten.uniq
303
+ @assets_for_printing = Asset.for_printing.where(uuid: ids_for_print)
304
+ end
305
+
306
+ def find_asset(asset_or_uuid)
307
+ find_instance_of_class_by_uuid(Asset, asset_or_uuid)
308
+ end
309
+
310
+ def find_asset_group(asset_group_or_id)
311
+ find_instance_of_class_by_uuid(AssetGroup, asset_group_or_id)
312
+ end
313
+
314
+ def find_assets(assets_or_uuids)
315
+ assets_or_uuids.uniq.map do |asset_or_uuid|
316
+ find_instance_of_class_by_uuid(Asset, asset_or_uuid)
317
+ end
318
+ end
319
+
320
+ def build_assets(assets)
321
+ assets.uniq.map do |asset_or_uuid|
322
+ find_instance_of_class_by_uuid(Asset, asset_or_uuid, true)
323
+ end
324
+ end
325
+
326
+ def find_asset_groups(asset_groups_or_uuids)
327
+ asset_groups_or_uuids.uniq.map do |asset_group_or_uuid|
328
+ find_instance_of_class_by_uuid(AssetGroup, asset_group_or_uuid)
329
+ end
330
+ end
331
+
332
+ def build_asset_groups(asset_groups)
333
+ asset_groups.uniq.map do |asset_group_or_uuid|
334
+ find_instance_of_class_by_uuid(AssetGroup, asset_group_or_uuid, true)
335
+ end
336
+ end
337
+
338
+ def new_record?(uuid)
339
+ (instances_from_uuid[uuid] && instances_from_uuid[uuid].new_record?) == true
340
+ end
341
+
342
+ def find_instance_of_class_by_uuid(klass, instance_or_uuid_or_id, create = false)
343
+ if ExtractionTokenUtil.wildcard?(instance_or_uuid_or_id)
344
+ uuid = uuid_for_wildcard(instance_or_uuid_or_id)
345
+ # Do not try to find it if it is a new wildcard created
346
+ found = find_instance_from_uuid(klass, uuid) unless create
347
+ found = ((instances_from_uuid[uuid] ||= klass.new(uuid: uuid))) if !found && create
348
+ elsif ExtractionTokenUtil.uuid?(instance_or_uuid_or_id)
349
+ found = find_instance_from_uuid(klass, instance_or_uuid_or_id)
350
+ if !found && create
351
+ found = ((instances_from_uuid[instance_or_uuid_or_id] ||= klass.new(
352
+ uuid: instance_or_uuid_or_id
353
+ )))
354
+ end
355
+ else
356
+ found = instance_or_uuid_or_id
357
+ end
358
+ unless found
359
+ _produce_error([%(
360
+ Element identified by #{instance_or_uuid_or_id} should be declared before using it
361
+ )])
362
+ end
363
+ found
364
+ end
365
+
366
+ def uuid_for_wildcard(wildcard)
367
+ wildcards[wildcard] ||= SecureRandom.uuid
368
+ end
369
+
370
+ def wildcard_for_uuid(uuid)
371
+ wildcards.keys.select { |key| wildcards[key] == uuid }.first
372
+ end
373
+
374
+ def find_instance_from_uuid(klass, uuid)
375
+ found = klass.find_by(uuid: uuid) unless new_record?(uuid)
376
+ return found if found
377
+
378
+ instances_from_uuid[uuid]
379
+ end
380
+
381
+ def validate_instances(instances)
382
+ if instances.is_a?(Array)
383
+ instances.each { |a| raise StandardError, a if a.nil? }
384
+ else
385
+ raise StandardError, a if instances.nil?
386
+ end
387
+ instances
388
+ end
389
+
390
+ def set_errors(errors)
391
+ errors_added.concat(errors)
392
+ self
393
+ end
394
+
395
+ def create_assets(assets)
396
+ assets_to_create << validate_instances(build_assets(assets))
397
+ # assets_to_create.concat(validate_instances(build_assets(assets)))
398
+ self
399
+ end
400
+
401
+ def create_asset_groups(asset_groups)
402
+ asset_groups_to_create << validate_instances(build_asset_groups(asset_groups))
403
+ self
404
+ end
405
+
406
+ def delete_asset_groups(asset_groups)
407
+ asset_groups_to_destroy << validate_instances(find_asset_groups(asset_groups))
408
+ self
409
+ end
410
+
411
+ def delete_assets(assets)
412
+ assets_to_destroy << validate_instances(find_assets(assets))
413
+ self
414
+ end
415
+
416
+ def add_assets_to_group(group, assets)
417
+ add_assets([[group, assets]])
418
+ end
419
+
420
+ def remove_assets_from_group(group, assets)
421
+ remove_assets([[group, assets]])
422
+ end
423
+
424
+ def add_assets(list)
425
+ list.each do |elem|
426
+ if !elem.empty? && elem[1].is_a?(Array)
427
+ asset_group = elem[0].nil? ? nil : validate_instances(find_asset_group(elem[0]))
428
+ asset_ids = elem[1]
429
+ else
430
+ asset_group = nil
431
+ asset_ids = elem
432
+ end
433
+ assets = validate_instances(find_assets(asset_ids))
434
+ assets_to_add << assets.map { |asset| { asset_group: asset_group, asset: asset } }
435
+ end
436
+ self
437
+ end
438
+
439
+ def remove_assets(list)
440
+ list.each do |elem|
441
+ if !elem.empty? && elem[1].is_a?(Array)
442
+ asset_group = elem[0].nil? ? nil : validate_instances(find_asset_group(elem[0]))
443
+ asset_ids = elem[1]
444
+ else
445
+ asset_group = nil
446
+ asset_ids = elem
447
+ end
448
+ assets = validate_instances(find_assets(asset_ids))
449
+ assets_to_remove << assets.map { |asset| { asset_group: asset_group, asset: asset } }
450
+ end
451
+ self
452
+ end
453
+
454
+ private
455
+
456
+ def _handle_errors(step)
457
+ step.set_errors(errors_added)
458
+ _produce_error(errors_added) unless errors_added.empty?
459
+ end
460
+
461
+ def _produce_error(errors_added)
462
+ raise StandardError.new(message: errors_added.join("\n"))
463
+ end
464
+
465
+ def _set_remote_facts(facts)
466
+ Fact.where(id: facts.map(&:id).uniq.compact).update_all(is_remote?: true)
467
+ end
468
+
469
+ def _add_assets(step, asset_group_assets, with_operations = true)
470
+ modified_list = asset_group_assets.map do |o|
471
+ # If is nil, it will use the asset group from the step
472
+ o[:asset_group] = o[:asset_group] || step.asset_group
473
+ o
474
+ end
475
+ _instance_builder_for_import(AssetGroupsAsset, modified_list) do |instances|
476
+ _asset_group_operations('addAssets', step, instances) if with_operations
477
+ end
478
+ end
479
+
480
+ def _remove_assets(step, assets_to_remove, with_operations = true)
481
+ modified_list = assets_to_remove.map do |obj|
482
+ AssetGroupsAsset.where(
483
+ asset_group: obj[:asset_group] || step.asset_group,
484
+ asset: obj[:asset]
485
+ )
486
+ end
487
+ _instances_deletion(AssetGroupsAsset, modified_list) do |asset_group_assets|
488
+ _asset_group_operations('removeAssets', step, asset_group_assets) if with_operations
489
+ end
490
+ end
491
+
492
+ def _create_assets(step, assets, with_operations = true)
493
+ return unless assets
494
+
495
+ count = Asset.count + 1
496
+ assets = assets.each_with_index.map do |asset, barcode_index|
497
+ _build_barcode(asset, count + barcode_index)
498
+ asset
499
+ end
500
+ _instance_builder_for_import(Asset, assets) do |_instances|
501
+ _asset_operations('createAssets', step, assets) if with_operations
502
+ end
503
+ end
504
+
505
+ ## TODO:
506
+ # Possibly it could be moved to Asset before_save callback
507
+ #
508
+ def _build_barcode(asset, num)
509
+ barcode_type = values_for_predicate(asset, 'barcodeType').first
510
+
511
+ return if barcode_type == 'NoBarcode'
512
+
513
+ barcode = values_for_predicate(asset, 'barcode').first
514
+ if barcode
515
+ asset.barcode = barcode
516
+ else
517
+ asset.build_barcode(num)
518
+ end
519
+ end
520
+
521
+ def _detach_assets(step, assets, with_operations = true)
522
+ operations = _asset_operations('deleteAssets', step, assets) if with_operations
523
+ _instances_deletion(Fact, assets.map(&:facts).flatten.compact)
524
+ _instances_deletion(AssetGroupsAsset, assets.map(&:asset_groups_assets).flatten.compact)
525
+ operations
526
+ end
527
+
528
+ def _create_asset_groups(step, asset_groups, with_operations = true)
529
+ return unless asset_groups
530
+
531
+ asset_groups.each_with_index do |asset_group, _index|
532
+ asset_group.update_attributes(
533
+ name: ExtractionTokenUtil.to_asset_group_name(wildcard_for_uuid(asset_group.uuid)),
534
+ activity_owner: step.activity
535
+ )
536
+ asset_group.save
537
+ end
538
+ _asset_group_building_operations('createAssetGroups', step, asset_groups) if with_operations
539
+ end
540
+
541
+ def _detach_asset_groups(step, asset_groups, with_operations = true)
542
+ if with_operations
543
+ operations = _asset_group_building_operations('deleteAssetGroups', step, asset_groups)
544
+ end
545
+ instances = asset_groups.flatten
546
+ ids_to_remove = instances.map(&:id).compact.uniq
547
+
548
+ if ids_to_remove && !ids_to_remove.empty?
549
+ AssetGroup.where(id: ids_to_remove).update_all(activity_owner_id: nil)
550
+ end
551
+ operations
552
+ end
553
+
554
+ def _create_facts(step, params_for_facts, with_operations = true)
555
+ _instance_builder_for_import(Fact, params_for_facts) do |facts|
556
+ _fact_operations('addFacts', step, facts) if with_operations
557
+ end
558
+ end
559
+
560
+ def _remove_facts(step, facts_to_remove, with_operations = true)
561
+ ids = []
562
+ modified_list = facts_to_remove.each_with_object([]) do |data, memo|
563
+ if data[:id]
564
+ ids.push(data[:id])
565
+ elsif data[:object].is_a? String
566
+ elems = Fact.where(asset: data[:asset], predicate: data[:predicate],
567
+ object: data[:object])
568
+ else
569
+ elems = Fact.where(asset: data[:asset], predicate: data[:predicate],
570
+ object_asset: data[:object_asset])
571
+ end
572
+ memo.concat(elems) if elems
573
+ end.concat(Fact.where(id: ids))
574
+ _instances_deletion(Fact, modified_list) do
575
+ _fact_operations('removeFacts', step, modified_list) if with_operations
576
+ end
577
+ end
578
+
579
+ def _asset_group_building_operations(action_type, step, asset_groups)
580
+ asset_groups.map do |asset_group|
581
+ Operation.new(action_type: action_type, step: step, object: asset_group.uuid)
582
+ end
583
+ end
584
+
585
+ def _asset_group_operations(action_type, step, asset_group_assets)
586
+ asset_group_assets.map do |asset_group_asset, _index|
587
+ Operation.new(action_type: action_type, step: step,
588
+ asset: asset_group_asset.asset, object: asset_group_asset.asset_group.uuid)
589
+ end
590
+ end
591
+
592
+ def _asset_operations(action_type, step, assets)
593
+ assets.map do |asset, _index|
594
+ # refer = (action_type == 'deleteAsset' ? nil : asset)
595
+ Operation.new(action_type: action_type, step: step, object: asset.uuid)
596
+ end
597
+ end
598
+
599
+ def listening_to_predicate?(predicate)
600
+ predicate == 'parent'
601
+ end
602
+
603
+ def _fact_operations(action_type, step, facts)
604
+ modified_assets = []
605
+ operations = facts.map do |fact|
606
+ modified_assets.push(fact.object_asset) if listening_to_predicate?(fact.predicate)
607
+ Operation.new(action_type: action_type, step: step,
608
+ asset: fact.asset, predicate: fact.predicate, object: fact.object,
609
+ object_asset: fact.object_asset)
610
+ end
611
+ modified_assets.flatten.compact.uniq.each(&:touch)
612
+ operations
613
+ end
614
+
615
+ def all_values_are_new_records(hash)
616
+ hash.values.all? do |value|
617
+ (value.respond_to?(:new_record?) && value.new_record?)
618
+ end
619
+ end
620
+
621
+ def _instance_builder_for_import(klass, params_list)
622
+ instances = params_list.map do |params_for_instance|
623
+ if params_for_instance.is_a?(klass)
624
+ params_for_instance if params_for_instance.new_record?
625
+ elsif all_values_are_new_records(params_for_instance) ||
626
+ !klass.exists?(params_for_instance)
627
+ klass.new(params_for_instance)
628
+ end
629
+ end.compact.uniq
630
+ instances.each do |instance|
631
+ instance.run_callbacks(:save) { false }
632
+ instance.run_callbacks(:create) { false }
633
+ end
634
+ return unless instances && !instances.empty?
635
+
636
+ klass.import(instances)
637
+ # import does not return the ids for the instances, so we need to reload
638
+ # again. Uuid is the only identificable attribute set
639
+ klass.synchronize(instances, [:uuid]) if klass == Asset
640
+ yield instances
641
+ end
642
+
643
+ def _instances_deletion(klass, instances)
644
+ operations = block_given? ? yield(instances) : instances
645
+ instances = instances.flatten
646
+ ids_to_remove = instances.map(&:id).compact.uniq
647
+
648
+ klass.where(id: ids_to_remove).delete_all if ids_to_remove && !ids_to_remove.empty?
649
+ operations
650
+ end
651
+ end
652
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: extraction_metadata_changes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1a
5
+ platform: ruby
6
+ authors:
7
+ - Eduardo Martin Rojo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: extraction_token_util
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.3a11
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.3a11
27
+ - !ruby/object:Gem::Dependency
28
+ name: google_hash
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_bot
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.80'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.80'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.38'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.38'
97
+ description:
98
+ email:
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - LICENSE
104
+ - README.md
105
+ - lib/extraction_metadata_changes.rb
106
+ - lib/extraction_metadata_changes/disjoint_list.rb
107
+ - lib/extraction_metadata_changes/fact_changes.rb
108
+ homepage: https://rubygems.org/gems/extraction_metadata_changes
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">"
124
+ - !ruby/object:Gem::Version
125
+ version: 1.3.1
126
+ requirements: []
127
+ rubygems_version: 3.0.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Client interface tool that talks with the metadata service to store and apply
131
+ all metadata modifications in a single transaction
132
+ test_files: []