maestrano-connector-rails 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +7 -20
  3. data/VERSION +1 -1
  4. data/app/controllers/maestrano/synchronizations_controller.rb +3 -3
  5. data/app/models/maestrano/connector/rails/concerns/complex_entity.rb +50 -22
  6. data/app/models/maestrano/connector/rails/concerns/connec_helper.rb +157 -19
  7. data/app/models/maestrano/connector/rails/concerns/entity.rb +63 -42
  8. data/app/models/maestrano/connector/rails/concerns/external.rb +4 -0
  9. data/app/models/maestrano/connector/rails/concerns/sub_entity_base.rb +18 -5
  10. data/app/models/maestrano/connector/rails/organization.rb +1 -1
  11. data/lib/generators/connector/templates/complex_entity_example/contact_and_lead.rb +5 -5
  12. data/lib/generators/connector/templates/entity.rb +4 -5
  13. data/lib/generators/connector/templates/external.rb +6 -0
  14. data/lib/generators/connector/templates/home_index.haml +7 -4
  15. data/maestrano-connector-rails.gemspec +7 -4
  16. data/release_notes.md +5 -0
  17. data/spec/factories.rb +2 -2
  18. data/spec/integration/complex_id_references_spec.rb +248 -0
  19. data/spec/integration/complex_naming_spec.rb +191 -0
  20. data/spec/integration/{integration_complex_spec.rb → complex_spec.rb} +9 -9
  21. data/spec/integration/connec_to_external_spec.rb +7 -3
  22. data/spec/integration/id_references_spec.rb +581 -0
  23. data/spec/integration/singleton_spec.rb +3 -3
  24. data/spec/models/complex_entity_spec.rb +42 -27
  25. data/spec/models/connec_helper_spec.rb +399 -31
  26. data/spec/models/entity_spec.rb +76 -21
  27. data/spec/models/external_spec.rb +4 -0
  28. data/spec/models/sub_entity_base_spec.rb +11 -4
  29. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dc59b725143370fc5d008c5ac038245a3e35616a
4
- data.tar.gz: 589185264707571da796f97d3672223109136a8f
3
+ metadata.gz: 01e80e8ec8f1cf42c1b4dc6e1c55939d010bc8e3
4
+ data.tar.gz: ad2dc078eb9f605c2bd0a236690801577f38a43b
5
5
  SHA512:
6
- metadata.gz: dac38cc67856ecd00562faa4e09882af3b10c59a17a575b69724f375b0aedc4efa2cef0912b30d9423d67d9207824457b6883e38f3436d916401dde42e76dac3
7
- data.tar.gz: a5a72a4b5a460d0401e0b060552451db1db5ef50c4871fee66a1bc9305e7e4352a0df8a83e08791bc639bf87fa57379d815ea5d37cd934a2059b9aa0e0a74c3e
6
+ metadata.gz: ef502e52b0d613ba02a37ec02db1c248142553c18178008a4cab46e472c86213eff01ba772db527b360b8f190a943df1e69b549a857a7480996db02b68bb7413
7
+ data.tar.gz: cc32cc071376de85c8e5269c1cbdd4dff33ca20049fedf8711bbd09c56f69b3f21a05d090206bb614a65b6e8a78fd067d6382ec46046a71e34e6727036cd20d9
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2016-07-05 12:03:48 +0100 using RuboCop version 0.41.1.
3
+ # on 2016-07-26 04:25:37 +0100 using RuboCop version 0.41.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -19,7 +19,7 @@ Lint/UnusedBlockArgument:
19
19
  Exclude:
20
20
  - 'app/jobs/maestrano/connector/rails/synchronization_job.rb'
21
21
 
22
- # Offense count: 14
22
+ # Offense count: 17
23
23
  # Cop supports --auto-correct.
24
24
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
25
25
  Lint/UnusedMethodArgument:
@@ -29,29 +29,23 @@ Lint/UnusedMethodArgument:
29
29
  - 'app/models/maestrano/connector/rails/concerns/entity.rb'
30
30
  - 'app/models/maestrano/connector/rails/concerns/external.rb'
31
31
 
32
- # Offense count: 21
32
+ # Offense count: 23
33
33
  Metrics/AbcSize:
34
34
  Max: 70
35
35
 
36
- # Offense count: 9
36
+ # Offense count: 10
37
37
  Metrics/CyclomaticComplexity:
38
38
  Max: 14
39
39
 
40
- # Offense count: 40
41
- # Configuration parameters: AllowHeredoc, AllowURI, URISchemes.
42
- # URISchemes: http, https
43
- Metrics/LineLength:
44
- Max: 311
45
-
46
- # Offense count: 16
40
+ # Offense count: 20
47
41
  # Configuration parameters: CountComments.
48
42
  Metrics/MethodLength:
49
43
  Max: 42
50
44
 
51
- # Offense count: 1
45
+ # Offense count: 2
52
46
  # Configuration parameters: CountComments.
53
47
  Metrics/ModuleLength:
54
- Max: 249
48
+ Max: 263
55
49
 
56
50
  # Offense count: 2
57
51
  # Configuration parameters: CountKeywordArgs.
@@ -136,13 +130,6 @@ Style/PredicateName:
136
130
  - 'app/helpers/maestrano/connector/rails/session_helper.rb'
137
131
  - 'app/models/maestrano/connector/rails/concerns/entity.rb'
138
132
 
139
- # Offense count: 1
140
- # Cop supports --auto-correct.
141
- # Configuration parameters: AllowMultipleReturnValues.
142
- Style/RedundantReturn:
143
- Exclude:
144
- - 'app/jobs/maestrano/connector/rails/push_to_connec_worker.rb'
145
-
146
133
  # Offense count: 1
147
134
  # Cop supports --auto-correct.
148
135
  Style/RedundantSelf:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.3
1
+ 1.3.0
@@ -2,7 +2,7 @@ class Maestrano::SynchronizationsController < Maestrano::Rails::WebHookControlle
2
2
  def show
3
3
  uid = params[:id]
4
4
  organization = Maestrano::Connector::Rails::Organization.find_by_uid(uid)
5
- return render json: {errors: {message: "Organization not found", code: 404}}, status: :not_found unless organization
5
+ return render json: {errors: [{message: "Organization not found", code: 404}]}, status: :not_found unless organization
6
6
 
