forest_liana 9.20.9 → 9.20.10
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 +4 -4
- data/app/models/forest_liana/model/action.rb +25 -0
- data/config/routes/actions.rb +5 -2
- data/lib/forest_liana/engine.rb +26 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/controllers/host_catch_all_controller.rb +5 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/lib/forest_liana/collections/island.rb +14 -0
- data/spec/models/forest_liana/model/action_spec.rb +48 -0
- data/spec/requests/actions_controller_spec.rb +47 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5449af5d3c0aa87c4c56f8417d3527e279690d22a34f02cb081cee74d2de06b1
|
|
4
|
+
data.tar.gz: '044797af4de1b516a177d859566fdbdea4e5458eb938c107077d01a19314a763'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cce3c3edaacf3cea3dfc9ea29f3a7391cf2e1a09b399784c1be06c767d6013e600d70c7ca2fa70673451c0e18431df0d521e3cff2285408c003ef4911ca18336
|
|
7
|
+
data.tar.gz: b56dc37e18b8f8e78e730c1d8eb844731134d22be8478a27e4ea576ff75df9523f6c6ba7b7da8b07a585b97419d9b2d3058656527acad2dcb94146533e9afabd
|
|
@@ -81,4 +81,29 @@ class ForestLiana::Model::Action
|
|
|
81
81
|
def persisted?
|
|
82
82
|
false
|
|
83
83
|
end
|
|
84
|
+
|
|
85
|
+
# Normalized at the boundary so every consumer sees one canonical value: the schema sent to
|
|
86
|
+
# Forest (the UI appends /hooks/load to it), the permission lookup comparing request paths,
|
|
87
|
+
# and the routes drawn from it. Leading slash kept as declared — consumers tolerate both.
|
|
88
|
+
def endpoint=(value)
|
|
89
|
+
@endpoint = value && value.to_s.gsub(%r{/+}, '/').delete_suffix('/')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def endpoint_under_forest_mount?
|
|
93
|
+
normalized_endpoint.start_with?('forest/')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def engine_relative_endpoint
|
|
97
|
+
normalized_endpoint.delete_prefix('forest')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def absolute_endpoint
|
|
101
|
+
"/#{normalized_endpoint}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def normalized_endpoint
|
|
107
|
+
endpoint.to_s.delete_prefix('/')
|
|
108
|
+
end
|
|
84
109
|
end
|
data/config/routes/actions.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
ForestLiana.apimap.each do |collection|
|
|
2
2
|
if !collection.actions.empty?
|
|
3
3
|
collection.actions.each do |action|
|
|
4
|
+
# Endpoints outside the mount are routed on the host app instead — see ForestLiana::Engine.
|
|
5
|
+
next unless action.endpoint_under_forest_mount?
|
|
6
|
+
|
|
4
7
|
# Unconditional: clients probe /hooks/load even when no load hook is declared.
|
|
5
|
-
post action.
|
|
8
|
+
post action.engine_relative_endpoint + '/hooks/load' => 'actions#load', action_name: ActiveSupport::Inflector.parameterize(action.name)
|
|
6
9
|
if action.hooks && action.hooks[:change].present?
|
|
7
|
-
post action.
|
|
10
|
+
post action.engine_relative_endpoint + '/hooks/change' => 'actions#change', action_name: ActiveSupport::Inflector.parameterize(action.name)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
data/lib/forest_liana/engine.rb
CHANGED
|
@@ -127,6 +127,32 @@ module ForestLiana
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
|
+
|
|
131
|
+
ForestLiana::Engine.register_out_of_mount_hook_routes
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# An endpoint outside the engine mount can only be served by the host router: a route drawn in
|
|
135
|
+
# the engine gets prefixed with the mount, where no client ever calls. Prepended so a host
|
|
136
|
+
# catch-all (SPA fallback) cannot shadow the hooks, and registered from after_initialize rather
|
|
137
|
+
# than from config/routes/actions.rb, which is re-evaluated on every reload and would stack a
|
|
138
|
+
# duplicate block per pass — prepended blocks persist across reloads on their own. Defined here
|
|
139
|
+
# so the block's closure captures this empty scope, not after_initialize's locals: the block is
|
|
140
|
+
# retained by the router for the process lifetime and would otherwise pin the bootstrapper.
|
|
141
|
+
def self.register_out_of_mount_hook_routes
|
|
142
|
+
Rails.application.routes.prepend do
|
|
143
|
+
ForestLiana.apimap.each do |collection|
|
|
144
|
+
collection.actions.each do |action|
|
|
145
|
+
next if action.endpoint_under_forest_mount?
|
|
146
|
+
|
|
147
|
+
post "#{action.absolute_endpoint}/hooks/load" => 'forest_liana/actions#load',
|
|
148
|
+
action_name: ActiveSupport::Inflector.parameterize(action.name)
|
|
149
|
+
if action.hooks && action.hooks[:change].present?
|
|
150
|
+
post "#{action.absolute_endpoint}/hooks/change" => 'forest_liana/actions#change',
|
|
151
|
+
action_name: ActiveSupport::Inflector.parameterize(action.name)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
130
156
|
end
|
|
131
157
|
end
|
|
132
158
|
end
|
data/lib/forest_liana/version.rb
CHANGED
data/spec/dummy/config/routes.rb
CHANGED
|
@@ -180,6 +180,20 @@ class Forest::Island
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
action 'out_of_mount_action',
|
|
184
|
+
endpoint: '/api/custom/islands/out_of_mount_action',
|
|
185
|
+
fields: [foo],
|
|
186
|
+
hooks: {
|
|
187
|
+
:load => -> (context) {
|
|
188
|
+
context[:fields]
|
|
189
|
+
},
|
|
190
|
+
:change => {
|
|
191
|
+
'on_foo_changed' => -> (context) {
|
|
192
|
+
context[:fields]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
183
197
|
action 'multiple_enums_action',
|
|
184
198
|
fields: [foo, multiple_enum],
|
|
185
199
|
hooks: {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
describe Model::Action do
|
|
3
|
+
def action_with(endpoint)
|
|
4
|
+
Model::Action.new(name: 'an action', endpoint: endpoint)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
describe 'endpoint routing' do
|
|
8
|
+
it 'defaults to an endpoint under the mount, with or without a leading slash' do
|
|
9
|
+
action = Model::Action.new(name: 'an action')
|
|
10
|
+
|
|
11
|
+
expect(action.endpoint).to eq('forest/actions/an-action')
|
|
12
|
+
expect(action.endpoint_under_forest_mount?).to be true
|
|
13
|
+
expect(action.engine_relative_endpoint).to eq('/actions/an-action')
|
|
14
|
+
|
|
15
|
+
expect(action_with('/forest/actions/foo').endpoint_under_forest_mount?).to be true
|
|
16
|
+
expect(action_with('/forest/actions/foo').engine_relative_endpoint).to eq('/actions/foo')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'routes an endpoint outside the mount on the host, verbatim' do
|
|
20
|
+
action = action_with('/domains/organizations/suspensions/suspend')
|
|
21
|
+
|
|
22
|
+
expect(action.endpoint_under_forest_mount?).to be false
|
|
23
|
+
expect(action.absolute_endpoint).to eq('/domains/organizations/suspensions/suspend')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A trailing or doubled slash would otherwise desynchronize the three consumers of the
|
|
27
|
+
# declared endpoint: the drawn route, the schema the UI appends /hooks/load to, and the
|
|
28
|
+
# permission lookup comparing request paths.
|
|
29
|
+
it 'normalizes redundant slashes at assignment, so every consumer sees one value' do
|
|
30
|
+
expect(action_with('/domains/x/suspend/').endpoint).to eq('/domains/x/suspend')
|
|
31
|
+
expect(action_with('//domains/x/suspend').endpoint).to eq('/domains/x/suspend')
|
|
32
|
+
expect(action_with('/domains/x/suspend/').absolute_endpoint).to eq('/domains/x/suspend')
|
|
33
|
+
expect(action_with('domains/x/suspend').endpoint).to eq('domains/x/suspend')
|
|
34
|
+
expect(action_with('/forest/actions/foo/').engine_relative_endpoint).to eq('/actions/foo')
|
|
35
|
+
expect(action_with('//forest/actions/foo').endpoint_under_forest_mount?).to be true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `sub('forest', '')` used to match anywhere, mangling these into '/ry/actions/x' and '/my//x'.
|
|
39
|
+
it 'does not treat a path merely containing "forest" as being under the mount' do
|
|
40
|
+
expect(action_with('forestry/actions/x').endpoint_under_forest_mount?).to be false
|
|
41
|
+
expect(action_with('forestry/actions/x').absolute_endpoint).to eq('/forestry/actions/x')
|
|
42
|
+
|
|
43
|
+
expect(action_with('my/forest/x').endpoint_under_forest_mount?).to be false
|
|
44
|
+
expect(action_with('my/forest/x').absolute_endpoint).to eq('/my/forest/x')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -170,6 +170,53 @@ describe 'Requesting Actions routes', :type => :request do
|
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
describe 'on an endpoint declared outside the engine mount' do
|
|
174
|
+
params = {
|
|
175
|
+
data: {
|
|
176
|
+
attributes: { ids: [1], collection_name: 'Island' }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# The dummy app declares a `/api/*path` catch-all standing in for a host SPA fallback: these
|
|
181
|
+
# routes have to outrank it, or the 404 this fix removes comes straight back.
|
|
182
|
+
it 'should serve /load at the endpoint the client calls' do
|
|
183
|
+
post '/api/custom/islands/out_of_mount_action/hooks/load', params: JSON.dump(params), headers: headers
|
|
184
|
+
expect(response.status).to eq(200)
|
|
185
|
+
expect(JSON.parse(response.body)['fields'].map { |field| field['field'] }).to eq(['foo'])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'should let the host catch-all keep every other path under it' do
|
|
189
|
+
post '/api/custom/islands/something_else', params: JSON.dump(params), headers: headers
|
|
190
|
+
expect(JSON.parse(response.body)).to eq({'caught_by' => 'host catch-all'})
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'should serve /change at the endpoint the client calls' do
|
|
194
|
+
action = island.actions.select { |action| action.name == 'out_of_mount_action' }.first
|
|
195
|
+
foo = action.fields.select { |field| field[:field] == 'foo' }.first
|
|
196
|
+
updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
|
|
197
|
+
p = {
|
|
198
|
+
data: {
|
|
199
|
+
attributes: {
|
|
200
|
+
ids: [1],
|
|
201
|
+
fields: [updated_foo],
|
|
202
|
+
collection_name: 'Island',
|
|
203
|
+
changed_field: 'foo'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
post '/api/custom/islands/out_of_mount_action/hooks/change', params: JSON.dump(p), headers: headers
|
|
209
|
+
expect(response.status).to eq(200)
|
|
210
|
+
expect(JSON.parse(response.body)['fields'].map { |field| field['field'] }).to eq(['foo'])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'should not expose the hooks under the engine mount' do
|
|
214
|
+
expect {
|
|
215
|
+
post '/forest/api/custom/islands/out_of_mount_action/hooks/load', params: JSON.dump(params), headers: headers
|
|
216
|
+
}.to raise_error(ActionController::RoutingError)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
173
220
|
describe 'call /change' do
|
|
174
221
|
it 'should respond 200' do
|
|
175
222
|
action = island.actions.select { |action| action.name == 'my_action' }.first
|
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: 9.20.
|
|
4
|
+
version: 9.20.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -371,6 +371,7 @@ files:
|
|
|
371
371
|
- spec/dummy/app/config/routes.rb
|
|
372
372
|
- spec/dummy/app/controllers/application_controller.rb
|
|
373
373
|
- spec/dummy/app/controllers/forest/islands_controller.rb
|
|
374
|
+
- spec/dummy/app/controllers/host_catch_all_controller.rb
|
|
374
375
|
- spec/dummy/app/helpers/application_helper.rb
|
|
375
376
|
- spec/dummy/app/models/address.rb
|
|
376
377
|
- spec/dummy/app/models/application_record.rb
|
|
@@ -441,6 +442,7 @@ files:
|
|
|
441
442
|
- spec/lib/forest_liana/collection_spec.rb
|
|
442
443
|
- spec/lib/forest_liana/engine_spec.rb
|
|
443
444
|
- spec/lib/forest_liana/schema_file_updater_spec.rb
|
|
445
|
+
- spec/models/forest_liana/model/action_spec.rb
|
|
444
446
|
- spec/rails_helper.rb
|
|
445
447
|
- spec/requests/actions_controller_spec.rb
|
|
446
448
|
- spec/requests/authentications_spec.rb
|
|
@@ -684,6 +686,7 @@ test_files:
|
|
|
684
686
|
- spec/dummy/app/config/routes.rb
|
|
685
687
|
- spec/dummy/app/controllers/application_controller.rb
|
|
686
688
|
- spec/dummy/app/controllers/forest/islands_controller.rb
|
|
689
|
+
- spec/dummy/app/controllers/host_catch_all_controller.rb
|
|
687
690
|
- spec/dummy/app/helpers/application_helper.rb
|
|
688
691
|
- spec/dummy/app/models/address.rb
|
|
689
692
|
- spec/dummy/app/models/application_record.rb
|
|
@@ -754,6 +757,7 @@ test_files:
|
|
|
754
757
|
- spec/lib/forest_liana/collection_spec.rb
|
|
755
758
|
- spec/lib/forest_liana/engine_spec.rb
|
|
756
759
|
- spec/lib/forest_liana/schema_file_updater_spec.rb
|
|
760
|
+
- spec/models/forest_liana/model/action_spec.rb
|
|
757
761
|
- spec/rails_helper.rb
|
|
758
762
|
- spec/requests/actions_controller_spec.rb
|
|
759
763
|
- spec/requests/authentications_spec.rb
|