Skip to content

Commit

Permalink
enhance: standardize API endpoints (#981)
Browse files Browse the repository at this point in the history
- Wrap return JSON in an "envelope" to provide the ability to send down
  viewable data versus metadata (such as paging information).
- Moves spec `json_response` to a support/api_helpers file to allow for
  a single source for parsing response body into json_response (data)
  and json_meta (meta).

Eventually we will want to add API headers for versioning so we can
introduce changes with backwards compatibility.

Co-authored-by: Laura Mosher <lauramosher@users.noreply.github.com>
  • Loading branch information
lauramosher and lauramosher committed Oct 28, 2023
1 parent 52cb696 commit 718bc49
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 119 deletions.
15 changes: 15 additions & 0 deletions rails/app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ class BaseController < ActionController::Base

rescue_from ActiveRecord::RecordNotFound, with: :not_found


helper_method :envelope
def envelope(json, pageMeta = nil)
json.data do
yield if block_given?
end
if pageMeta
json.meta do
json.total @page.total
json.hasNextPage @page.has_next_page?
json.nextPageMeta @page.next_page_meta
end
end
end

protected

def not_found
Expand Down
42 changes: 22 additions & 20 deletions rails/app/views/api/communities/index.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
json.array! @communities do |community|
json.extract! community, :name, :description, :slug
envelope(json) do
json.array! @communities do |community|
json.extract! community, :name, :description, :slug

json.createdAt community.created_at
json.updatedAt community.updated_at
json.createdAt community.created_at
json.updatedAt community.updated_at

if community.display_image.attached?
json.displayImage rails_blob_url(community.display_image)
end
if community.display_image.attached?
json.displayImage rails_blob_url(community.display_image)
end

if community.theme.static_map.attached?
json.staticMapUrl rails_blob_url(community.theme.static_map)
end
if community.theme.static_map.attached?
json.staticMapUrl rails_blob_url(community.theme.static_map)
end

json.mapConfig do
json.mapboxAccessToken community.theme.mapbox_access_token
json.mapboxStyle community.theme.mapbox_style
json.mapbox3dEnabled community.theme.mapbox_3d
json.mapProjection community.theme.map_projection
json.mapConfig do
json.mapboxAccessToken community.theme.mapbox_access_token
json.mapboxStyle community.theme.mapbox_style
json.mapbox3dEnabled community.theme.mapbox_3d
json.mapProjection community.theme.map_projection

json.center community.theme.center
json.center community.theme.center

json.zoom community.theme.zoom
json.pitch community.theme.pitch
json.bearing community.theme.bearing
json.zoom community.theme.zoom
json.pitch community.theme.pitch
json.bearing community.theme.bearing
end
end
end
end
88 changes: 45 additions & 43 deletions rails/app/views/api/communities/show.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
json.(@community, :name, :slug, :description)

# Details are used specifically in the Explore Panel
json.details do
json.(@community, :name, :description)
json.sponsorLogos @community.sponsor_logos do |logo|
json.url rails_blob_url(logo)
json.blobId logo.blob.id
envelope(json) do
json.(@community, :name, :slug, :description)

# Details are used specifically in the Explore Panel
json.details do
json.(@community, :name, :description)
json.sponsorLogos @community.sponsor_logos do |logo|
json.url rails_blob_url(logo)
json.blobId logo.blob.id
end
if @community.display_image.attached?
json.displayImage rails_blob_url(@community.display_image)
end
end
if @community.display_image.attached?
json.displayImage rails_blob_url(@community.display_image)

# Initial Map Configuration
stories = @community.stories.preload(:places).where(permission_level: :anonymous)
json.storiesCount stories.size
json.points stories.flat_map(&:public_points).uniq

# Side Panel Filter Categories
json.categories Community::FILTERABLE_ATTRIBUTES

# Side Panel Filter Options (generated from available content)
json.filters @community.filters

json.mapConfig do
json.mapboxAccessToken @community.theme.mapbox_access_token
json.mapboxStyle @community.theme.mapbox_style
json.mapbox3dEnabled @community.theme.mapbox_3d
json.mapProjection @community.theme.map_projection

json.centerLat @community.theme.center_lat
json.centerLong @community.theme.center_long
json.swBoundaryLat @community.theme.sw_boundary_lat
json.swBoundaryLong @community.theme.sw_boundary_long
json.neBoundaryLat @community.theme.ne_boundary_lat
json.neBoundaryLong @community.theme.ne_boundary_long

json.center @community.theme.center
json.maxBounds @community.theme.boundaries

json.zoom @community.theme.zoom
json.pitch @community.theme.pitch
json.bearing @community.theme.bearing
end
end

# Initial Map Configuration
stories = @community.stories.preload(:places).where(permission_level: :anonymous)
json.storiesCount stories.size
json.points stories.flat_map(&:public_points).uniq

# Side Panel Filter Categories
json.categories Community::FILTERABLE_ATTRIBUTES

# Side Panel Filter Options (generated from available content)
json.filters @community.filters

json.mapConfig do
json.mapboxAccessToken @community.theme.mapbox_access_token
json.mapboxStyle @community.theme.mapbox_style
json.mapbox3dEnabled @community.theme.mapbox_3d
json.mapProjection @community.theme.map_projection

json.centerLat @community.theme.center_lat
json.centerLong @community.theme.center_long
json.swBoundaryLat @community.theme.sw_boundary_lat
json.swBoundaryLong @community.theme.sw_boundary_long
json.neBoundaryLat @community.theme.ne_boundary_lat
json.neBoundaryLong @community.theme.ne_boundary_long

json.center @community.theme.center
json.maxBounds @community.theme.boundaries

json.zoom @community.theme.zoom
json.pitch @community.theme.pitch
json.bearing @community.theme.bearing
end
end
10 changes: 6 additions & 4 deletions rails/app/views/api/places/show.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
json.(@place, :id, :name, :description, :region)
envelope(json) do
json.(@place, :id, :name, :description, :region)

json.placenameAudio @place.name_audio_url(full_url: true)
json.typeOfPlace @place.type_of_place
json.placenameAudio @place.name_audio_url(full_url: true)
json.typeOfPlace @place.type_of_place

json.points [@place.public_point_feature]
json.points [@place.public_point_feature]
end
31 changes: 14 additions & 17 deletions rails/app/views/api/stories/index.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
json.total @page.total

# Regardless of Story list page, all points in the data relation
# should be returned for map markers.
json.points @page.relation.flat_map { |s| s.public_points }.uniq

json.stories @stories do |story|
json.extract! story, :id, :title, :topic, :desc, :language
json.mediaContentTypes story.media_types
json.mediaPreviewUrl story.media_preview_thumbnail

json.createdAt story.created_at
json.updatedAt story.updated_at
end

json.hasNextPage @page.has_next_page?
json.nextPageMeta @page.next_page_meta
envelope(json, @page) do
# Regardless of Story list page, all points in the data relation
# should be returned for map markers.
json.points @page.relation.flat_map { |s| s.public_points }.uniq

json.stories @stories do |story|
json.extract! story, :id, :title, :topic, :desc, :language
json.mediaContentTypes story.media_types
json.mediaPreviewUrl story.media_preview_thumbnail

json.createdAt story.created_at
json.updatedAt story.updated_at
end
end
2 changes: 2 additions & 0 deletions rails/app/views/api/stories/show.json.jbuilder
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
envelope(json) do
json.(@story, :id, :title, :desc, :topic, :language)
json.media @story.media do |media|
json.contentType media.content_type
Expand Down Expand Up @@ -46,3 +47,4 @@ json.places @story.places do |place|
end

json.points @story.public_points
end
4 changes: 0 additions & 4 deletions rails/spec/requests/api/public_communities_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
let!(:public_community) { FactoryBot.create(:public_community, name: "Cool Community") }
let!(:community) { FactoryBot.create(:community, name: "Private Community") }

def json_response
JSON.parse(response.body)
end

it "returns an array of public communities" do
get "/api/communities"

Expand Down
4 changes: 0 additions & 4 deletions rails/spec/requests/api/public_community_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
RSpec.describe "Public Community (show) Endpoint", type: :request do
let!(:public_community) { FactoryBot.create(:public_community, name: "Cool Community") }

def json_response
JSON.parse(response.body)
end

it "returns 404 when community can't be found" do
get "/api/communities/unknown"

Expand Down
5 changes: 0 additions & 5 deletions rails/spec/requests/api/public_place_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@
)
end


