Here’s the test we added to the handler_test.exs file.
test "GET /api/bears" do
request = """
GET /api/bears HTTP/1.1\r
Host: example.com\r
User-Agent: ExampleBrowser/1.0\r
Accept: */*\r
\r
"""
response = handle(request)
expected_response = """
HTTP/1.1 200 OK\r
Content-Type: application/json\r
Content-Length: 605\r
\r
[{"type":"Brown","name":"Teddy","id":1,"hibernating":true},
{"type":"Black","name":"Smokey","id":2,"hibernating":false},
{"type":"Brown","name":"Paddington","id":3,"hibernating":false},
{"type":"Grizzly","name":"Scarface","id":4,"hibernating":true},
{"type":"Polar","name":"Snow","id":5,"hibernating":false},
{"type":"Grizzly","name":"Brutus","id":6,"hibernating":false},
{"type":"Black","name":"Rosie","id":7,"hibernating":true},
{"type":"Panda","name":"Roscoe","id":8,"hibernating":false},
{"type":"Polar","name":"Iceman","id":9,"hibernating":true},
{"type":"Grizzly","name":"Kenai","id":10,"hibernating":false}]
"""
assert remove_whitespace(response) == remove_whitespace(expected_response)
endRendering JSON in Elixir
Silencing Poison Warnings
To silence any warnings related to the poison library, update the version to 5.0. First, change the version in the mix.exs file:
defp deps do
[{:poison, "~> 5.0"}]
endThen install the dependencies:
mix deps.get
Organizing Code
You can organize your Elixir project code however you like under the lib directory. For example, you could create a lib/controllers directory for all controller files.
Tip: Dependency Version Format
Here’s how to specify multiple dependencies in mix.exs. For example, to add both poison and faker libraries:
defp deps do
[{:poison, "~> 5.0"}, {:faker, "~> 0.8.0"}]
endEach tuple in the list consists of a package name (an atom) and the version string.
So how do you remember what the version format means? If you’re ever in doubt, type h Version in an iex session to see a list of examples and explanations.
Exercise: Support Multiple Response Headers
In the video, we supported different content types by adding the resp_content_type field to the Conv struct. Instead of storing just one header, you can store response headers in a map like so:
resp_headers: %{"Content-Type" => "text/html"}Now update the Handler.format_response function to use the map’s "Content-Type" value:
Content-Type: #{conv.resp_headers["Content-Type"]}\rSetting Response Headers
Define a helper function, put_resp_content_type/2, that updates the content type:
def put_resp_content_type(conv, content_type) do
headers = Map.put(conv.resp_headers, "Content-Type", content_type)
%{ conv | resp_headers: headers }
endYou can call this helper in the controller like so:
def index(conv) do
json = Servy.Wildthings.list_bears() |> Poison.encode!
conv = put_resp_content_type(conv, "application/json")
%{ conv | status: 200, resp_body: json }
endExercise: Handle POSTed JSON Data
To parse JSON in a POST request, use Poison.Parser.parse!. Here’s an example:
iex> json = ~s({"name": "Breezly", "type": "Polar"})
"{"name": "Breezly", "type": "Polar"}"
iex> Poison.Parser.parse!(json, %{})
%{"name" => "Breezly", "type" => "Polar"}Write the code in your API controller to handle the incoming POST request and parse the JSON data. Define the action:
def create(conv) do
params = Poison.Parser.parse!(conv.resp_body, %{})
%{ conv | status: 201, resp_body: "Created a #{params["type"]} bear named #{params["name"]}!" }
endExercise: Read Bears from a JSON File
If you’d like more practice, move the bear data to a JSON file and use Poison.decode! to load it:
Poison.decode!(json, as: %{"bears" => [%Bear{}]})Save the bear data in a JSON file, read it in your Wildthings module, and decode it into Bear structs.
Exercise: Serve Markdown Pages
You can add support for Markdown pages using a library like earmark. First, add the dependency:
defp deps do
[{:poison, "~> 3.1"}, {:earmark, "~> 1.4"}]
endDownload the dependency:
mix deps.get
Next, create a route that matches requests for pages, reads the corresponding Markdown file, and converts it to HTML:
def route(%Conv{method: "GET", path: "/pages/" <> name} = conv) do
@pages_path
|> Path.join("#{name}.md")
|> File.read
|> handle_file(conv)
|> markdown_to_html
end
def markdown_to_html(%Conv{status: 200} = conv) do
%{ conv | resp_body: Earmark.as_html!(conv.resp_body) }
end
def markdown_to_html(%Conv{} = conv), do: convCode So Far
The code for this section is in the external-library directory found within the video-code directory of the code bundle
Why always me?