-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: pull in place index name from environment variable * chore: pull AWS credentials from awscli configuration in dev * chore: add AWS_PLACE_INDEX to envrc template * feat: request and parse search results * feat: expose search results via API endpoint * fix: correct FilterBBox * fix: make map component bounds match tileset bounds * feat: request and parse suggestions * feat: API endpoint for suggestions * refactor: have one source of truth for map bounds * refactor: make search/1 and suggest/1 return parsed value
- Loading branch information
Showing
20 changed files
with
524 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { create, Infer, number, type } from "superstruct" | ||
import appData from "./appData" | ||
|
||
const MapLimits = type({ | ||
north: number(), | ||
south: number(), | ||
east: number(), | ||
west: number(), | ||
}) | ||
type MapLimits = Infer<typeof MapLimits> | ||
|
||
const getMapLimits = (): MapLimits | null => { | ||
const mapLimitsJson = appData()?.mapLimits | ||
|
||
if (mapLimitsJson === undefined) { | ||
return null | ||
} | ||
|
||
const mapLimits = create(JSON.parse(mapLimitsJson) as unknown, MapLimits) | ||
|
||
return mapLimits | ||
} | ||
|
||
export default getMapLimits |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { StructError } from "superstruct" | ||
import appData from "../src/appData" | ||
import getMapLimits from "../src/mapLimits" | ||
|
||
jest.mock("appData") | ||
|
||
describe("getMapLimits", () => { | ||
test("returns map limits when properly formatted", () => { | ||
const limits = { north: 1, south: 2, east: 3, west: 4 } | ||
;(appData as jest.Mock).mockImplementation(() => ({ | ||
mapLimits: JSON.stringify(limits), | ||
})) | ||
|
||
expect(getMapLimits()).toEqual(limits) | ||
}) | ||
|
||
test.each([ | ||
// JSON.parse should throw when given invalid json | ||
{ expectedError: SyntaxError, mockTestGroupData: "" }, | ||
{ expectedError: SyntaxError, mockTestGroupData: "test 1234 !@#$ []{}" }, | ||
{ expectedError: SyntaxError, mockTestGroupData: [1, 2, 3] }, | ||
|
||
// Superstruct catches valid json but invalid types | ||
{ expectedError: StructError, mockTestGroupData: null }, | ||
{ expectedError: StructError, mockTestGroupData: JSON.stringify(null) }, | ||
{ expectedError: StructError, mockTestGroupData: true }, | ||
{ expectedError: StructError, mockTestGroupData: JSON.stringify(true) }, | ||
{ | ||
expectedError: StructError, | ||
mockTestGroupData: JSON.stringify([1, 2, 3]), | ||
}, | ||
{ | ||
expectedError: StructError, | ||
mockTestGroupData: JSON.stringify({ irrelevant: "keys" }), | ||
}, | ||
])( | ||
"raise error when test group data is not the correct type: case %#", | ||
({ mockTestGroupData, expectedError: expectedError }) => { | ||
;(appData as jest.Mock).mockImplementationOnce(() => ({ | ||
mapLimits: mockTestGroupData, | ||
})) | ||
expect(() => getMapLimits()).toThrowError(expectedError) | ||
} | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
defmodule Skate.LocationSearch.AwsLocationRequest do | ||
alias Skate.LocationSearch.SearchResult | ||
|
||
@spec search(String.t()) :: {:ok, map()} | {:error, term()} | ||
def search(text) do | ||
request_fn = Application.get_env(:skate, :aws_request_fn, &ExAws.request/1) | ||
|
||
path = | ||
"/places/v0/indexes/" <> Application.get_env(:skate, :aws_place_index) <> "/search/text" | ||
|
||
case %ExAws.Operation.RestQuery{ | ||
http_method: :post, | ||
path: path, | ||
body: Map.merge(base_arguments(), %{Text: text}), | ||
service: :places | ||
} | ||
|> request_fn.() do | ||
{:ok, response} -> parse_search_response(response) | ||
{:error, error} -> {:error, error} | ||
end | ||
end | ||
|
||
@spec suggest(String.t()) :: {:ok, map()} | {:error, term()} | ||
def suggest(text) do | ||
request_fn = Application.get_env(:skate, :aws_request_fn, &ExAws.request/1) | ||
|
||
path = | ||
"/places/v0/indexes/" <> | ||
Application.get_env(:skate, :aws_place_index) <> "/search/suggestions" | ||
|
||
case %ExAws.Operation.RestQuery{ | ||
http_method: :post, | ||
path: path, | ||
body: Map.merge(base_arguments(), %{Text: text}), | ||
service: :places | ||
} | ||
|> request_fn.() do | ||
{:ok, response} -> parse_suggest_response(response) | ||
{:error, error} -> {:error, error} | ||
end | ||
end | ||
|
||
defp parse_search_response(%{status_code: 200, body: body}) do | ||
%{"Results" => results} = Jason.decode!(body) | ||
|
||
Enum.map(results, fn result -> | ||
%{"PlaceId" => id, "Place" => place} = result | ||
|
||
%{"Label" => label, "Geometry" => %{"Point" => [longitude, latitude]}} = place | ||
|
||
{name, address} = | ||
separate_label_text(label, Map.get(place, "AddressNumber"), Map.get(place, "Street")) | ||
|
||
%SearchResult{ | ||
id: id, | ||
name: name, | ||
address: address, | ||
latitude: latitude, | ||
longitude: longitude | ||
} | ||
end) | ||
end | ||
|
||
defp parse_suggest_response(%{status_code: 200, body: body}) do | ||
%{"Results" => results} = Jason.decode!(body) | ||
|
||
Enum.map(results, fn result -> Map.get(result, "Text") end) | ||
end | ||
|
||
@spec separate_label_text(String.t(), String.t() | nil, String.t() | nil) :: | ||
{String.t() | nil, String.t()} | ||
defp separate_label_text(label, nil, nil), do: {nil, label} | ||
|
||
defp separate_label_text(label, address_number, street) do | ||
address_prefix = address_number || street | ||
|
||
case address_prefix | ||
|> Regex.escape() | ||
|> Regex.compile!() | ||
|> Regex.split(label, parts: 2, include_captures: true) do | ||
["", prefix, address] -> | ||
{nil, prefix <> address} | ||
|
||
[name, prefix, address] -> | ||
{Regex.replace(~r/, $/, name, ""), prefix <> address} | ||
|
||
[prefix, address] -> | ||
{nil, prefix <> address} | ||
end | ||
end | ||
|
||
defp base_arguments do | ||
map_limits = Application.get_env(:skate, :map_limits) | ||
|
||
%{ | ||
FilterBBox: [map_limits[:west], map_limits[:south], map_limits[:east], map_limits[:north]] | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
defmodule Skate.LocationSearch.SearchResult do | ||
@type t :: %__MODULE__{ | ||
id: String.t(), | ||
name: String.t() | nil, | ||
address: String.t(), | ||
latitude: float(), | ||
longitude: float() | ||
} | ||
|
||
@enforce_keys [ | ||
:id, | ||
:address, | ||
:latitude, | ||
:longitude | ||
] | ||
|
||
@derive {Jason.Encoder, only: [:id, :name, :address, :latitude, :longitude]} | ||
defstruct [ | ||
:id, | ||
:name, | ||
:address, | ||
:latitude, | ||
:longitude | ||
] | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
defmodule SkateWeb.LocationSearchController do | ||
use SkateWeb, :controller | ||
|
||
alias Skate.LocationSearch.AwsLocationRequest | ||
|
||
@spec search(Plug.Conn.t(), map()) :: Plug.Conn.t() | ||
def search(conn, %{"query" => query}) do | ||
seach_fn = Application.get_env(:skate, :location_search_fn, &AwsLocationRequest.search/1) | ||
|
||
{:ok, result} = seach_fn.(query) | ||
|
||
json(conn, %{data: result}) | ||
end | ||
|
||
@spec suggest(Plug.Conn.t(), map()) :: Plug.Conn.t() | ||
def suggest(conn, %{"query" => query}) do | ||
suggest_fn = Application.get_env(:skate, :location_suggest_fn, &AwsLocationRequest.suggest/1) | ||
|
||
{:ok, result} = suggest_fn.(query) | ||
|
||
json(conn, %{data: result}) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.