7
7
  h = {
8
8
  group_id: organization.uid,
@@ -25,7 +25,7 @@ class Maestrano::SynchronizationsController < Maestrano::Rails::WebHookControlle
25
25
  uid = params[:group_id]
26
26
  opts = params[:opts] || {}
27
27
  organization = Maestrano::Connector::Rails::Organization.find_by_uid(uid)
28
- return render json: {errors: {message: "Organization not found", code: 404}}, status: :not_found unless organization
28
+ return render json: {errors: [{message: "Organization not found", code: 404}]}, status: :not_found unless organization
29
29
 
30
30
  Maestrano::Connector::Rails::SynchronizationJob.perform_later(organization, opts.with_indifferent_access)
31
31
  head :created
@@ -34,7 +34,7 @@ class Maestrano::SynchronizationsController < Maestrano::Rails::WebHookControlle
34
34
  def toggle_sync
35
35
  uid = params[:group_id]
36
36
  organization = Maestrano::Connector::Rails::Organization.find_by_uid(uid)
37
- return render json: {errors: {message: "Organization not found", code: 404}}, status: :not_found unless organization
37
+ return render json: {errors: [{message: "Organization not found", code: 404}]}, status: :not_found unless organization
38
38
 
39
39
  organization.toggle(:sync_enabled)
40
40
  organization.save
@@ -14,6 +14,19 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
14
14
  raise 'Not implemented'
15
15
  end
16
16
 
17
+ def formatted_external_entities_names
18
+ formatted_entities_names(external_entities_names)
19
+ end
20
+
21
+ def formatted_connec_entities_names
22
+ formatted_entities_names(connec_entities_names)
23
+ end
24
+
25
+ def formatted_entities_names(names)
26
+ return names.with_indifferent_access if names.is_a?(Hash)
27
+ names.index_by { |name| name }.with_indifferent_access
28
+ end
29
+
17
30
  def count_entities(entities)
18
31
  entities.values.map(&:size).max
19
32
  end
@@ -59,54 +72,54 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
59
72
  def get_connec_entities(last_synchronization_date = nil)
60
73
  entities = ActiveSupport::HashWithIndifferentAccess.new
61
74
 
62
- self.class.connec_entities_names.each do |connec_entity_name|
63
- sub_entity_instance = instantiate_sub_entity_instance(connec_entity_name)
75
+ self.class.formatted_connec_entities_names.each do |connec_entity_name, connec_class_name|
76
+ sub_entity_instance = instantiate_sub_entity_instance(connec_class_name)
64
77
  entities[connec_entity_name] = sub_entity_instance.get_connec_entities(last_synchronization_date)
65
78
  end
66
79
  entities
67
80
  end
68
81
 
69
- def get_external_entities_wrapper(last_synchronization_date = nil)
82
+ def get_external_entities_wrapper(last_synchronization_date = nil, entity_name = '')
70
83
  entities = ActiveSupport::HashWithIndifferentAccess.new
71
84
 
72
- self.class.external_entities_names.each do |external_entity_name|
73
- sub_entity_instance = instantiate_sub_entity_instance(external_entity_name)
74
- entities[external_entity_name] = sub_entity_instance.get_external_entities_wrapper(last_synchronization_date)
85
+ self.class.formatted_external_entities_names.each do |external_entity_name, external_class_name|
86
+ sub_entity_instance = instantiate_sub_entity_instance(external_class_name)
87
+ entities[external_entity_name] = sub_entity_instance.get_external_entities_wrapper(last_synchronization_date, external_entity_name)
75
88
  end
76
89
  entities
77
90
  end
78
91
 
79
92
  def consolidate_and_map_data(connec_entities, external_entities)
80
- modeled_external_entities = external_model_to_connec_model(external_entities)
81
- modeled_connec_entities = connec_model_to_external_model(connec_entities)
93
+ modelled_external_entities = external_model_to_connec_model(external_entities)
94
+ modelled_connec_entities = connec_model_to_external_model(connec_entities)
82
95
 
83
- mapped_connec_entities = consolidate_and_map_connec_entities(modeled_connec_entities, modeled_external_entities)
84
- mapped_external_entities = consolidate_and_map_external_entities(modeled_external_entities)
96
+ mapped_connec_entities = consolidate_and_map_connec_entities(modelled_connec_entities, modelled_external_entities)
97
+ mapped_external_entities = consolidate_and_map_external_entities(modelled_external_entities)
85
98
 
86
99
  {connec_entities: mapped_connec_entities, external_entities: mapped_external_entities}
87
100
  end
88
101
 
89
- def consolidate_and_map_connec_entities(modeled_connec_entities, modeled_external_entities)
90
- modeled_connec_entities.each do |connec_entity_name, entities_in_external_model|
102
+ def consolidate_and_map_connec_entities(modelled_connec_entities, modelled_external_entities)
103
+ modelled_connec_entities.each do |connec_entity_name, entities_in_external_model|
91
104
  entities_in_external_model.each do |external_entity_name, entities|
92
- sub_entity_instance = instantiate_sub_entity_instance(connec_entity_name)
93
- equivalent_external_entities = (modeled_external_entities[external_entity_name] && modeled_external_entities[external_entity_name][connec_entity_name]) || []
105
+ sub_entity_instance = instantiate_sub_entity_instance(self.class.formatted_connec_entities_names[connec_entity_name])
106
+ equivalent_external_entities = (modelled_external_entities[external_entity_name] && modelled_external_entities[external_entity_name][connec_entity_name]) || []
94
107
 
95
108
  entities_in_external_model[external_entity_name] = sub_entity_instance.consolidate_and_map_connec_entities(entities, equivalent_external_entities, sub_entity_instance.class.references[external_entity_name] || [], external_entity_name)
96
109
  end
97
110
  end
98
- modeled_connec_entities
111
+ modelled_connec_entities
99
112
  end
100
113
 
101
- def consolidate_and_map_external_entities(modeled_external_entities)
102
- modeled_external_entities.each do |external_entity_name, entities_in_connec_model|
114
+ def consolidate_and_map_external_entities(modelled_external_entities)
115
+ modelled_external_entities.each do |external_entity_name, entities_in_connec_model|
103
116
  entities_in_connec_model.each do |connec_entity_name, entities|
104
- sub_entity_instance = instantiate_sub_entity_instance(external_entity_name)
117
+ sub_entity_instance = instantiate_sub_entity_instance(self.class.formatted_external_entities_names[external_entity_name])
105
118
 
106
119
  entities_in_connec_model[connec_entity_name] = sub_entity_instance.consolidate_and_map_external_entities(entities, connec_entity_name)
107
120
  end
108
121
  end
109
- modeled_external_entities
122
+ modelled_external_entities
110
123
  end
111
124
 
112
125
  # input : {
@@ -120,7 +133,7 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
120
133
  # }
121
134
  def push_entities_to_connec(mapped_external_entities_with_idmaps)
122
135
  mapped_external_entities_with_idmaps.each do |external_entity_name, entities_in_connec_model|
123
- sub_entity_instance = instantiate_sub_entity_instance(external_entity_name)
136
+ sub_entity_instance = instantiate_sub_entity_instance(self.class.formatted_external_entities_names[external_entity_name])
124
137
  entities_in_connec_model.each do |connec_entity_name, mapped_entities_with_idmaps|
125
138
  sub_entity_instance.push_entities_to_connec_to(mapped_entities_with_idmaps, connec_entity_name)
126
139
  end
@@ -129,7 +142,7 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
129
142
 
130
143
  def push_entities_to_external(mapped_connec_entities_with_idmaps)
131
144
  mapped_connec_entities_with_idmaps.each do |connec_entity_name, entities_in_external_model|
132
- sub_entity_instance = instantiate_sub_entity_instance(connec_entity_name)
145
+ sub_entity_instance = instantiate_sub_entity_instance(self.class.formatted_connec_entities_names[connec_entity_name])
133
146
  entities_in_external_model.each do |external_entity_name, mapped_entities_with_idmaps|
134
147
  sub_entity_instance.push_entities_to_external_to(mapped_entities_with_idmaps, external_entity_name)
135
148
  end
@@ -137,7 +150,7 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
137
150
  end
138
151
 
139
152
  def instantiate_sub_entity_instance(entity_name)
140
- "Entities::SubEntities::#{entity_name.titleize.split.join}".constantize.new(@organization, @connec_client, @external_client, @opts)
153
+ self.class.instantiate_sub_entity_instance(entity_name, @organization, @connec_client, @external_client, @opts)
141
154
  end
142
155
 
143
156
  # -------------------------------------------------------------
@@ -154,5 +167,20 @@ module Maestrano::Connector::Rails::Concerns::ComplexEntity
154
167
  def build_hash_with_entities(entities_name, entity_name, proc, entities)
155
168
  Hash[*entities_name.collect { |name| proc.call(name) == entity_name ? [name, entities] : [name, []] }.flatten(1)]
156
169
  end
170
+
171
+ def instantiate_sub_entity_instance(entity_name, organization, connec_client, external_client, opts)
172
+ "Entities::SubEntities::#{entity_name.titleize.split.join}".constantize.new(organization, connec_client, external_client, opts)
173
+ end
174
+
175
+ def find_complex_entity_and_instantiate_external_sub_entity_instance(entity_name, organization, connec_client, external_client, opts)
176
+ Maestrano::Connector::Rails::External.entities_list.each do |entity_name_from_list|
177
+ clazz = "Entities::#{entity_name_from_list.singularize.titleize.split.join}".constantize
178
+ if clazz.methods.include?('external_entities_names'.to_sym)
179
+ formatted_names = clazz.formatted_external_entities_names
180
+ return instantiate_sub_entity_instance(formatted_names[entity_name], organization, connec_client, external_client, opts) if formatted_names[entity_name]
181
+ end
182
+ end
183
+ nil
184
+ end
157
185
  end
158
186
  end
@@ -17,6 +17,8 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
17
17
  client
18
18
  end
19
19
 
20
+ # Returns a string of the tenant's current connec version.
21
+ # Can use Gem::Version for version comparison
20
22
  def connec_version(organization)
21
23
  @@connec_version = Rails.cache.fetch("connec_version_#{organization.tenant}", namespace: 'maestrano', expires_in: 1.day) do
22
24
  response = get_client(organization).class.get("#{Maestrano[organization.tenant].param('connec.host')}/version")
@@ -26,32 +28,52 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
26
28
  @@connec_version
27
29
  end
28
30
 
29
- # Replace the ids arrays by the external id
30
- # If a reference has no id for this oauth_provider and oauth_uid but has one for connec, returns nil
31
+ # Replaces the arrays of id received from Connec! by the id of the external application
32
+ # Returns a hash {entity: {}, connec_id: '', id_refs_only_connec_entity: {}}
33
+ # If an array has no id for this oauth_provider and oauth_uid but has one for connec, it returns a nil entity (skip the record)
31
34
  def unfold_references(connec_entity, references, organization)
32
- unfolded_connec_entity = connec_entity.with_indifferent_access
35
+ references = format_references(references)
36
+ unfolded_connec_entity = connec_entity.deep_dup.with_indifferent_access
33
37
  not_nil = true
34
38
 
35
39
  # Id
36
40
  id_hash = unfolded_connec_entity['id'].find { |id| id['provider'] == organization.oauth_provider && id['realm'] == organization.oauth_uid }
37
- unfolded_connec_entity[:__connec_id] = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id']
41
+ connec_id = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id']
38
42
  unfolded_connec_entity['id'] = id_hash ? id_hash['id'] : nil
39
43
 
40
- # Other refs
41
- references.each do |reference|
42
- not_nil &&= unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization)
44
+ # Other references
45
+ # Record references are references to other records (organization_id, item_id, ...)
46
+ references[:record_references].each do |reference|
47
+ not_nil &= unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization)
43
48
  end
