forest_liana 5.2.3 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4f5dd198db274bc38945cb0f30878d6636154086fbceed39b9f392c4cf2d9c7
4
- data.tar.gz: 07c78e436df2aa66b9dbeaa776ad9125d3ffc04f1b60531b96848a84a56dfda4
3
+ metadata.gz: e035848d2e7346973187338d78579301dcf062b2b6192ba383cd1f50dcccf584
4
+ data.tar.gz: 5f8a3ecdea7a8f4643922c27626397bfa3d497c14302f77f377153a22f064b0f
5
5
  SHA512:
6
- metadata.gz: 2f9023a07c00e560f87e04184b88d6c6294e076bf29900c03d07eaa5f564308843e17456b09469777209d953b695085ef5650dbcdea029c0c4f3c1ddbb9c1d32
7
- data.tar.gz: 0f1cdf525dedff5ec0d39f34952396263b3fe73ad25926eb47eef43641bd72835eb9ff87bde88d6864181cdab4fec7d654b5a4c8c9c1b9583ba34a9e262e419e
6
+ metadata.gz: a30b760625155ae65acf9c8d6941e1e8ddd408b98cd377fdd6f6385c943ceef74e72cf3318d1373f966f8eda48dd9c35c4198bdaeecba2c9c6ffd52f2285e465
7
+ data.tar.gz: 52b468bfea30b0c115c318ab60eefd2caf382c94df171ea53ec0e203d6869d823f0deb23b20f66939f2560a8611d55b2c6b64f1dcb0b5ffb3d461de3471db8a5
@@ -1,7 +1,89 @@
1
1
  module ForestLiana
2
2
  class ActionsController < ForestLiana::BaseController
3
+
3
4
  def values
4
5
  render serializer: nil, json: {}, status: :ok
5
6
  end
7
+
8
+ def get_collection(collection_name)
9
+ ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
10
+ end
11
+
12
+ def get_action(collection_name)
13
+ collection = get_collection(collection_name)
14
+ begin
15
+ collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
16
+ rescue => error
17
+ FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
18
+ nil
19
+ end
20
+ end
21
+
22
+ def get_record
23
+ model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collectionName])
24
+ redord_getter = ForestLiana::ResourceGetter.new(model, {:id => params[:recordIds][0]})
25
+ redord_getter.perform
26
+ redord_getter.record
27
+ end
28
+
29
+ def get_smart_action_load_ctx(fields)
30
+ fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.merge!(value: nil))}
31
+ {:record => get_record, :fields => fields}
32
+ end
33
+
34
+ def get_smart_action_change_ctx(fields)
35
+ fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.permit!.to_h)}
36
+ {:record => get_record, :fields => fields}
37
+ end
38
+
39
+ def handle_result(result, formatted_fields, action)
40
+ if result.nil? || !result.is_a?(Hash)
41
+ return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
42
+ end
43
+ is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
44
+ unless is_same_data_structure.perform
45
+ return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
46
+ end
47
+
48
+ # Apply result on fields (transform the object back to an array), preserve order.
49
+ fields = action.fields.map { |field| result[field[:field]] }
50
+
51
+ render serializer: nil, json: { fields: fields}, status: :ok
52
+ end
53
+
54
+ def load
55
+ action = get_action(params[:collectionName])
56
+
57
+ if !action
58
+ render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
59
+ else
60
+ # Transform fields from array to an object to ease usage in hook, adds null value.
61
+ context = get_smart_action_load_ctx(action.fields)
62
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
63
+
64
+ # Call the user-defined load hook.
65
+ result = action.hooks[:load].(context)
66
+
67
+ handle_result(result, formatted_fields, action)
68
+ end
69
+ end
70
+
71
+ def change
72
+ action = get_action(params[:collectionName])
73
+
74
+ if !action
75
+ render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
76
+ else
77
+ # Transform fields from array to an object to ease usage in hook.
78
+ context = get_smart_action_change_ctx(params[:fields])
79
+ formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
80
+
81
+ # Call the user-defined change hook.
82
+ field_name = params[:fields].select { |field| field[:value] != field[:previousValue] }[0][:field]
83
+ result = action.hooks[:change][field_name].(context)
84
+
85
+ handle_result(result, formatted_fields, action)
86
+ end
87
+ end
6
88
  end
7
89
  end
