Skip to content

Commit a899c4f

Browse files
authored
(#990) Add depth parameter to GET /resources/:ctid (#1007)
1 parent ab05344 commit a899c4f

4 files changed

Lines changed: 153 additions & 5 deletions

File tree

app/api/v1/resources.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ class Resources < MountableAPI
9191
desc 'Return a resource.'
9292
params do
9393
requires :id, type: String, desc: 'Resource id.'
94+
optional :depth,
95+
type: Integer,
96+
default: 0,
97+
desc: 'Maximum depth for nested blank nodes.',
98+
values: ->(value) { value >= 0 }
9499
end
95100
before do
96101
authenticate_community!
@@ -99,7 +104,8 @@ class Resources < MountableAPI
99104
resource = FetchEnvelopeResource
100105
.new(
101106
envelope_community: current_community,
102-
resource_id: params[:id]
107+
resource_id: params[:id],
108+
depth: params[:depth]
103109
)
104110
.resource
105111

app/services/fetch_envelope_resource.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
class FetchEnvelopeResource # rubocop:todo Style/Documentation
22
BNODE_ID_REGEX = '(?<=")_:[^"]+(?=")'.freeze
33

4-
attr_reader :envelope_community, :resource_id
4+
attr_reader :envelope_community, :resource_id, :depth
55

66
delegate :connection, to: ActiveRecord::Base
77
delegate :quote, to: :connection
88

9-
def initialize(envelope_community:, resource_id:)
9+
def initialize(envelope_community:, resource_id:, depth: nil)
1010
@envelope_community = envelope_community
1111
@resource_id = resource_id&.downcase
12+
@depth = depth
1213
end
1314

1415
def query
@@ -43,6 +44,7 @@ def query
4344
) bnodes
4445
ON envelope_resources.resource_id = bnodes.resource_id
4546
WHERE NOT envelope_resources.resource_id = ANY(bnodes.path)
47+
AND #{depth_recursion_condition}
4648
AND envelopes.deleted_at IS NULL
4749
AND envelopes.envelope_community_id = #{envelope_community.id}
4850
)
@@ -74,4 +76,12 @@ def resource
7476
result.first.fetch('resource')
7577
end
7678
end
79+
80+
private
81+
82+
def depth_recursion_condition
83+
return 'TRUE' if depth.nil?
84+
85+
"cardinality(bnodes.path) <= #{depth.to_i}"
86+
end
7787
end

lib/swagger_docs/sections/resources.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ module Resources # rubocop:todo Metrics/ModuleLength, Style/Documentation
208208

209209
parameter community_name
210210
parameter resource_id
211+
parameter name: :depth,
212+
in: :query,
213+
type: :integer,
214+
format: :int32,
215+
default: 0,
216+
required: false,
217+
minimum: 0,
218+
description: 'Maximum depth for nested blank nodes'
211219

212220
response 200 do
213221
key :description, 'Retrieves a resource by identifier'

spec/api/v1/resources_spec.rb

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,30 @@
2424
let(:resource_with_ids) do
2525
resource.merge('@id' => full_id, 'ceterms:ctid' => ctid)
2626
end
27+
let(:extract_resources) { false }
28+
let(:request_params) { {} }
29+
let(:request_path) do
30+
path = "/resources/#{CGI.escape(id).upcase}"
31+
return path if request_params.empty?
32+
33+
"#{path}?#{Rack::Utils.build_query(request_params)}"
34+
end
2735

2836
before do
2937
allow_any_instance_of(EnvelopeCommunity) # rubocop:todo RSpec/AnyInstance
3038
.to receive(:id_field).and_return(id_field)
3139