44
- not_nil ? unfolded_connec_entity : nil
49
+ # Id references are references to sub entities ids (invoice lines id, ...)
50
+ # We do not return nil if we're missing an id reference
51
+ references[:id_references].each do |reference|
52
+ unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization)
53
+ end
54
+ unfolded_connec_entity = not_nil ? unfolded_connec_entity : nil
55
+
56
+ # Filter the connec entity to keep only the id_references fields (in order to save some memory)
57
+ # Give an empty hash if there's nothing left
58
+ id_refs_only_connec_entity = filter_connec_entity_for_id_refs(connec_entity, references[:id_references])
59
+
60
+ {entity: unfolded_connec_entity, connec_id: connec_id, id_refs_only_connec_entity: id_refs_only_connec_entity}
45
61
  end
46
62
 
63
+ # Replaces ids from the external application by arrays containing them
47
64
  def fold_references(mapped_external_entity, references, organization)
65
+ references = format_references(references)
48
66
  mapped_external_entity = mapped_external_entity.with_indifferent_access
49
- (references + ['id']).each do |reference|
67
+
68
+ # Use both record_references and id_references + the id
69
+ (references.values.flatten + ['id']).each do |reference|
50
70
  fold_references_helper(mapped_external_entity, reference.split('/'), organization)