@@ -0,0 +1,44 @@
1
+ require 'set'
2
+
3
+ module ForestLiana
4
+ module IsSameDataStructureHelper
5
+ class Analyser
6
+ def initialize(object, other, deep = 0)
7
+ @object = object
8
+ @other = other
9
+ @deep = deep
10
+ end
11
+
12
+ def are_objects(object, other)
13
+ object && other && object.is_a?(Hash) && other.is_a?(Hash)
14
+ end
15
+
16
+ def check_keys(object, other, step = 0)
17
+ unless are_objects(object, other)
18
+ return false
19
+ end
20
+
21
+ object_keys = object.keys
22
+ other_keys = other.keys
23
+
24
+ if object_keys.length != other_keys.length
25
+ return false
26
+ end
27
+
28
+ object_keys_set = object_keys.to_set
29
+ other_keys.each { |key|
30
+ if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
31
+ return false
32
+ end
33
+ }
34
+
35
+ return true
36
+ end
37
+
38
+ def perform
39
+ check_keys(@object, @other)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
5
5
  extend ActiveModel::Naming
6
6
 
7
7
  attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
8
- :type, :download
8
+ :type, :download, :hooks
9
9
 
10
10
  def initialize(attributes = {})
11
11
  if attributes.key?(:global)
@@ -66,6 +66,7 @@ class ForestLiana::Model::Action
66
66
  @base_url ||= nil
67
67
  @type ||= "bulk"
68
68
  @download ||= false
69
+ @hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
69
70
  end
70
71
 
71
72
  def persisted?
@@ -39,6 +39,7 @@ module ForestLiana
39
39
  'redirect',
40
40
  'download',
41
41
  'fields',
42
+ 'hooks',
42
43
  ]
43
44
  KEYS_ACTION_FIELD = [
44
45
  'field',
@@ -12,11 +12,11 @@ module ForestLiana
12
12
  @collection = get_collection(@collection_name)
13
13
  @fields_to_serialize = get_fields_to_serialize
14
14
  @field_names_requested = field_names_requested
15
- get_segment()
16
- compute_includes()
15
+ get_segment
16
+ compute_includes
17
17
  @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection)
18
18
 
19
- prepare_query()
19
+ prepare_query
20
20
  end
21
21
 
22
22
  def self.get_ids_from_request(params)
@@ -57,4 +57,6 @@ ForestLiana::Engine.routes.draw do
57
57
 
58
58
  # Smart Actions forms value
59
59
  post 'actions/:action_name/values' => 'actions#values'
60
+ post 'actions/:action_name/hooks/load' => 'actions#load'
61
+ post 'actions/:action_name/hooks/change' => 'actions#change'
60
62
  end
@@ -38,6 +38,14 @@ module ForestLiana
38
38
 
39
39
  private
40
40
 
41
+ def get_collection(collection_name)
42
+ ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
43
+ end
44
+
45
+ def get_action(collection, action_name)
46
+ collection.actions.find {|action| action.name == action_name}
47
+ end
48
+
41
49
  def generate_apimap
42
50
  create_apimap
43
51
  require_lib_forest_liana
@@ -45,6 +53,17 @@ module ForestLiana
45
53
 
46
54
  if Rails.env.development?
47
55
  @collections_sent = ForestLiana.apimap.as_json
56
+
57
+ @collections_sent.each do |collection|
58
+ collection['actions'].each do |action|
59
+ c = get_collection(collection['name'])
60
+ a = get_action(c, action['name'])
61
+ load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
62
+ change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
63
+ action['hooks'] = {:load => load, :change => change}
64
+ end
65
+ end
66
+
48
67
  @meta_sent = ForestLiana.meta
49
68
  SchemaFileUpdater.new(SCHEMA_FILENAME, @collections_sent, @meta_sent).perform()
50
69
  else
@@ -50,6 +50,7 @@ module ForestLiana
50
50
  'redirect',
51
51
  'download',
52
52
  'fields',
53
+ 'hooks',
53
54
  ]
