Building the X Search API
Team Oblivion
About
- Team Oblivian approach
- Challenges
- Tools we used
** Things might have changed
Built with
Elixir 🔗
Phoenix Framework 🔗
Vagrant 🔗
Elixir getting started
Mix Elixir
- Get basic project file layout
- Unit testing (ExUnit)
mix new awesome_project --module AwesomeProject
Mix is a build tool, run tasks and tests
mix compile
// after compile
iex -S mix
-
Create an iex session (Elixir’s interactive shell)
-
Learn more about mix 🔗
Elixir
- Head | tail recursion
- Pattern matching
- Everything is immutable
Thinking functionally
- Basics easy to get
- Dynamically typed language
- Modules and named function
- Piping |>
- :Atoms (name is their value)
Language and syntax
Using Vagrant
Main benefits
- Developing on Windows side (editor and browsers)
- Disposable environment
- Fairly easy to configure
- Everyone has the same environment, close to live
- Easy access to a Linux
vagrant up
vagrant ssh
Phoenix
- Web framework
- MVC
- Core structure for our API
- Routing
- Controllers
- Comes with loads of other stuff
- Channels - send and receive messages
- Ecto - writing queries and interacting with databases
- Templates and views
Phoenix docs 🔗
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
Phoenix routing
pipeline :search do
plug :accepts, ["json"]
plug XSearchApi.CORS # plug: connection lifecycle
end
scope "/", XSearchApi do # group similar routes together
pipe_through :search
get "/category/count", SearchController, :category_search_count
end
# eg scope "/admin" ...
- Simple
- Scope for Search API
- Pipeline used to transform to JSON
- Pipeline are simple plugs
- Plug for CORS
Phoenix controller
defmodule XSearchApi.SearchController do
use XSearchApi.Web, :controller
def category_search_count(conn, params) do
valid_api_params = get_valid_api_params(params)
safe_result_link = get_safe_result_link(conn, params)
get_response(conn, valid_api_params, safe_result_link)
end
defp get_response(conn, {:ok, valid_api_params}, {:ok, safe_result_link}) do
{_status, response} = XSearchApi.SAPI.get(
"records/record", [], [params: valid_api_params])
conn
|> put_status(response.status_code)
|> json(handle_response(response, safe_result_link))
end
...
- Easy to read
- Public action 'def'
- 'defp' private function
HTTPoison
- Http client library
- Use it to call SAPI
- 'use HTTPoison.base'
- use a module in the current context
defmodule XSearchApi.SAPI do
use HTTPoison.Base
def get(_url, [], [{:params, params}]) when is_nil(params), do:
{:error, %XSearchApi.Error { reason: "Bad request", status_code: 400 }}
def get(url, [], [{:params, params}]) do
{status, response} = request(:get, url, "", [], [{:params, params}])
set_response(status, response)
end
...
Tough challenges
- Build simple odata parser
- Use odata to build a link to search result page
@year_fields ["YearOfBirth", "YearOfDeath"]
@year_offset "_offset"
def append_year_offset([[year_field, :eq, value]|tail])
when year_field in @year_fields do
exact_year_phrase = [year_field, :eq, value]
Enum.concat(
[exact_year_phrase, :and, ["#{year_field}#{@year_offset}", :eq, "0"]],
append_year_offset(tail))
end
Cool piece of code :D
Deployment
- Team city - run mix commands
- Flightplan
- Upstart part of Ubuntu
- Handle tasks and services
- Stop and start anywhere
more on upstart 🔗
Hex package
# Register
mix hex.user register
# Publish package
mix hex.publish
ExUnit & Mocks
- ExUnit package for unit testing
- Mock package
defmodule XSearchApi.SearchControllerTest do
use ExUnit.Case
use Phoenix.ConnTest
import Mock
test "should pass odata query to sapi ..."
do
with_mock XSearchApi.SAPI,
[get: fn() -> {:ok, %XSearchApi.Response{ ... } end]
do
conn_response = SearchController
.category_search_count(
conn, %{"$filter" => "LastName eq Kotze"}
)
assert called SAPI.get("/record", [params: %{"$filter" => "LastName eq 'Kotze'"}])
assert conn_response.status == 200
end
end
...
White_bread
- BDD tool for running acceptance tests
- Based on cucumber
- Parses Gherkin formatted text
more on white_bread 🔗
given_ ~r/^I am a third party consumer$/, fn state ->
{:ok, state}
end
when_ ~r/I make a request to the API .../, fn state, %{first_name: first_name} ->
encoded_json = get(
put_req_header(conn(), "partnerid", "0123"),
"/category/count?$filter=FirstName eq #{first_name}")
{:ok, state |> Dict.put(:response, encoded_json)}
end
then_ ~r/I expect the response to be a link .../, fn state, %{link_first_name: first_name} ->
response = state |> Dict.get(:response)
decode_body = json_response(response, 200)
decoded_uri = decode_body["ResultUrl"]
assert decoded_uri == "#{@search_link_base}FirstName=#{first_name}"
{:ok, state}
end
eye_drops
- more on eye_drops 🔗
- It's a watch task
- Run commands based on file changes
- e.g. Run unit tests when '_test.exs' files change
- e.g. Run acceptance tests when '.feature' file changes
config :eye_drops,
tasks: [
%{
id: :unit_tests,
name: "Unit tests",
cmd: "ssh -F .ssh-config default mix test",
paths: ["web/*", "test/*"]
}
]
Vagrant tip
Questions?
Elixir X Search API
By Richard Kotze
Elixir X Search API
- 1,577