32-
create(
40+
envelope = create(
3341
:envelope,
3442
:from_cer,
3543
:with_cer_credential,
3644
envelope_community: ec,
3745
processed_resource: resource_with_ids
3846
)
3947

40-
get "/resources/#{CGI.escape(id).upcase}"
48+
ExtractEnvelopeResources.call(envelope:) if extract_resources
49+
50+
get request_path
4151
end
4252

4353
# rubocop:todo RSpec/MultipleMemoizedHelpers
@@ -257,6 +267,120 @@
257267
end
258268
# rubocop:enable RSpec/MultipleMemoizedHelpers
259269
end
270+
271+
# rubocop:todo RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers
272+
context 'with `depth` query param' do # rubocop:todo RSpec/MultipleMemoizedHelpers, RSpec/NestedGroups
273+
# rubocop:enable RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers
274+
let(:id_field) { 'ceterms:ctid' }
275+
let(:id) { ctid }
276+
let(:extract_resources) { true }
277+
let(:parent_bnode_id) { "_:#{Envelope.generate_ctid}" }
278+
let(:child_bnode_id) { "_:#{Envelope.generate_ctid}" }
279+
let(:request_params) { { depth: depth } }
280+
let(:resource_with_ids) do
281+
{
282+
'@id' => "https://credentialengineregistry.org/graph/#{ctid}",
283+
'@type' => 'ceasn:CompetencyFramework',
284+
'@context' => 'http://credreg.net/ctdlasn/schema/context/json',
285+
'@graph' => [main_resource, parent_nested_resource, child_nested_resource]
286+
}
287+
end
288+
let(:main_resource) do
289+
{
290+
'@id' => full_id,
291+
'@type' => 'ceasn:CompetencyFramework',
292+
'ceterms:ctid' => ctid,
293+
'ceasn:hasTopChild' => [{ '@id' => parent_bnode_id }]
294+
}
295+
end
296+
let(:parent_nested_resource) do
297+
{
298+
'@id' => parent_bnode_id,
299+
'@type' => 'ceasn:Competency',
300+
'ceasn:narrowAlignment' => [{ '@id' => child_bnode_id }]
301+
}
302+
end
303+
let(:child_nested_resource) do
304+
{
305+
'@id' => child_bnode_id,
306+
'@type' => 'ceasn:Competency'
307+
}
308+
end
309+
310+
# rubocop:todo RSpec/NestedGroups
311+
# rubocop:todo RSpec/MultipleMemoizedHelpers
312+
context 'when depth is 0' do # rubocop:todo RSpec/NestedGroups
313+
# rubocop:enable RSpec/NestedGroups
314+
let(:depth) { 0 }
315+
316+
it 'does not include nested blank nodes' do
317+
expect_status(:ok)
318+
expect(JSON(response.body).fetch('@included', [])).to be_empty
319+
end
320+
end
321+
# rubocop:enable RSpec/MultipleMemoizedHelpers
322+
323+
# rubocop:todo RSpec/NestedGroups
324+
# rubocop:todo RSpec/MultipleMemoizedHelpers
325+
context 'when depth is omitted' do # rubocop:todo RSpec/NestedGroups
326+
# rubocop:enable RSpec/NestedGroups
327+
let(:request_params) { {} }
328+
329+
it 'defaults to 0 depth' do
330+
expect_status(:ok)
331+
expect(JSON(response.body).fetch('@included', [])).to be_empty
332+
end
333+
end
334+
# rubocop:enable RSpec/MultipleMemoizedHelpers
335+
336+
# rubocop:todo RSpec/NestedGroups
337+
# rubocop:todo RSpec/MultipleMemoizedHelpers
338+
context 'when depth is 1' do # rubocop:todo RSpec/NestedGroups
339+
# rubocop:enable RSpec/NestedGroups
340+
let(:depth) { 1 }
341+
342+
it 'includes first-level blank nodes only' do
343+
expect_status(:ok)
344+
included_ids = JSON(response.body)
345+
.fetch('@included', [])
346+
.map { |item| item.fetch('@id') }
347+
expect(
348+
included_ids
349+
).to contain_exactly(parent_bnode_id)
350+
end
351+
end
352+
# rubocop:enable RSpec/MultipleMemoizedHelpers
353+
354+
# rubocop:todo RSpec/NestedGroups
355+
# rubocop:todo RSpec/MultipleMemoizedHelpers
356+
context 'when depth is 2' do # rubocop:todo RSpec/NestedGroups
357+
# rubocop:enable RSpec/NestedGroups
358+
let(:depth) { 2 }
359+
360+
it 'includes nested blank nodes up to the requested depth' do
361+
expect_status(:ok)
362+
included_ids = JSON(response.body)
363+
.fetch('@included', [])
364+
.map { |item| item.fetch('@id') }
365+
expect(
366+
included_ids
367+
).to contain_exactly(parent_bnode_id, child_bnode_id)
368+
end
369+
end
370+
# rubocop:enable RSpec/MultipleMemoizedHelpers
371+
372+
# rubocop:todo RSpec/NestedGroups
373+
# rubocop:todo RSpec/MultipleMemoizedHelpers
374+
context 'when depth is negative' do # rubocop:todo RSpec/NestedGroups
375+
# rubocop:enable RSpec/NestedGroups
376+
let(:depth) { -1 }
377+
378+
it 'returns bad_request' do
379+
expect_status(:bad_request)
380+
end
381+
end
382+
# rubocop:enable RSpec/MultipleMemoizedHelpers
383+
end
260384
end
261385
# rubocop:enable RSpec/MultipleMemoizedHelpers
262386

0 commit comments

Comments
 (0)