def json_response
JSON.parse(response.body)
end

it "returns a 404 not found if community is not found" do
get "/api/communities/unknown/places/123"

Expand Down
33 changes: 15 additions & 18 deletions rails/spec/requests/api/public_stories_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
RSpec.describe "Public Stories Endpoint", type: :request do
let!(:community) { FactoryBot.create(:public_community, name: "Cool Community") }

def json_response
JSON.parse(response.body)
end

it "returns 404 when community can't be found" do
get "/api/communities/unknown/stories"

Expand All @@ -25,7 +21,8 @@ def json_response
get "/api/communities/cool_community/stories"

expect(response).to have_http_status(:ok)
expect(json_response.keys).to contain_exactly("total", "points", "stories", "hasNextPage", "nextPageMeta")
expect(json_response.keys).to contain_exactly("points", "stories")
expect(json_meta.keys).to contain_exactly("total", "hasNextPage", "nextPageMeta")
end

context "filters and sort" do
Expand Down Expand Up @@ -90,43 +87,43 @@ def json_response
# filter by place (id / name)
get "/api/communities/cool_community/stories", params: {places: [place_2.id]}

expect(json_response["total"]).to eq(1)
expect(json_meta["total"]).to eq(1)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id)

# filter by region
get "/api/communities/cool_community/stories", params: {region: ["the internet"]}