51
71
  end
72
+
52
73
  mapped_external_entity
53
74
  end
54
75
 
76
+ # Builds an id_hash from the id and organization
55
77
  def id_hash(id, organization)
56
78
  {
57
79
  id: id,
@@ -60,6 +82,7 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
60
82
  }
61
83
  end
62
84
 
85
+ # Recursive method for folding references
63
86
  def fold_references_helper(entity, array_of_refs, organization)
64
87
  ref = array_of_refs.shift
65
88
  field = entity[ref]
@@ -71,7 +94,7 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
71
94
  field.each do |f|
72
95
  fold_references_helper(f, array_of_refs.dup, organization)
73
96
  end
74
- when HashWithIndifferentAccess
97
+ when Hash
75
98
  fold_references_helper(entity[ref], array_of_refs, organization)
76
99
  else
77
100
  id = field
@@ -79,6 +102,7 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
79
102
  end
80
103
  end
81
104
 
105
+ # Recursive method for unfolding references
82
106
  def unfold_references_helper(entity, array_of_refs, organization)
83
107
  ref = array_of_refs.shift
84
108
  field = entity[ref]
@@ -93,23 +117,137 @@ module Maestrano::Connector::Rails::Concerns::ConnecHelper
93
117
  # We may enqueue a fetch on the endpoint of the missing association, followed by a re-fetch on this one.
94
118
  # However it's expected to be an edge case, so for now we rely on the fact that the webhooks should be relativly in order.
95
119
  # Worst case it'll be done on following sync
120
+ entity.delete(ref)
96
121
  return nil
97
122
  end
123
+ true
98
124
 
99
125
  # Follow embedment path
100
126
  else