54
55
  KEYS_ACTION_FIELD = [
55
56
  'field',
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "5.2.3"
2
+ VERSION = "5.3.0"
3
3
  end
@@ -0,0 +1,87 @@
1
+ module ForestLiana
2
+ context 'IsSameDataStructure class' do
3
+ it 'should: be valid with simple data' do
4
+ object = {:a => 'a', :b => 'b'}
5
+ other = {:a => 'a', :b => 'b'}
6
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
7
+ expect(result).to be true
8
+ end
9
+
10
+ it 'should: be invalid with simple data' do
11
+ object = {:a => 'a', :b => 'b'}
12
+ other = {:a => 'a', :c => 'c'}
13
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
14
+ expect(result).to be false
15
+ end
16
+
17
+ it 'should: be invalid with not same hash' do
18
+ object = {:a => 'a', :b => 'b'}
19
+ other = {:a => 'a', :b => 'b', :c => 'c'}
20
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
21
+ expect(result).to be false
22
+ end
23
+
24
+ it 'should: be invalid with nil' do
25
+ object = nil
26
+ other = {:a => 'a', :b => 'b', :c => 'c'}
27
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
28
+ expect(result).to be false
29
+ end
30
+
31
+ it 'should: be invalid with not hash' do
32
+ object = nil
33
+ other = {:a => 'a', :b => 'b', :c => 'c'}
34
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
35
+ expect(result).to be false
36
+ end
37
+
38
+ it 'should: be invalid with integer' do
39
+ object = 1
40
+ other = {:a => 'a', :b => 'b', :c => 'c'}
41
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
42
+ expect(result).to be false
43
+ end
44
+
45
+ it 'should: be invalid with string' do
46
+ object = 'a'
47
+ other = {:a => 'a', :b => 'b', :c => 'c'}
48
+ result = IsSameDataStructureHelper::Analyser.new(object, other).perform
49
+ expect(result).to be false
50
+ end
51
+
52
+ it 'should: be valid with depth 1' do
53
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
54
+ other = {:a => {:c => 'c'}, :b => {:d => 'd'}}
55
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
56
+ expect(result).to be true
57
+ end
58
+
59
+ it 'should: be invalid with depth 1' do
60
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
61
+ other = {:a => {:c => 'c'}, :b => {:e => 'e'}}
62
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
63
+ expect(result).to be false
64
+ end
65
+
66
+ it 'should: be invalid with depth 1 and nil' do
67
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
68
+ other = {:a => {:c => 'c'}, :b => nil}
69
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
70
+ expect(result).to be false
71
+ end
72
+
73
+ it 'should: be invalid with depth 1 and integer' do
74
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
75
+ other = {:a => {:c => 'c'}, :b => 1}
76
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
77
+ expect(result).to be false
78
+ end
79
+
80
+ it 'should: be invalid with depth 1 and string' do
81
+ object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
82
+ other = {:a => {:c => 'c'}, :b => 'b'}
83
+ result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
84
+ expect(result).to be false
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,136 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'Requesting Actions routes', :type => :request do
4
+ before(:each) do
5
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
6
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
7
+ Island.create(name: 'Corsica')
8
+ end
9
+
10
+ after(:each) do
11
+ Island.destroy_all
12
+ end
13
+
14
+ describe 'call /values' do
15
+ it 'should respond 200' do
16
+ post '/forest/actions/foo/values', {}
17
+ expect(response.status).to eq(200)
18
+ expect(response.body).to be {}
19
+ end
20
+ end
21
+
22
+ describe 'hooks' do
23
+ foo = {
24
+ field: 'foo',
25
+ type: 'String',
26
+ default_value: nil,
27
+ enums: nil,
28
+ is_required: false,
29
+ reference: nil,
30
+ description: nil,
31
+ widget: nil,
32
+ }
33
+ action_definition = {
34
+ name: 'my_action',
35
+ fields: [foo],
36
+ hooks: {
37
+ :load => -> (context) {
38
+ context[:fields]
39
+ },
40
+ :change => {
41
+ 'foo' => -> (context) {
42
+ fields = context[:fields]
43
+ fields['foo'][:value] = 'baz'
44
+ return fields
45
+ }
46
+ }
47
+ }
48
+ }
49
+ fail_action_definition = {
50
+ name: 'fail_action',
51
+ fields: [foo],
52
+ hooks: {
53
+ :load => -> (context) {
54
+ 1
55
+ },
56
+ :change => {
57
+ 'foo' => -> (context) {
58
+ 1
59
+ }
60
+ }
61
+ }
62
+ }
63
+ cheat_action_definition = {
64
+ name: 'cheat_action',
65
+ fields: [foo],
66
+ hooks: {
67
+ :load => -> (context) {
68
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
69
+ context[:fields]
70
+ },
71
+ :change => {
72
+ 'foo' => -> (context) {
73
+ context[:fields]['baz'] = foo.clone.update({field: 'baz'})
74
+ context[:fields]
75
+ }
76
+ }
77
+ }
78
+ }
79
+ action = ForestLiana::Model::Action.new(action_definition)
80
+ fail_action = ForestLiana::Model::Action.new(fail_action_definition)
81
+ cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
82
+ island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
83
+ island.actions = [action, fail_action, cheat_action]
84
+
85
+ describe 'call /load' do
86
+ params = {recordIds: [1], collectionName: 'Island'}
87
+
88
+ it 'should respond 200' do
89
+ post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
90
+ expect(response.status).to eq(200)
91
+ expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
92
+ end
93
+
94
+ it 'should respond 500 with bad params' do
95
+ post '/forest/actions/my_action/hooks/load', {}
96
+ expect(response.status).to eq(500)
97
+ end
98
+
99
+ it 'should respond 500 with bad hook result type' do
100
+ post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
101
+ expect(response.status).to eq(500)
102
+ end
103
+
104
+ it 'should respond 500 with bad hook result data structure' do
105
+ post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
106
+ expect(response.status).to eq(500)
107
+ end
108
+ end
109
+
110
+ describe 'call /change' do
111
+ updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
112
+ params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island'}
113
+
114
+ it 'should respond 200' do
115
+ post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
116
+ expect(response.status).to eq(200)
117
+ expect(JSON.parse(response.body)).to eq({'fields' => [updated_foo.merge({:value => 'baz'}).stringify_keys]})
118
+ end
119
+
120
+ it 'should respond 500 with bad params' do
121
+ post '/forest/actions/my_action/hooks/change', {}
122
+ expect(response.status).to eq(500)
123
+ end
124
+
125
+ it 'should respond 500 with bad hook result type' do
126
+ post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
127
+ expect(response.status).to eq(500)
128
+ end
129
+
130
+ it 'should respond 500 with bad hook result data structure' do
131
+ post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
132
+ expect(response.status).to eq(500)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -75,7 +75,8 @@ module ForestLiana
75
75
  type: 'File',
76
76
  field: 'File'
77
77
  }],