expect(json_response["total"]).to eq(2)
expect(json_meta["total"]).to eq(2)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_2.id, story_3.id)

# filter by type of place
get "/api/communities/cool_community/stories", params: {type_of_place: ["online"]}

expect(json_response["total"]).to eq(1)
expect(json_meta["total"]).to eq(1)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id)

# filter by topic
get "/api/communities/cool_community/stories", params: {topic: ["nonprofit work", "tech"]}

expect(json_response["total"]).to eq(2)
expect(json_meta["total"]).to eq(2)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_2.id, story_3.id)

# filter by language
get "/api/communities/cool_community/stories", params: {language: ["Spanish", "Other"]}

expect(json_response["total"]).to eq(1)
expect(json_meta["total"]).to eq(1)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_3.id)

# filter by speaker
get "/api/communities/cool_community/stories", params: {speakers: [speaker_1.id, speaker_2.id]}

expect(json_response["total"]).to eq(3)
expect(json_meta["total"]).to eq(3)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id, story_2.id, story_3.id)

# filter by speaker community
get "/api/communities/cool_community/stories", params: {speaker_community: ["ruby for good"]}

expect(json_response["total"]).to eq(2)
expect(json_meta["total"]).to eq(2)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id, story_2.id)
end

Expand All @@ -136,7 +133,7 @@ def json_response
get "/api/communities/cool_community/stories"


expect(json_response["total"]).to eq(1)
expect(json_meta["total"]).to eq(1)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id)
end

Expand All @@ -145,7 +142,7 @@ def json_response

get "/api/communities/cool_community/stories"

expect(json_response["total"]).to eq(2)
expect(json_meta["total"]).to eq(2)
expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id, story_3.id)
end

Expand All @@ -172,16 +169,16 @@ def json_response
it "correctly paginates with filters" do
get "/api/communities/cool_community/stories", params: {limit: 1}

expect(json_response["total"]).to eq(3)
expect(json_meta["total"]).to eq(3)
expect(json_response["stories"].count).to eq(1)
expect(json_response["hasNextPage"]).to be true
expect(json_meta["hasNextPage"]).to be true

# filter down to one place
get "/api/communities/cool_community/stories", params: {limit: 1, places: [place_2.id]}

expect(json_response["total"]).to eq(1)
expect(json_meta["total"]).to eq(1)
expect(json_response["stories"].count).to eq(1)
expect(json_response["hasNextPage"]).to be false
expect(json_meta["hasNextPage"]).to be false
end
end
end
4 changes: 0 additions & 4 deletions rails/spec/requests/api/public_story_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
)
end

def json_response
JSON.parse(response.body)
end

it "returns 404 when community can't be found" do
get "/api/communities/unknown/stories/123"

Expand Down
11 changes: 11 additions & 0 deletions rails/spec/support/api_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def json_body
JSON.parse(response.body)
end

def json_response
json_body["data"]
end

def json_meta
json_body["meta"]
end

0 comments on commit 718bc49

Please sign in to comment.