Skip to content

Commit a82df9b

Browse files
authored
Merge pull request #672 from alphagov/CM-951-search-endpoint-for-api
(CM-951) Search endpoint for API
2 parents 99b30fc + 21a7181 commit a82df9b

23 files changed

Lines changed: 1060 additions & 5 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Check OpenAPI documentation is up to date
2+
on:
3+
workflow_call:
4+
5+
jobs:
6+
check-open-api:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
10+
with:
11+
show-progress: false
12+
13+
- name: Setup Ruby
14+
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
15+
with:
16+
bundler-cache: true
17+
18+
- name: Setup Postgres
19+
id: setup-postgres
20+
uses: alphagov/govuk-infrastructure/.github/actions/setup-postgres@main
21+
22+
- name: Initialize database
23+
env:
24+
RAILS_ENV: test
25+
TEST_DATABASE_URL: ${{ steps.setup-postgres.outputs.db-url }}
26+
run: bundle exec rails db:setup
27+
28+
- name: Build OpenAPI documentation
29+
run: bundle exec rake api:generate_swagger
30+
env:
31+
RAILS_ENV: test
32+
TEST_DATABASE_URL: ${{ steps.setup-postgres.outputs.db-url }}
33+
34+
- name: Check for uncommitted changes
35+
run: |
36+
if git diff --exit-code; then
37+
echo "No uncommitted changes detected."
38+
else
39+
echo "::error title=Uncommitted changes to OpenAPI docs::If these are your changes, run \\\`rake api:generate_swagger\\\` and commit the changes."
40+
exit 1
41+
fi

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ jobs:
2929
security-events: write
3030
actions: read
3131

32+
check-open-api:
33+
name: Check OpenAPI
34+
uses: ./.github/workflows/check-open-api.yml
35+
3236
lint-scss:
3337
name: Lint SCSS
3438
uses: ./.github/workflows/lintscss.yml

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ group :development, :test do
5050
gem "pry-byebug"
5151
gem "pry-rails"
5252
gem "rspec-rails"
53+
gem "rswag-specs"
5354
gem "rubocop-govuk"
5455
gem "unparser", require: false
5556
end

