maestrano-connector-rails 1.2.3 → 1.3.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 (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