101
- unless field.blank?
102
- case field
103
- when Array
104
- field.each do |f|
105
- unfold_references_helper(f, array_of_refs.dup, organization)
106
- end
107
- when HashWithIndifferentAccess
108
- unfold_references_helper(entity[ref], array_of_refs, organization)
127
+ return true if field.blank?
128
+ case field
129
+ when Array
130
+ bool = true
131
+ field.each do |f|
132
+ bool &= unfold_references_helper(f, array_of_refs.dup, organization)
133
+ end
134
+ bool
135
+ when Hash
136
+ unfold_references_helper(entity[ref], array_of_refs, organization)
137
+ end
138
+ end
139
+ end
140
+
141
+ # Transforms the references into an hash {record_references: [], id_references: []}
142
+ # References can either be an array (only record references), or a hash
143
+ def format_references(references)
144
+ return {record_references: references, id_references: []} if references.is_a?(Array)
145
+ references[:record_references] ||= []
146
+ references[:id_references] ||= []
147
+ references
148
+ end
149
+
150
+ # Returns the connec_entity without all the fields that are not id_references
151
+ def filter_connec_entity_for_id_refs(connec_entity, id_references)
152
+ return {} if id_references.empty?
153
+
154
+ entity = connec_entity.dup.with_indifferent_access
155
+ tree = build_id_references_tree(id_references)
156
+
157
+ filter_connec_entity_for_id_refs_helper(entity, tree)
158
+
159
+ # TODO, improve performance by returning an empty hash if all the id_references have their id in the connec hash
160
+ # We should still return all of them if at least one is missing as we are relying on the id
161
+ entity
162
+ end
163
+
164
+ # Recursive method for filtering connec entities
165
+ def filter_connec_entity_for_id_refs_helper(entity_hash, tree)
166
+ return if tree.empty?
167
+ entity_hash.slice!(*tree.keys)
168
+
169
+ tree.each do |key, children|
170
+ case entity_hash[key]
171
+ when Array
172
+ entity_hash[key].each do |hash|
173
+ filter_connec_entity_for_id_refs_helper(hash, children)
174
+ end
175
+ when Hash
176
+ filter_connec_entity_for_id_refs_helper(entity_hash[key], children)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Builds a tree from an array of id_references
182
+ # input: %w(lines/id lines/linked/id linked/id)
183
+ # output: {"lines"=>{"id"=>{}, "linked"=>{"id"=>{}}}, "linked"=>{"id"=>{}}}
184
+ def build_id_references_tree(id_references)
185
+ tree = {}
186
+
187
+ id_references.each do |id_reference|
188
+ array_of_refs = id_reference.split('/')
189
+
190
+ t = tree
191
+ array_of_refs.each do |ref|
192
+ t[ref] ||= {}
193
+ t = t[ref]
194
+ end
195
+ end
196
+
197
+ tree
198
+ end
199
+
200
+ # Merges the id arrays from two hashes while keeping only the id_references fields
201
+ def merge_id_hashes(dist, src, id_references)
202
+ dist = dist.with_indifferent_access
203
+ src = src.with_indifferent_access
204
+
205
+ id_references.each do |id_reference|
206
+ array_of_refs = id_reference.split('/')
207
+
208
+ merge_id_hashes_helper(dist, array_of_refs, src)
209
+ end
210
+
211
+ dist
212
+ end
213
+
214
+ # Recursive helper for merging id hashes
215
+ def merge_id_hashes_helper(hash, array_of_refs, src, path = [])
216
+ ref = array_of_refs.shift
217
+ field = hash[ref]
218
+
219
+ if array_of_refs.empty? && field
220
+ value = value_from_hash(src, path + [ref])
221
+ if value.is_a?(Array)
222
+ hash[ref] = (field + value).uniq
223
+ else
224
+ hash.delete(ref)
225
+ end
226
+ else
227
+ case field
228
+ when Array
229
+ field.each_with_index do |f, index|
230
+ merge_id_hashes_helper(f, array_of_refs.dup, src, path + [ref, index])
109
231
  end
232
+ when Hash
233
+ merge_id_hashes_helper(field, array_of_refs, src, path + [ref])
234
+ end
235
+ end
236
+ end
237
+
238
+ # Returns the value from a hash following the given path
239
+ # Path sould be an array like [:lines, 0, :id]
240
+ def value_from_hash(hash, path)
241
+ value = hash
242
+
243
+ begin
244
+ path.each do |p|
245
+ value = value[p]
110
246
  end
247
+ value
248
+ rescue NoMethodError
249
+ nil
111
250
  end
112
- true
113
251
  end
114
252
  end
115
253
  end