forest_liana 5.2.3 → 5.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.
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