78
- http_method: nil
78
+ http_method: nil,
79
+ hooks: nil,
79
80
  }
80
81
  }, {
81
82
  attributes: {
@@ -95,7 +96,8 @@ module ForestLiana
95
96
  download: nil,
96
97
  endpoint: nil,
97
98
  redirect: nil,
98
- 'http_method': nil
99
+ 'http_method': nil,
100
+ hooks: nil,
99
101
  }
100
102
  }]
101
103
  }
@@ -148,8 +150,8 @@ module ForestLiana
148
150
  end
149
151
 
150
152
  it 'should sort the included actions and segments objects attributes values' do
151
- expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download'])
152
- expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields'])
153
+ expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download', 'hooks'])
154
+ expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields', 'hooks'])
153
155
  expect(apimap_sorted['included'][2]['attributes'].keys).to eq(['name'])
154
156
  expect(apimap_sorted['included'][3]['attributes'].keys).to eq(['name'])
155
157
  end
@@ -9,7 +9,7 @@ module ForestLiana
9
9
 
10
10
  expect(collection.fields.map { |field| field[:field] }).to eq(
11
11
  ["id", "name", "created_at", "updated_at", "trees"]
12
- );
12
+ )
13
13
  end
14
14
  end
15
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.3
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-02 00:00:00.000000000 Z
11
+ date: 2020-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -211,6 +211,7 @@ files:
211
211
  - app/helpers/forest_liana/adapter_helper.rb
212
212
  - app/helpers/forest_liana/application_helper.rb
213
213
  - app/helpers/forest_liana/decoration_helper.rb
214
+ - app/helpers/forest_liana/is_same_data_structure_helper.rb
214
215
  - app/helpers/forest_liana/query_helper.rb
215
216
  - app/helpers/forest_liana/schema_helper.rb
216
217
  - app/models/forest_liana/model/action.rb
@@ -336,9 +337,11 @@ files:
336
337
  - spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb
337
338
  - spec/dummy/db/migrate/20190716135241_add_type_to_user.rb
338
339
  - spec/dummy/db/schema.rb
340
+ - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
339
341
  - spec/helpers/forest_liana/query_helper_spec.rb
340
342
  - spec/helpers/forest_liana/schema_helper_spec.rb
341
343
  - spec/rails_helper.rb
344
+ - spec/requests/actions_controller_spec.rb
342
345
  - spec/requests/resources_spec.rb
343
346
  - spec/services/forest_liana/apimap_sorter_spec.rb
344
347
  - spec/services/forest_liana/filters_parser_spec.rb
@@ -556,6 +559,7 @@ test_files:
556
559
  - spec/services/forest_liana/apimap_sorter_spec.rb
557
560
  - spec/services/forest_liana/filters_parser_spec.rb
558
561
  - spec/spec_helper.rb
562
+ - spec/requests/actions_controller_spec.rb
559
563
  - spec/requests/resources_spec.rb
560
564
  - spec/dummy/README.rdoc
561
565
  - spec/dummy/app/views/layouts/application.html.erb
@@ -598,5 +602,6 @@ test_files:
598
602
  - spec/dummy/config/initializers/backtrace_silencers.rb
599
603
  - spec/dummy/config/database.yml
600
604
  - spec/helpers/forest_liana/schema_helper_spec.rb
605
+ - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
601
606
  - spec/helpers/forest_liana/query_helper_spec.rb
602
607
  - spec/rails_helper.rb