Interfacing With Erlang's Xmerl Library to Parse XML

The Record module lets us interface with Erlang records. The first argument to #defrecord is an atom representing the name of the erlang record, the second argument is the record definition in the xmerl library we are using.

defmodule WeatherParser do
  require Record
  Record.defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")
  Record.defrecord :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")

  def parse(xml, attrs) do
    xml
    |> :binary.bin_to_list
    |> :xmerl_scan.string
    |> extract_values(attrs, [])
  end

  def extract_values(xml, [], res), do: Enum.reverse(res)

  def extract_values({xml, _rest}, list, res), do: extract_values(xml, list, res)

  def extract_values(xml, [h | tail], res) do
    val = extract_element(xml, h)
          |> extract_element_content
          |> extract_content_value

    extract_values(xml, tail, [val | res])
  end

  def extract_element(xml, name), do: :xmerl_xpath.string('/current_observation/#{name}', xml)

  def extract_element_content([element]), do: xmlElement(element, :content)

  def extract_content_value([content_element]), do: xmlText(content_element, :value)
end

Using head / tail recursion and an accumulator, we are able to build up a list of the parsed results. Elixir’s pipe operator makes the transformation very clear. And just to be sure, some test cases to make sure everything is working smoothly:

defmodule WeatherParserTest do
  use ExUnit.Case

  test "returns the specified attributes" do
    data = WeatherParser.parse(sample_xml, [:location, :temp_f, :temp_c])
    assert data == ['Unknown Station', '39.2', '4.0']
  end

  def sample_xml do
  """
  <?xml version="1.0" encoding="ISO-8859-1"?>
  <?xml-stylesheet href="latest_ob.xsl" type="text/xsl"?>
  <current_observation version="1.0"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="http://www.weather.gov/view/current_observation.xsd">
    <credit>NOAA's National Weather Service</credit>
    <credit_URL>http://weather.gov/</credit_URL>
    <image>
      <url>http://weather.gov/images/xml_logo.gif</url>
      <title>NOAA's National Weather Service</title>
      <link>http://weather.gov</link>
    </image>
    <suggested_pickup>15 minutes after the hour</suggested_pickup>
    <suggested_pickup_period>60</suggested_pickup_period>
    <location>Unknown Station</location>
    <station_id>ALIA2</station_id>
    <observation_time>Last Updated on Feb 13 2016, 2:30 pm AST</observation_time>
          <observation_time_rfc822>Sat, 13 Feb 2016 14:30:00 -0400</observation_time_rfc822>
    <temperature_string>39.2 F (4.0 C)</temperature_string>
    <temp_f>39.2</temp_f>
    <temp_c>4.0</temp_c>
    <water_temp_f>40.3</water_temp_f>
    <water_temp_c>4.6</water_temp_c>
    <wind_string>Northeast at 9.2 MPH (7.97 KT)</wind_string>
    <wind_dir>Northeast</wind_dir>
    <wind_degrees>60</wind_degrees>
    <wind_mph>9.2</wind_mph>
    <wind_gust_mph>0.0</wind_gust_mph>
    <wind_kt>7.97</wind_kt>
    <pressure_string>988.5 mb</pressure_string>
    <pressure_mb>988.5</pressure_mb>
    <windchill_string>33 F (1 C)</windchill_string>
          <windchill_f>33</windchill_f>
          <windchill_c>1</windchill_c>
    <mean_wave_dir>Northeast</mean_wave_dir>
    <mean_wave_degrees></mean_wave_degrees>
    <disclaimer_url>http://weather.gov/disclaimer.html</disclaimer_url>
    <copyright_url>http://weather.gov/disclaimer.html</copyright_url>
    <privacy_policy_url>http://weather.gov/notice.html</privacy_policy_url>
  </current_observation>
  """
  end
end