Gemfile.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,11 @@ GEM
771771
rspec-mocks (>= 3.13.0, < 5.0.0)
772772
rspec-support (>= 3.13.0, < 5.0.0)
773773
rspec-support (3.13.7)
774+
rswag-specs (2.17.0)
775+
activesupport (>= 5.2, < 8.2)
776+
json-schema (>= 2.2, < 7.0)
777+
railties (>= 5.2, < 8.2)
778+
rspec-core (>= 2.14)
774779
rubocop (1.86.0)
775780
json (~> 2.3)
776781
language_server-protocol (~> 3.17.0.2)
@@ -995,6 +1000,7 @@ DEPENDENCIES
9951000
record_tag_helper
9961001
rinku
9971002
rspec-rails
1003+
rswag-specs
9981004
rubocop-govuk
9991005
sentry-sidekiq
10001006
shoulda-matchers
@@ -1037,7 +1043,6 @@ CHECKSUMS
10371043
bootsnap (1.24.4) sha256=a4d939fc2cc5242a83d3a7cb4fb97743ac58475afe91e0600479a3df6f117541
10381044
brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f
10391045
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
1040-
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
10411046
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
10421047
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
10431048
capybara-playwright-driver (0.5.9) sha256=4c17fed20817b7e1bde2cfc8186e7bf5cd72017ce1aa695e35130f8e600e7282
@@ -1274,6 +1279,7 @@ CHECKSUMS
12741279
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
12751280
rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df
12761281
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
1282+
rswag-specs (2.17.0) sha256=a3b2bdf6df89f8741fe4a4ee47ceb1e77dc13e1c96bbe07352117d6e61afa9e3
12771283
rubocop (1.86.0) sha256=4ff1186fe16ebe9baff5e7aad66bb0ad4cabf5cdcd419f773146dbba2565d186
12781284
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
12791285
rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ To run all linter with automatic formatting corrections, run:
101101
bundle exec rake:lint_autocorrect
102102
```
103103

104+
### API
105+
106+
The code for the API is in the [`engines/api`](engines/api) directory. It is a Rails engine, and uses
107+
[`rswag`](https://github.com/rswag/rswag) to generate [OpenAPI documentation](engines/api/swagger) from the tests in
108+
[`engines/api/spec/requests`](engines/api/spec/requests).
109+
110+
If you make any changes to the API, then you can regenerate the OpenAPI documentation by running:
111+
112+
```
113+
rake api:generate_swagger
114+
```
115+
116+
This will then update the OpenAPI documentation in [`engines/api/swagger`](engines/api/swagger).
117+
104118
### Further documentation
105119

106120
See the [`docs/`](docs/) directory.

app/public/models/content_block.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ def initialize(edition)
1616
@edition = edition
1717
end
1818

19-
delegate :id, :title, :state, :details, :document, :auth_bypass_id, :content_id, :render, to: :edition
20-
delegate :schema, to: :document
21-
delegate :embeddable_as_block?, to: :schema
19+
delegate :id, :title, :state, :details, :document, :auth_bypass_id, :content_id, :render, :lead_organisation, to: :edition
20+
delegate :schema, :embed_code, to: :document
21+
delegate :embeddable_as_block?, :formats, to: :schema
2222

2323
def block_type
2424
schema.name
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class ContentBlock
2+
class Query
3+
DEFAULT_PAGE_SIZE = 10
4+
Result = Data.define(:blocks, :current_page, :total_pages, :total_count)
5+
6+
def self.call(filters = {})
7+
new(filters).results
8+
end
9+
10+
def initialize(filters)
11+
@filters = filters
12+
end
13+
14+
def results
15+
Result.new(
16+
blocks: paginated_results.map { |document| ContentBlock.new(document.most_recent_edition) },
17+
current_page: paginated_results.current_page,
18+
total_pages: paginated_results.total_pages,
19+
total_count: paginated_results.total_count,
20+
)
21+
end
22+
23+
private
24+
25+
attr_reader :filters
26+
27+
def paginated_results
28+
@paginated_results ||= unpaginated_results.page(page).per(DEFAULT_PAGE_SIZE)
29+
end
30+
31+
def page
32+
filters[:page].presence || 1
33+
end
34+
35+
def unpaginated_results
36+
Document.joins(:editions)
37+
.merge(Edition.most_recent_for_document)
38+
.merge(Edition.published)
39+
.extending(Scopes)
40+
.by_block_type(filters[:block_type])
41+
.by_lead_organisation_id(filters[:lead_organisation_id])
42+
.by_keyword(filters[:keyword])
43+
end
44+
45+
module Scopes
46+
def by_block_type(block_type)
47+
return self if block_type.blank?
48+
49+
where("block_type = ?", block_type)
50+
end
51+
52+
def by_lead_organisation_id(lead_organisation_id)
53+
return self if lead_organisation_id.blank?
54+
55+
where("editions.lead_organisation_id = ?", lead_organisation_id)
56+
end
57+
58+
def by_keyword(keyword)
59+
return self if keyword.blank?
60+
61+
where("documents.id IN (?)", Document.with_keyword(keyword).pluck(:id))
62+
end
63+
end
64+
end
65+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Api::BlocksController < Api::ApplicationController
2+
def search
3+
result = ContentBlock::Query.call(filters)
4+
render json: Api::ResultsPresenter.present(result, request.original_url)
5+
end
6+
7+
private
8+
9+
def filters
10+
params.permit(:block_type, :lead_organisation_id, :keyword, :page)
11+
end
12+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class Api::BlockPresenter
2+
class << self
3+
def present(block)
4+
new(block).present
5+
end
6+
7+
def present_collection(blocks)
8+
blocks.map { |block| present(block) }
9+
end
10+
end
11+
12+
def initialize(block)
13+
@block = block
14+
end
15+
16+
def present
17+
{
18+
title: @block.title,
19+
block_type: @block.block_type,
20+
organisation: {
21+
name: @block.lead_organisation.name,
22+
content_id: @block.lead_organisation.id,
23+
},
24+
state: "published",
25+
embed_code: @block.embed_code,
26+
formats: @block.formats,
27+
}
28+
end
29+
end
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
module Api
2+
class ResultsPresenter
3+
class << self
4+
def present(result, request_url)
5+
new(result, request_url).present
6+
end
7+
end
8+
9+
def initialize(result, request_url)
10+
@result = result
11+
@request_url = URI.parse(request_url)
12+
end
13+
14+
def present
15+
{
16+
total: result.total_count,
17+
pages: result.total_pages,
18+
current_page: result.current_page,
19+
links:,
20+
results: BlockPresenter.present_collection(result.blocks),
21+
}
22+
end
23+
24+
private
25+
26+
attr_reader :result, :request_url
27+
28+
def links
29+
[previous_link, next_link, self_link].compact
30+
end
31+
32+
def previous_link
33+
return unless previous_page?
34+
35+
{
36+
href: page_href(-1),
37+
rel: "previous",
38+
}
39+
end
40+
41+
def next_link
42+
return unless next_page?
43+
44+
{
45+
href: page_href(1),
46+
rel: "next",
47+
}
48+
end
49+
50+
def self_link
51+
{
52+
href: page_href(0),
53+
rel: "self",
54+
}
55+
end
56+
57+
def page_href(offset)
58+
request_url.tap { |url|
59+
query_hash = Rack::Utils.parse_query(url.query)
60+
query_hash["page"] = result.current_page + offset
61+
url.query = Rack::Utils.build_query(query_hash)
62+
}.to_s
63+
end
64+
65+
def previous_page?
66+
result.current_page > 1
67+
end
68+
69+
def next_page?
70+
result.current_page < result.total_pages
71+
end
72+
end
73+
end

0 commit comments

Comments
 (0)