parser improvement

parser improvement

defmodule Pan.Parser.Analyzer do
  alias Pan.Parser.Iterator
  alias Pan.Parser.Helpers
  require Logger

  defdelegate dm(left, right), to: Pan.Parser.Helpers, as: :deep_merge

#wrappers to dive into
  def call(map, "tag", [:rss,     _, value]), do: Iterator.parse(map, "tag", value)
  def call(map, "tag", [:channel, _, value]), do: Iterator.parse(map, "tag", value)

# simple tags to include in podcast
  def call(_, "tag", [:title,             _, []]), do: %{}
  def call(_, "tag", [:title,             _, [value]]), do: %{title: value}
  def call(_, "tag", [:"itunes:author",   _, []]), do: %{}
  def call(_, "tag", [:author,            _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"googleplay:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"itunes:author",   _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"itunes:summary",  _, []]),      do: %{}
  def call(_, "tag", [:"itunes:summary",  _, [value | _]]), do: %{summary: value}
  def call(_, "tag", [:link,              _, []]), do: %{}
  def call(_, "tag", [:link,              _, [value]]), do: %{website: value}
  def call(_, "tag", [:"itunes:explicit", _, [value]]), do: %{explicit: Helpers.boolify(value)}
  def call(_, "tag", [:explicit,          _, [value]]), do: %{explicit: Helpers.boolify(value)}
  def call(_, "tag", [:lastBuildDate,     _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "tag", [:"dc:date", _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "tag", [:pubDate, _, []]), do: %{}
  def call(_, "tag", [:pubDate, _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end

# image with fallback to itunes:image
  def call(map, "tag", [:image, _, value]), do: Iterator.parse(map, "image", value)
  def call(_, "image", [:title, _, _]), do: %{}
  def call(_, "image", [:title, _, [value]]), do: %{image_title: value}
  def call(_, "image", [:url,   _, []]), do: %{}
  def call(_, "image", [:url,   _, [value]]), do: %{image_url: value}
  def call(map, "image", [:link,        _, _]), do: map
  def call(map, "image", [:description, _, _]), do: map
  def call(map, "image", [:width,       _, _]), do: map
  def call(map, "image", [:height,      _, _]), do: map

  def call(map, "image", [:"itunes:image", attr, _]) do
    if map[:image_url], do: map,
                        else: %{image_url: attr[:href],
                                image_title: attr[:href]}
  end

  def call(map, "tag", [:"itunes:image", attr, _]) do
    if map[:image_url], do: map,
                        else: %{image_url: attr[:href],
                                image_title: attr[:href]}
  end

  def call(map, "tag", [:"itunes:image", _, [value]]) do
    if map[:image_url], do: map,
                        else: %{image_url: value}
  end

# Description with fallback to itunes:subtitle
  def call(_, "tag", [:description, _, []]), do: %{}
  def call(_, "tag", [:description, _, [value]]), do: %{description: value}

  def call(_, "tag", [:"itunes:subtitle", _, []]), do: %{}
  def call(map, "tag", [:"itunes:subtitle", _, [value]]) do
    if map[:description], do: %{},
                          else: %{description: value}
  end

# simple tags to include into nested structure
  def call(_, "tag", [:generator, _, []]), do: %{}
  def call(_, "tag", [:generator, _, [value]]), do: %{feed: %{feed_generator: value}}

# the links are a mixture of the two above
  def call(_, "tag", [:"atom:link", attr, _]) do
    case attr[:rel] do
      "self"      -> %{feed: %{self_link_title: attr[:title],
                            self_link_url: attr[:href]}}
      "current"   -> %{feed: %{self_link_title: attr[:title],
                            self_link_url: attr[:href]}}
      "next"      -> %{feed: %{next_page_url: attr[:href]}}
      "prev"      -> %{feed: %{prev_page_url: attr[:href]}}
      "prev-archive" -> %{feed: %{prev_page_url: attr[:href]}}
      "previous"  -> %{feed: %{prev_page_url: attr[:href]}}
      "first"     -> %{feed: %{first_page_url: attr[:href]}}
      "last"      -> %{feed: %{last_page_url: attr[:href]}}
      "hub"       -> %{feed: %{hub_link_url: attr[:href]}}
      "search"    -> %{}
      "related"   -> %{}
      "via"       -> %{}
      "alternate" ->
        uuid = String.to_atom(UUID.uuid1())
        alternate_feed_map = %{uuid => %{title: attr[:title], url: attr[:href]}}
        %{feed: %{alternate_feeds: alternate_feed_map}}
      "payment" -> %{payment_link_title: attr[:title],
                     payment_link_url: String.slice(attr[:href], 0, 255)}
    end
  end

  def call(_, "tag", [:"rawvoice:donate", attr, [value]]), do: %{payment_link_title: value,
                                                                 payment_link_url: attr[:href]}
  def call(_, "tag", [:"rawvoice:donate", attr, []]), do: %{payment_link_title: attr[:href],
                                                            payment_link_url: attr[:href]}

  def call(_, "tag", [:"atom10:link", attr, _]) do
    case attr[:rel] do
      "self"  -> %{feed: %{self_link_title: attr[:title],
                           self_link_url:   attr[:href]}}
      "hub" -> %{}
    end
  end

# tags to ignore
  def call(map, "tag", [tag_atom, _, _]) when tag_atom in [
    :"feedpress:locale", :"fyyd:verify", :"itunes:block", :"itunes:keywords", :"media:thumbnail",
    :"media:keywords", :"media:category", :category, :site, :docs, :"feedburner:info", :logo,
    :"media:credit", :"media:copyright", :"media:rating", :"media:description", :"copyright",
    :"feedburner:feedFlare", :"geo:lat", :"geo:long", :"creativeCommons:license", :"clipper:id",
    :"feedburner:emailServiceId", :"feedburner:feedburnerHostname", :managingEditor,
    :"sy:updatePeriod", :"sy:updateFrequency", :"wfw:commentRss", :"rawvoice:subscribe",
    :webMaster, :ttl, :"itunes:new-feed-url", :"googleplay:description", :"googleplay:email",
    :"googleplay:category", :"rawvoice:rating", :"rawvoice:location", :"rawvoice:frequency",
    :"ppg:seriesDetails", :"ppg:systemRef", :"ppg:network", :cloud, :"googleplay:image",
    :"googleplay:author", :"googleplay:explicit", :feed, :webmaster, :ilink, :ffmpeg, :domain,
    :lame, :broadcastlimit, :"itunes:link", :"channelExportDir", :"atom:id", :"sy:updateBase",
    :"openSearch:totalResults", :"openSearch:startIndex", :"openSearch:itemsPerPage", :"html",
    :"managingeditor", :"ard:programInformation", :"dc:creator", :"itunes:complete", :feedType,
    :changefreq, :"dc:title", :"feedburner:browserFriendly", :"itunesowner", :textInput,
    :"podcastRF:originStation", :"itunes:explicit", :meta, :"dc:rights", :skipDays, :a, :p, :br, :b,
    :"sc:totalAvailable", :skipHours, :keywords, :script, :"googleplay:block", :guid,
    :"manageEditor", :"itunes:name", :"amp:logo", :"itunes:catago", :"xhtml:meta", :"avms:id",
    :"blogChannel:blogRoll", :"blogChannel:blink", :"thespringbox:skin", :"admin:generatorAgent",
    :"feedpress:podcastId", :summary, :rating, :Category, :"amp:background", :"amp:banner",
    :"amp:halfBanner", :"amp:networkLogo", :"amp:networkSmallLogo", :"amp:networkHalfBanner",
    :"amp:networkBackground", :"amp:networkWebsite", :artwork, :"amp:showFeaturedLogo",
    :"amp:tracking", :"itunes:subitle", :"feedpress:newsletterId", :"blogger:adultContent"
  ], do: map

  def call(_, "episode", [tag_atom, _, _]) when tag_atom in [
    :"googleplay:description", :"googleplay:image", :"googleplay:explicit", :"googleplay:block",
    :"frn:id", :"frn:title", :"frn:language", :"frn:art", :"frn:radio", :"frn:serie", :"frn:laenge",
    :"frn:licence", :"frn:last_update", :"itunes:keywords", :"post-id", :author, :"itunes:explicit",
    :category, :"dc:creator", :comments, :"feedburner:origLink", :"itunes:image", :"dc:modifieddate",
    :"feedburner:origEnclosureLink", :"wfw:commentRss", :"slash:comments", :"itunes:block", :meta,
    :"itunes:order", :"ppg:canonical", :"cba:productionDate", :"cba:broadcastDate", :payment,
    :"cba:containsCopyright", :"media:thumbnail", :image, :source, :"media:description", :programid,
    :poddid, :"dcterms:modified", :"dcterms:created", :toPubDate, :audioId, :"atom:updated",
    :"thr:total", :"ard:visibility", :"series:name", :"rawvoice:poster", :"georss:point",
    :"copyright", :"ard:programInformation", :"sc:chapters", :"xhtml:body", :"itunesu:category",
    :"wfw:content", :"wfw:comment", :"creativeCommons:license", :"image_link", :itemDate, :timestamp,
    :"media:keywords", :"media:rights", :"ppg:enclosureLegacy", :"ppg:enclosureSecure",
    :"podcastRF:businessReference", :"podcastRF:magnetothequeID", :"podcastRF:stepID", :explicit,
    :"media:title",:"media:credit", :"dc:subject", :"dc:identifier", :"georss:featurename",
    :"georss:box", :"gd:extendedProperty", :"media:content", :"rawvoice:metamark", :"media:player",
    :"itunes:category", :"fyyd:episodeID", :"fyyd:podcastID", :"fyyd:origPubdate", :"geo:lat",
    :"geo:long", :"rawvoice:isHD", :"podcast:type", :"podcast:description", :"media:rating",
    :"podfm:nodownload", :"podfm:downloadCount", :script, :"rte-days", :"rawvoice:embed",
    :"lastBuildDate", :"merriam:shortdef", :"dc:title", :div, :"rawvoice:webm", :"subTitleLink",
    :"app:edited", :"media:text", :"ecc:description", :guide, :"dc:description", :"itunes:keyword",
    :"media:group", :"rawvoice:donate", :"podcast:title", :"media:copyright", :"dc:type",
    :"itunes:length", :"podcast:name", :"blip:user", :"username", :"dc:copyright", :"pingback:server",
    :"pingback:target", :"trackback:ping", :filename, :"blip:userid", :"blip:safeusername",
    :"blip:showpath", :"blip:show", :"blip:showpage", :"blip:picture", :"blip:posts_id",
    :"blip:item_id", :"blip:item_type", :"blip:rating", :"blip:datestamp", :"blip:language",
    :"blip:adChannel", :"blip:categories", :"blip:license", :"blip:puredescription", :"dc:rights",
    :"blip:thumbnail_src", :"blip:", :"blip:embedUrl", :"blip:embedLookup", :"blip:runtime",
    :"blip:adminRating", :"blip:core_value", :"blip:core", :"blip:recommendable", :"avms:id",
    :"blip:recommendations", :"yv:adInfo", :"blip:smallThumbnail", :"clipper:id", :"a10:link",
    :"uzhfeeds:image", :"amp:banner", :"itunes:isClosedCaptioned", :"blip:poster_image",
    :"georss:where", :"itunes:subitle", :"media:category", :"geourl:latitude", :"geourl:longitude",
    :"icbm:latitude", :"icbm:longitude", :"itunes:owner", :"jwplayer:image", :"flickr:date_taken",
    :"dc:date.Taken"
  ], do: %{}

  def call(_, "image", [tag_atom, _, _]) when tag_atom in [
    :guid, :meta
  ], do: %{}

# We expect several language tags
  def call(_, "tag", [:language, _, _]), do: %{}
  def call(_, "tag", [tag_atom, _, [value]]) when tag_atom in [:language, :"dc:language"] do
    uuid = String.to_atom(UUID.uuid1())
    %{languages: %{uuid => %{shortcode: value}}}
  end

# We expect one owner
  def call(map, "tag", [:"itunes:owner",    _, value]), do: Iterator.parse(map, "owner", value)
  def call(map, "tag", [:owner,             _, value]), do: Iterator.parse(map, "owner", value)
  def call(map, "tag", [:"itunes:email",    _, value]), do: Iterator.parse(map, "owner", value)
  def call(_, "owner", [:"itunes:name",     _, []]), do: %{}
  def call(_, "owner", [:"itunes:name",     _, [value]]), do: %{name: String.slice(value, 0, 255)}
  def call(_, "owner", [:name,              _, [value]]), do: %{name: String.slice(value, 0, 255)}
  def call(_, "owner", [:"itunes:email",    _, []]), do: %{}
  def call(_, "owner", [:"itunes:email",    _, [value]]), do: %{email: value}
  def call(_, "owner", [:email,             _, [value]]), do: %{email: value}
# FIXME: HAS20170130 shwould be pulled out of itunes owner
  def call(_, "owner", [:"panoptikum:pid",  _, [value]]), do: %{pid: value}

# Parsing categories infintely deep
  def call(_, "tag", [:"itunes:category", attr, []]) do
    %{categories: %{UUID.uuid1() => %{title: attr[:text], parent: nil}}}
  end
  def call(_, "tag", [:"itunes:category", [], [value]]) do
    %{categories: %{UUID.uuid1() => %{title: value, parent: nil}}}
  end
  def call(_, "tag", [:"itunes:category", attr, value]) do
    Iterator.parse(%{categories: %{UUID.uuid1() => %{title: attr[:text], parent: nil}}}, "category", value, attr[:text])
  end

  def call("category", [:"itunes:category", attr, []], parent_title) do
    %{categories: %{UUID.uuid1() => %{title: attr[:text], parent: parent_title}}}
  end

  def call("category", [:"itunes:category", attr, value], parent_title) do
    Iterator.parse(%{categories: %{UUID.uuid1() => %{title: attr[:text], parent: parent_title}}}, "category", value, attr[:text])
  end

# Episodes
  def call(map, "tag", [:item, _, value]), do: Iterator.parse(map, "episode", value, UUID.uuid1())
  def call(map, "episode", [:item, _, value]), do: Iterator.parse(map, "episode", value, UUID.uuid1())

  def call(_, "episode", [:title, _, []]), do: %{title: "emtpy"}
  def call(_, "episode", [:title, _, [value | _]]), do: %{title: String.slice(value, 0, 255)}
  def call(_, "episode", [:"itunes:title", _, []]), do: %{title: "emtpy"}
  def call(_, "episode", [:"itunes:title", _, [value | _]]), do: %{title: String.slice(value, 0, 255)}

  def call(_, "episode", [:link, _, []]), do: %{}
  def call(_, "episode", [:link, _, [value]]), do: %{link:        String.slice(value, 0, 255)}
  def call(_, "episode", [:guid, _, [value]]), do: %{guid:        String.slice(value, 0, 255)}
  def call(_, "episode", [:guid, _, _]), do: %{}
  def call(_, "episode", [:contentId, _, [value]]), do: %{guid:   String.slice(value, 0, 255)}

  def call(_, "episode", [:description, _, []]), do: %{}
  def call(_, "episode", [:description, _, [value | _]]), do: %{description: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:"itunes:description", _, []]), do: %{}
  def call(_, "episode", [:"itunes:description", _, [value | _]]), do: %{description: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"content:encoded", _, []]), do: %{}
  def call(_, "episode", [:"content:encoded", _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:content, _, []]), do: %{}
  def call(_, "episode", [:content, _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:shownotes, _, []]), do: %{}
  def call(_, "episode", [:shownotes, _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"itunes:summary",  _, []]), do: %{}
  def call(_, "episode", [:"itunes:summary",  _, [value | _]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:summary,           _, [value]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:"atom:summary",  _, []]), do: %{}
  def call(_, "episode", [:"atom:summary",  _, [value | _]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"itunes:subtitle", _, []]), do: %{}
  def call(_, "episode", [:"itunes:subtitle", _, [value]]), do: %{subtitle: String.slice(value, 0, 255)}
  def call(_, "episode", [:subtitle,          _, [value]]), do: %{subtitle: String.slice(value, 0, 255)}
  def call(_, "episode", [:"itunes:subtitle", _, [value | _]]), do: %{subtitle: String.slice(value, 0, 255)}

  def call(_, "episode", [:"itunes:author", _, []]), do: %{}
  def call(_, "episode", [:"itunes:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "episode", [:"googleplay:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "episode", [:"dc:publisher", _, [value]]), do: %{author: String.slice(value, 0, 255)}

  def call(_, "episode", [:"itunes:duration", _, []]), do: %{}
  def call(_, "episode", [:"itunes:duration", _, [value]]), do: %{duration: value}
  def call(_, "episode", [:duration, _, []]), do: %{}
  def call(_, "episode", [:duration, _, [value]]), do: %{duration: value}

  def call(_, "episode", [:pubDate,           _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "episode", [:pubDate, _, []]) do
    %{publishing_date: Timex.now()}
  end
  def call(_, "episode", [:"dc:date", _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "episode", [:pubDateShort, _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end

  def call(_, "episode", [:"atom:link", attr, _]) do
    case attr[:rel] do
      "http://podlove.org/deep-link" ->
        %{deep_link: String.slice(attr[:href], 0, 255)}
      "payment" ->
        %{payment_link_title: attr[:title],
          payment_link_url: String.slice(attr[:href], 0, 255)}
      "alternate" ->
        %{}
    end
  end

# Enclosures a.k.a. Audiofiles
  def call(_, "episode", [:enclosure, attr, _]) do
    enclosure_map = %{url:    String.slice(attr[:url], 0, 255),
                      length: String.slice(attr[:length], 0, 255),
                      type:   String.slice(attr[:type], 0, 255),
                      guid:   String.slice(attr[:"bitlove:guid"], 0, 255)}
    uuid = String.to_atom(UUID.uuid1())
    %{enclosures: %{uuid => enclosure_map}}
  end

# Chapters
  def call(_, "episode", [tag_atom, _, value]) when tag_atom in [:"psc:chapters", :chapters] do
    Iterator.parse(%{}, "chapter", value)
  end

# We expect several contributors
  def call(map, "tag", [:"atom:contributor", _, value]) do
    Iterator.parse(map, "contributor", value, UUID.uuid1())
  end

# Episode contributors
  def call(_, "episode", [:"atom:contributor", _, value]) do
    contributor_uuid = String.to_atom(UUID.uuid1())
    Iterator.parse(%{contributors: %{contributor_uuid => %{}}}, "episode-contributor", value, contributor_uuid)
  end
  def call(_, "episode", [:"dc:contributor", _, [value]]) do
    contributor_uuid = String.to_atom(UUID.uuid1())
    %{contributors: %{contributor_uuid => %{name: value, uri: value}}}
  end

# Show debugging information for unknown tags on console
  def call(_, mode, [tag, attr, value]) do
    Logger.warn "=== Tag unknown: ==="
    Logger.warn "Mode: #{mode}"
    Logger.warn ~s(Tag: :"#{tag}")
    Logger.warn "Attr: #{inspect attr}"
    Logger.warn "Value: #{inspect value}"
    {:error, "tag unknown"}
  end

# Now the namespaces:
  def call("chapter", [tag_atom, attr, _]) when tag_atom in [:"psc:chapter", :chapter] do
    chapter_uuid = String.to_atom(UUID.uuid1())
    %{chapter_uuid => %{start: attr[:start], title: String.slice(attr[:title], 0, 255)}}
  end

  def call("contributor", [:"atom:name",       _, [value]]), do: %{name: value}
  def call("contributor", [:"atom:uri",        _, [value]]), do: %{uri:  value}
  def call("contributor", [:"panoptikum:pid",  _, [value]]), do: %{pid:  value}

  def call("episode-contributor", [:"atom:name",       _, [value]]), do: %{name:  value}
  def call("episode-contributor", [:"atom:uri",        _, [value]]), do: %{uri:   value}
  def call("episode-contributor", [:"atom:email",      _, [value]]), do: %{email: value}
# FIXME: HAS20170130 should be pulled out of itunes owner
  def call("episode-contributor", [:"panoptikum:pid",  _, [value]]), do: %{pid:   value}
end

defmodule Pan.Parser.Analyzer do
  alias Pan.Parser.Iterator
  alias Pan.Parser.Helpers
  require Logger

  defdelegate dm(left, right), to: Pan.Parser.Helpers, as: :deep_merge

#wrappers to dive into
  def call(map, "tag", [:rss,     _, value]), do: Iterator.parse(map, "tag", value)
  def call(map, "tag", [:channel, _, value]), do: Iterator.parse(map, "tag", value)

# simple tags to include in podcast
  def call(_, "tag", [:title,             _, []]), do: %{}
  def call(_, "tag", [:title,             _, [value]]), do: %{title: value}
  def call(_, "tag", [:"itunes:author",   _, []]), do: %{}
  def call(_, "tag", [:author,            _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"googleplay:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"itunes:author",   _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "tag", [:"itunes:summary",  _, []]),      do: %{}
  def call(_, "tag", [:"itunes:summary",  _, [value | _]]), do: %{summary: value}
  def call(_, "tag", [:link,              _, []]), do: %{}
  def call(_, "tag", [:link,              _, [value]]), do: %{website: value}
  def call(_, "tag", [:"itunes:explicit", _, [value]]), do: %{explicit: Helpers.boolify(value)}
  def call(_, "tag", [:explicit,          _, [value]]), do: %{explicit: Helpers.boolify(value)}
  def call(_, "tag", [:lastBuildDate,     _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "tag", [:"dc:date", _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "tag", [:pubDate, _, []]), do: %{}
  def call(_, "tag", [:pubDate, _, [value]]) do
    %{last_build_date: Helpers.to_naive_datetime(value)}
  end

# image with fallback to itunes:image
  def call(map, "tag", [:image, _, value]), do: Iterator.parse(map, "image", value)
  def call(_, "image", [:title, _, _]), do: %{}
  def call(_, "image", [:title, _, [value]]), do: %{image_title: value}
  def call(_, "image", [:url,   _, []]), do: %{}
  def call(_, "image", [:url,   _, [value]]), do: %{image_url: value}
  def call(map, "image", [:link,        _, _]), do: map
  def call(map, "image", [:description, _, _]), do: map
  def call(map, "image", [:width,       _, _]), do: map
  def call(map, "image", [:height,      _, _]), do: map

  def call(map, "image", [:"itunes:image", attr, _]) do
    if map[:image_url], do: map,
                        else: %{image_url: attr[:href],
                                image_title: attr[:href]}
  end

  def call(map, "tag", [:"itunes:image", attr, _]) do
    if map[:image_url], do: map,
                        else: %{image_url: attr[:href],
                                image_title: attr[:href]}
  end

  def call(map, "tag", [:"itunes:image", _, [value]]) do
    if map[:image_url], do: map,
                        else: %{image_url: value}
  end

# Description with fallback to itunes:subtitle
  def call(_, "tag", [:description, _, []]), do: %{}
  def call(_, "tag", [:description, _, [value]]), do: %{description: value}

  def call(_, "tag", [:"itunes:subtitle", _, []]), do: %{}
  def call(map, "tag", [:"itunes:subtitle", _, [value]]) do
    if map[:description], do: %{},
                          else: %{description: value}
  end

# simple tags to include into nested structure
  def call(_, "tag", [:generator, _, []]), do: %{}
  def call(_, "tag", [:generator, _, [value]]), do: %{feed: %{feed_generator: value}}

# the links are a mixture of the two above
  def call(_, "tag", [:"atom:link", attr, _]) do
    case attr[:rel] do
      "self"      -> %{feed: %{self_link_title: attr[:title],
                            self_link_url: attr[:href]}}
      "current"   -> %{feed: %{self_link_title: attr[:title],
                            self_link_url: attr[:href]}}
      "next"      -> %{feed: %{next_page_url: attr[:href]}}
      "prev"      -> %{feed: %{prev_page_url: attr[:href]}}
      "prev-archive" -> %{feed: %{prev_page_url: attr[:href]}}
      "previous"  -> %{feed: %{prev_page_url: attr[:href]}}
      "first"     -> %{feed: %{first_page_url: attr[:href]}}
      "last"      -> %{feed: %{last_page_url: attr[:href]}}
      "hub"       -> %{feed: %{hub_link_url: attr[:href]}}
      "search"    -> %{}
      "related"   -> %{}
      "via"       -> %{}
      "alternate" ->
        uuid = String.to_atom(UUID.uuid1())
        alternate_feed_map = %{uuid => %{title: attr[:title], url: attr[:href]}}
        %{feed: %{alternate_feeds: alternate_feed_map}}
      "payment" -> %{payment_link_title: attr[:title],
                     payment_link_url: String.slice(attr[:href], 0, 255)}
    end
  end

  def call(_, "tag", [:"rawvoice:donate", attr, [value]]), do: %{payment_link_title: value,
                                                                 payment_link_url: attr[:href]}
  def call(_, "tag", [:"rawvoice:donate", attr, []]), do: %{payment_link_title: attr[:href],
                                                            payment_link_url: attr[:href]}

  def call(_, "tag", [:"atom10:link", attr, _]) do
    case attr[:rel] do
      "self"  -> %{feed: %{self_link_title: attr[:title],
                           self_link_url:   attr[:href]}}
      "hub" -> %{}
    end
  end

# tags to ignore
  def call(map, "tag", [tag_atom, _, _]) when tag_atom in [
    :"feedpress:locale", :"fyyd:verify", :"itunes:block", :"itunes:keywords", :"media:thumbnail",
    :"media:keywords", :"media:category", :category, :site, :docs, :"feedburner:info", :logo,
    :"media:credit", :"media:copyright", :"media:rating", :"media:description", :"copyright",
    :"feedburner:feedFlare", :"geo:lat", :"geo:long", :"creativeCommons:license", :"clipper:id",
    :"feedburner:emailServiceId", :"feedburner:feedburnerHostname", :managingEditor,
    :"sy:updatePeriod", :"sy:updateFrequency", :"wfw:commentRss", :"rawvoice:subscribe",
    :webMaster, :ttl, :"itunes:new-feed-url", :"googleplay:description", :"googleplay:email",
    :"googleplay:category", :"rawvoice:rating", :"rawvoice:location", :"rawvoice:frequency",
    :"ppg:seriesDetails", :"ppg:systemRef", :"ppg:network", :cloud, :"googleplay:image",
    :"googleplay:author", :"googleplay:explicit", :feed, :webmaster, :ilink, :ffmpeg, :domain,
    :lame, :broadcastlimit, :"itunes:link", :"channelExportDir", :"atom:id", :"sy:updateBase",
    :"openSearch:totalResults", :"openSearch:startIndex", :"openSearch:itemsPerPage", :"html",
    :"managingeditor", :"ard:programInformation", :"dc:creator", :"itunes:complete", :feedType,
    :changefreq, :"dc:title", :"feedburner:browserFriendly", :"itunesowner", :textInput,
    :"podcastRF:originStation", :"itunes:explicit", :meta, :"dc:rights", :skipDays, :a, :p, :br, :b,
    :"sc:totalAvailable", :skipHours, :keywords, :script, :"googleplay:block", :guid,
    :"manageEditor", :"itunes:name", :"amp:logo", :"itunes:catago", :"xhtml:meta", :"avms:id",
    :"blogChannel:blogRoll", :"blogChannel:blink", :"thespringbox:skin", :"admin:generatorAgent",
    :"feedpress:podcastId", :summary, :rating, :Category, :"amp:background", :"amp:banner",
    :"amp:halfBanner", :"amp:networkLogo", :"amp:networkSmallLogo", :"amp:networkHalfBanner",
    :"amp:networkBackground", :"amp:networkWebsite", :artwork, :"amp:showFeaturedLogo",
    :"amp:tracking", :"itunes:subitle", :"feedpress:newsletterId"
  ], do: map

  def call(_, "episode", [tag_atom, _, _]) when tag_atom in [
    :"googleplay:description", :"googleplay:image", :"googleplay:explicit", :"googleplay:block",
    :"frn:id", :"frn:title", :"frn:language", :"frn:art", :"frn:radio", :"frn:serie", :"frn:laenge",
    :"frn:licence", :"frn:last_update", :"itunes:keywords", :"post-id", :author, :"itunes:explicit",
    :category, :"dc:creator", :comments, :"feedburner:origLink", :"itunes:image", :"dc:modifieddate",
    :"feedburner:origEnclosureLink", :"wfw:commentRss", :"slash:comments", :"itunes:block", :meta,
    :"itunes:order", :"ppg:canonical", :"cba:productionDate", :"cba:broadcastDate", :payment,
    :"cba:containsCopyright", :"media:thumbnail", :image, :source, :"media:description", :programid,
    :poddid, :"dcterms:modified", :"dcterms:created", :toPubDate, :audioId, :"atom:updated",
    :"thr:total", :"ard:visibility", :"series:name", :"rawvoice:poster", :"georss:point",
    :"copyright", :"ard:programInformation", :"sc:chapters", :"xhtml:body", :"itunesu:category",
    :"wfw:content", :"wfw:comment", :"creativeCommons:license", :"image_link", :itemDate, :timestamp,
    :"media:keywords", :"media:rights", :"ppg:enclosureLegacy", :"ppg:enclosureSecure",
    :"podcastRF:businessReference", :"podcastRF:magnetothequeID", :"podcastRF:stepID", :explicit,
    :"media:title",:"media:credit", :"dc:subject", :"dc:identifier", :"georss:featurename",
    :"georss:box", :"gd:extendedProperty", :"media:content", :"rawvoice:metamark", :"media:player",
    :"itunes:category", :"fyyd:episodeID", :"fyyd:podcastID", :"fyyd:origPubdate", :"geo:lat",
    :"geo:long", :"rawvoice:isHD", :"podcast:type", :"podcast:description", :"media:rating",
    :"podfm:nodownload", :"podfm:downloadCount", :script, :"rte-days", :"rawvoice:embed",
    :"lastBuildDate", :"merriam:shortdef", :"dc:title", :div, :"rawvoice:webm", :"subTitleLink",
    :"app:edited", :"media:text", :"ecc:description", :guide, :"dc:description", :"itunes:keyword",
    :"media:group", :"rawvoice:donate", :"podcast:title", :"media:copyright", :"dc:type",
    :"itunes:length", :"podcast:name", :"blip:user", :"username", :"dc:copyright", :"pingback:server",
    :"pingback:target", :"trackback:ping", :filename, :"blip:userid", :"blip:safeusername",
    :"blip:showpath", :"blip:show", :"blip:showpage", :"blip:picture", :"blip:posts_id",
    :"blip:item_id", :"blip:item_type", :"blip:rating", :"blip:datestamp", :"blip:language",
    :"blip:adChannel", :"blip:categories", :"blip:license", :"blip:puredescription", :"dc:rights",
    :"blip:thumbnail_src", :"blip:", :"blip:embedUrl", :"blip:embedLookup", :"blip:runtime",
    :"blip:adminRating", :"blip:core_value", :"blip:core", :"blip:recommendable", :"avms:id",
    :"blip:recommendations", :"yv:adInfo", :"blip:smallThumbnail", :"clipper:id", :"a10:link",
    :"uzhfeeds:image", :"amp:banner", :"itunes:isClosedCaptioned", :"blip:poster_image",
    :"georss:where", :"itunes:subitle", :"media:category", :"geourl:latitude", :"geourl:longitude",
    :"icbm:latitude", :"icbm:longitude", :"itunes:owner", :"jwplayer:image", :"flickr:date_taken",
    :"dc:date.Taken"
  ], do: %{}

  def call(_, "image", [tag_atom, _, _]) when tag_atom in [
    :guid, :meta
  ], do: %{}

# We expect several language tags
  def call(_, "tag", [:language, _, _]), do: %{}
  def call(_, "tag", [tag_atom, _, [value]]) when tag_atom in [:language, :"dc:language"] do
    uuid = String.to_atom(UUID.uuid1())
    %{languages: %{uuid => %{shortcode: value}}}
  end

# We expect one owner
  def call(map, "tag", [:"itunes:owner",    _, value]), do: Iterator.parse(map, "owner", value)
  def call(map, "tag", [:owner,             _, value]), do: Iterator.parse(map, "owner", value)
  def call(map, "tag", [:"itunes:email",    _, value]), do: Iterator.parse(map, "owner", value)
  def call(_, "owner", [:"itunes:name",     _, []]), do: %{}
  def call(_, "owner", [:"itunes:name",     _, [value]]), do: %{name: String.slice(value, 0, 255)}
  def call(_, "owner", [:name,              _, [value]]), do: %{name: String.slice(value, 0, 255)}
  def call(_, "owner", [:"itunes:email",    _, []]), do: %{}
  def call(_, "owner", [:"itunes:email",    _, [value]]), do: %{email: value}
  def call(_, "owner", [:email,             _, [value]]), do: %{email: value}
# FIXME: HAS20170130 shwould be pulled out of itunes owner
  def call(_, "owner", [:"panoptikum:pid",  _, [value]]), do: %{pid: value}

# Parsing categories infintely deep
  def call(_, "tag", [:"itunes:category", attr, []]) do
    %{categories: %{UUID.uuid1() => %{title: attr[:text], parent: nil}}}
  end
  def call(_, "tag", [:"itunes:category", [], [value]]) do
    %{categories: %{UUID.uuid1() => %{title: value, parent: nil}}}
  end
  def call(_, "tag", [:"itunes:category", attr, value]) do
    Iterator.parse(%{categories: %{UUID.uuid1() => %{title: attr[:text], parent: nil}}}, "category", value, attr[:text])
  end

  def call("category", [:"itunes:category", attr, []], parent_title) do
    %{categories: %{UUID.uuid1() => %{title: attr[:text], parent: parent_title}}}
  end

  def call("category", [:"itunes:category", attr, value], parent_title) do
    Iterator.parse(%{categories: %{UUID.uuid1() => %{title: attr[:text], parent: parent_title}}}, "category", value, attr[:text])
  end

# Episodes
  def call(map, "tag", [:item, _, value]), do: Iterator.parse(map, "episode", value, UUID.uuid1())
  def call(map, "episode", [:item, _, value]), do: Iterator.parse(map, "episode", value, UUID.uuid1())

  def call(_, "episode", [:title, _, []]), do: %{title: "emtpy"}
  def call(_, "episode", [:title, _, [value | _]]), do: %{title: String.slice(value, 0, 255)}
  def call(_, "episode", [:"itunes:title", _, []]), do: %{title: "emtpy"}
  def call(_, "episode", [:"itunes:title", _, [value | _]]), do: %{title: String.slice(value, 0, 255)}

  def call(_, "episode", [:link, _, []]), do: %{}
  def call(_, "episode", [:link, _, [value]]), do: %{link:        String.slice(value, 0, 255)}
  def call(_, "episode", [:guid, _, [value]]), do: %{guid:        String.slice(value, 0, 255)}
  def call(_, "episode", [:guid, _, _]), do: %{}
  def call(_, "episode", [:contentId, _, [value]]), do: %{guid:   String.slice(value, 0, 255)}

  def call(_, "episode", [:description, _, []]), do: %{}
  def call(_, "episode", [:description, _, [value | _]]), do: %{description: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:"itunes:description", _, []]), do: %{}
  def call(_, "episode", [:"itunes:description", _, [value | _]]), do: %{description: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"content:encoded", _, []]), do: %{}
  def call(_, "episode", [:"content:encoded", _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:content, _, []]), do: %{}
  def call(_, "episode", [:content, _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:shownotes, _, []]), do: %{}
  def call(_, "episode", [:shownotes, _, [value]]), do: %{shownotes: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"itunes:summary",  _, []]), do: %{}
  def call(_, "episode", [:"itunes:summary",  _, [value | _]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:summary,           _, [value]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}
  def call(_, "episode", [:"atom:summary",  _, []]), do: %{}
  def call(_, "episode", [:"atom:summary",  _, [value | _]]), do: %{summary: HtmlSanitizeEx2.basic_html_reduced(value)}

  def call(_, "episode", [:"itunes:subtitle", _, []]), do: %{}
  def call(_, "episode", [:"itunes:subtitle", _, [value]]), do: %{subtitle: String.slice(value, 0, 255)}
  def call(_, "episode", [:subtitle,          _, [value]]), do: %{subtitle: String.slice(value, 0, 255)}
  def call(_, "episode", [:"itunes:subtitle", _, [value | _]]), do: %{subtitle: String.slice(value, 0, 255)}

  def call(_, "episode", [:"itunes:author", _, []]), do: %{}
  def call(_, "episode", [:"itunes:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "episode", [:"googleplay:author", _, [value]]), do: %{author: String.slice(value, 0, 255)}
  def call(_, "episode", [:"dc:publisher", _, [value]]), do: %{author: String.slice(value, 0, 255)}

  def call(_, "episode", [:"itunes:duration", _, []]), do: %{}
  def call(_, "episode", [:"itunes:duration", _, [value]]), do: %{duration: value}
  def call(_, "episode", [:duration, _, []]), do: %{}
  def call(_, "episode", [:duration, _, [value]]), do: %{duration: value}

  def call(_, "episode", [:pubDate,           _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "episode", [:pubDate, _, []]) do
    %{publishing_date: Timex.now()}
  end
  def call(_, "episode", [:"dc:date", _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end
  def call(_, "episode", [:pubDateShort, _, [value]]) do
    %{publishing_date: Helpers.to_naive_datetime(value)}
  end

  def call(_, "episode", [:"atom:link", attr, _]) do
    case attr[:rel] do
      "http://podlove.org/deep-link" ->
        %{deep_link: String.slice(attr[:href], 0, 255)}
      "payment" ->
        %{payment_link_title: attr[:title],
          payment_link_url: String.slice(attr[:href], 0, 255)}
      "alternate" ->
        %{}
    end
  end

# Enclosures a.k.a. Audiofiles
  def call(_, "episode", [:enclosure, attr, _]) do
    enclosure_map = %{url:    String.slice(attr[:url], 0, 255),
                      length: String.slice(attr[:length], 0, 255),
                      type:   String.slice(attr[:type], 0, 255),
                      guid:   String.slice(attr[:"bitlove:guid"], 0, 255)}
    uuid = String.to_atom(UUID.uuid1())
    %{enclosures: %{uuid => enclosure_map}}
  end

# Chapters
  def call(_, "episode", [tag_atom, _, value]) when tag_atom in [:"psc:chapters", :chapters] do
    Iterator.parse(%{}, "chapter", value)
  end

# We expect several contributors
  def call(map, "tag", [:"atom:contributor", _, value]) do
    Iterator.parse(map, "contributor", value, UUID.uuid1())
  end

# Episode contributors
  def call(_, "episode", [:"atom:contributor", _, value]) do
    contributor_uuid = String.to_atom(UUID.uuid1())
    Iterator.parse(%{contributors: %{contributor_uuid => %{}}}, "episode-contributor", value, contributor_uuid)
  end
  def call(_, "episode", [:"dc:contributor", _, [value]]) do
    contributor_uuid = String.to_atom(UUID.uuid1())
    %{contributors: %{contributor_uuid => %{name: value, uri: value}}}
  end

# Show debugging information for unknown tags on console
  def call(_, mode, [tag, attr, value]) do
    Logger.warn "=== Tag unknown: ==="
    Logger.warn "Mode: #{mode}"
    Logger.warn ~s(Tag: :"#{tag}")
    Logger.warn "Attr: #{inspect attr}"
    Logger.warn "Value: #{inspect value}"
    {:error, "tag unknown"}
  end

# Now the namespaces:
  def call("chapter", [tag_atom, attr, _]) when tag_atom in [:"psc:chapter", :chapter] do
    chapter_uuid = String.to_atom(UUID.uuid1())
    %{chapter_uuid => %{start: attr[:start], title: String.slice(attr[:title], 0, 255)}}
  end

  def call("contributor", [:"atom:name",       _, [value]]), do: %{name: value}
  def call("contributor", [:"atom:uri",        _, [value]]), do: %{uri:  value}
  def call("contributor", [:"panoptikum:pid",  _, [value]]), do: %{pid:  value}

  def call("episode-contributor", [:"atom:name",       _, [value]]), do: %{name:  value}
  def call("episode-contributor", [:"atom:uri",        _, [value]]), do: %{uri:   value}
  def call("episode-contributor", [:"atom:email",      _, [value]]), do: %{email: value}
# FIXME: HAS20170130 should be pulled out of itunes owner
  def call("episode-contributor", [:"panoptikum:pid",  _, [value]]), do: %{pid:   value}
end


i did better

i did better

defmodule UartHandler do
  require Logger
  @baud Application.get_env(:uart, :baud)

  defp open_serial(_pid, [], tries) do
    Logger.error("Could not auto detect serial port. I tried: #{inspect tries}")
    {:ok, "ttyFail"}
  end

  defp open_serial(pid, ports, tries) do
    [{tty, _ } | rest ] = ports
    blah = Nerves.UART.open(pid, tty, speed: @baud, active: true)
    case blah do
      :ok -> {:ok, tty}
      _ -> open_serial(pid, rest, tries ++ [tty])
    end
  end

  defp open_serial(pid) do
    {:ok, tty} = open_serial(pid, list_ttys , []) # List of available ports
    Nerves.UART.configure(pid, framing: {Nerves.UART.Framing.Line, separator: "\r\n"}, rx_framing_timeout: 500)
    tty
  end

  def list_ttys do
    Nerves.UART.enumerate
    |> Map.drop(["ttyS0","ttyAMA0"])
    |> Map.to_list
  end

  def init(_) do
    Process.flag(:trap_exit, true)
    {:ok, pid} = Nerves.UART.start_link
    {:ok, handler} = NewHandler.start_link(pid)
    tty = open_serial(pid)
    {:ok, {pid, tty, handler}}
  end

  def start_link(_) do
    GenServer.start_link(__MODULE__, {}, name: __MODULE__)
  end

  def handle_cast({:update_fw, hex_file}, {nerves, tty, handler}) do
    Nerves.UART.close(nerves)
    System.cmd("avrdude", ["-v", "-patmega2560", "-cwiring", "-P/dev/#{tty}", "-b115200", "-D", "-Uflash:w:#{hex_file}:i"])
    new_tty = open_serial(nerves)
    {:noreply, {nerves, new_tty, handler}}
  end

  def handle_call(:e_stop, _from, {pid, tty, handler}) do
    write(pid, "E")
    raise "E STOP"
    Process.exit(pid, "asdf")
    Process.exit(handler, :e_stop)
    {:reply, :ok, {pid, tty, handler}}
  end

  @doc """
    Writes to a nerves uart tty. This only exists because
    I wanted to print what is being written
  """
  def write(pid, str) do
    Logger.debug("writing: #{str}")
    Nerves.UART.write(pid, str)
  end

  # WHEN A FULL SERIAL MESSAGE COMES IN.
  def handle_info({:nerves_uart, nerves_tty, message}, {pid, tty, handler})
  when is_binary(message) and nerves_tty == tty do
    gcode = Gcode.parse_code(String.strip(message))
    GenServer.cast(handler, gcode)
    {:noreply, {pid, tty, handler}}
  end

  def handle_info({:nerves_uart, _nerves_tty, message}, {pid, tty, handler})
  when is_binary(message) do
    Logger.debug("Something weird has happened.")
    {:noreply, {pid, tty, handler}}
  end

  def handle_info({:nerves_uart, _tty, {:partial, partial}}, state) do
    Logger.debug("Partial code: #{partial}")
    {:noreply, state}
  end

  def handle_info({:nerves_uart, _tty, {:error, :eio}}, state) do
    RPCMessageHandler.log("Serial disconnected!")
    {:noreply, state}
  end

  def handle_info({:nerves_uart, _tty, event}, state) do
    Logger.debug("Nerves UART Event: #{inspect event}")
    {:noreply, state}
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, _pid, :e_stop}, {nerves, tty, handler}) do
    Logger.debug("E STOPPING")
    Nerves.UART.close(nerves)
    Process.exit(self(), :real_e_stop)
    {:noreply, {nerves, tty, handler}}
  end

  def handle_info({:EXIT, pid, reason}, {nerves, tty, handler}) do
    if(pid == handler) do
      Logger.debug "gcode handler died: #{inspect reason}"
      {:ok, restarted} = NewHandler.start_link(nerves)
      {:noreply,  {nerves, tty, restarted}}
    else
      {:noreply,  {nerves, tty, handler}}
    end
  end

  def handle_info({:EXIT, pid, reason}, state) do
    Logger.debug("EXIT IN #{inspect pid}: #{inspect reason}")
    {:noreply, state}
  end

  def handle_info(event, state) do
    Logger.debug "info: #{inspect event}"
    {:noreply, state}
  end

  def terminate(reason, other) do
    Logger.debug("UART HANDLER DIED.")
    Logger.debug("#{inspect reason}: #{inspect other}")
  end
end

defmodule UartHandler do
  require Logger
  @baud Application.get_env(:uart, :baud)

  defp open_serial(_pid, [], tries) do
    Logger.error("Could not auto detect serial port. I tried: #{inspect tries}")
    {:ok, "ttyFail"}
  end

  defp open_serial(pid, ports, tries) do
    [{tty, _ } | rest ] = ports
    blah = Nerves.UART.open(pid, tty, speed: @baud, active: true)
    case blah do
      :ok -> {:ok, tty}
      _ -> open_serial(pid, rest, tries ++ [tty])
    end
  end

  defp open_serial(pid) do
    {:ok, tty} = open_serial(pid, list_ttys , []) # List of available ports
    Nerves.UART.configure(pid, framing: {Nerves.UART.Framing.Line, separator: "\r\n"}, rx_framing_timeout: 500)
    tty
  end

  def list_ttys do
    Nerves.UART.enumerate
    |> Map.drop(["ttyS0","ttyAMA0"])
    |> Map.to_list
  end

  def init(_) do
    Process.flag(:trap_exit, true)
    {:ok, pid} = Nerves.UART.start_link
    {:ok, handler} = NewHandler.start_link(pid)
    tty = open_serial(pid)
    {:ok, {pid, tty, handler}}
  end

  def start_link(_) do
    GenServer.start_link(__MODULE__, {}, name: __MODULE__)
  end

  def handle_cast({:update_fw, hex_file}, {nerves, _tty, handler}) do
    Nerves.UART.close(nerves)
    System.cmd("avrdude", ["-v", "-patmega2560", "-cwiring", "-P/dev/ttyACM0", "-b115200", "-D", "-Uflash:w:#{hex_file}:i"])
    new_tty = open_serial(nerves)
    {:noreply, {nerves, new_tty, handler}}
  end

  def handle_call(:e_stop, _from, {pid, tty, handler}) do
    write(pid, "E")
    raise "E STOP"
    Process.exit(pid, "asdf")
    Process.exit(handler, :e_stop)
    {:reply, :ok, {pid, tty, handler}}
  end

  @doc """
    Writes to a nerves uart tty. This only exists because
    I wanted to print what is being written
  """
  def write(pid, str) do
    Logger.debug("writing: #{str}")
    Nerves.UART.write(pid, str)
  end

  # WHEN A FULL SERIAL MESSAGE COMES IN.
  def handle_info({:nerves_uart, nerves_tty, message}, {pid, tty, handler})
  when is_binary(message) and nerves_tty == tty do
    gcode = Gcode.parse_code(String.strip(message))
    GenServer.cast(handler, gcode)
    {:noreply, {pid, tty, handler}}
  end

  def handle_info({:nerves_uart, _nerves_tty, message}, {pid, tty, handler})
  when is_binary(message) do
    Logger.debug("Something weird has happened.")
    {:noreply, {pid, tty, handler}}
  end

  def handle_info({:nerves_uart, _tty, {:partial, partial}}, state) do
    Logger.debug("Partial code: #{partial}")
    {:noreply, state}
  end

  def handle_info({:nerves_uart, _tty, {:error, :eio}}, state) do
    RPCMessageHandler.log("Serial disconnected!")
    {:noreply, state}
  end

  def handle_info({:nerves_uart, _tty, event}, state) do
    Logger.debug("Nerves UART Event: #{inspect event}")
    {:noreply, state}
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, _pid, :e_stop}, {nerves, tty, handler}) do
    Logger.debug("E STOPPING")
    Nerves.UART.close(nerves)
    Process.exit(self(), :real_e_stop)
    {:noreply, {nerves, tty, handler}}
  end

  def handle_info({:EXIT, pid, reason}, {nerves, tty, handler}) do
    if(pid == handler) do
      Logger.debug "gcode handler died: #{inspect reason}"
      {:ok, restarted} = NewHandler.start_link(nerves)
      {:noreply,  {nerves, tty, restarted}}
    else
      {:noreply,  {nerves, tty, handler}}
    end
  end

  def handle_info({:EXIT, pid, reason}, state) do
    Logger.debug("EXIT IN #{inspect pid}: #{inspect reason}")
    {:noreply, state}
  end

  def handle_info(event, state) do
    Logger.debug "info: #{inspect event}"
    {:noreply, state}
  end

  def terminate(reason, other) do
    Logger.debug("UART HANDLER DIED.")
    Logger.debug("#{inspect reason}: #{inspect other}")
  end
end


rename function to apply_to_sub_modules

rename function to apply_to_sub_modules

defmodule WhiteBread.Context do

  @steps_to_macro [:given_, :when_, :then_, :and_, :but_]

  @doc false
  defmacro __using__(_opts) do
    quote do
      import WhiteBread.Context
      import ExUnit.Assertions

      @string_steps HashDict.new

      # List of tuples {regex, function}
      @regex_steps []

      @sub_context_modules []

      @scenario_state_definied false
      @scenario_finalize_defined false
      @feature_state_definied false

      @before_compile WhiteBread.Context
    end
  end

  @doc false
  defmacro __before_compile__(_env) do

    quote do
      def execute_step(step, state) do
        {get_string_steps, get_regex_steps}
        |> WhiteBread.Context.StepExecutor.execute_step(step, state)
      end

      def get_string_steps do
        :get_string_steps
          |> apply_to_sub_modules
          |> Enum.into(@string_steps)
      end

      def get_regex_steps do
        :get_regex_steps
          |> apply_to_sub_modules
          |> Enum.into(@regex_steps)
      end

      unless @feature_state_definied do
        def feature_state() do
          # Always default to an empty map
          %{}
        end
      end

      unless @scenario_state_definied do
        def starting_state(state) do
          state
        end
      end

      unless @scenario_finalize_defined do
        def finalize(), do: nil
      end

      defp apply_to_sub_modules(function) do
        @sub_context_modules
        |> Enum.map(fn(sub_module) -> apply(sub_module, function, []) end)
        |> Enum.flat_map(fn(x) -> x end)
      end

    end
  end

  for step <- @steps_to_macro do

    defmacro unquote(step)(step_text, do: block) do
      define_block_step(step_text, block)
    end

    defmacro unquote(step)(step_text, step_function) do
      define_function_step(step_text, step_function)
    end
  end

  defmacro feature_starting_state(function) do
    quote do
      @feature_state_definied true
      def feature_state() do
        unquote(function).()
      end
    end
  end

  defmacro scenario_starting_state(function) do
    quote do
      @scenario_state_definied true
      def starting_state(state) do
        unquote(function).(state)
      end
    end
  end

  defmacro scenario_finalize(function) do
    quote do
      @scenario_finalize_defined true
      def finalize() do
        unquote(function).()
      end
    end
  end

  defmacro subcontext(context_module) do
    quote do
      @sub_context_modules [unquote(context_module) | @sub_context_modules]
    end
  end

  # Regexes
  defp define_block_step({:sigil_r, _, _} = step_regex, block) do
    function_name = step_name(step_regex)
    quote do
      @regex_steps [{unquote(step_regex), &__MODULE__.unquote(function_name)/2}
                    | @regex_steps]
      def unquote(function_name)(state, _extra \\ []) do
        unquote(block)
        {:ok, state}
      end
    end
  end

  defp define_block_step(step_text, block) do
    function_name = step_name(step_text)
    quote do
      @string_steps @string_steps
        |> Dict.put(unquote(step_text), &__MODULE__.unquote(function_name)/2)
      def unquote(function_name)(state, _extra \\ []) do
        unquote(block)
        {:ok, state}
      end
    end
  end

  # regexes
  defp define_function_step({:sigil_r, _, _} = step_regex, function) do
    function_name = step_name(step_regex)
    quote do
      @regex_steps [{unquote(step_regex), &__MODULE__.unquote(function_name)/2}
                    | @regex_steps]
      def unquote(function_name)(state, extra \\ []) do
        case is_function(unquote(function), 1) do
          true  -> unquote(function).(state)
          false -> apply(unquote(function), [state, extra])
        end
      end
    end
  end

  defp define_function_step(step_text, function) do
    function_name = step_name(step_text)
    quote do
      @string_steps @string_steps
       |> Dict.put(unquote(step_text), &__MODULE__.unquote(function_name)/2)
      def unquote(function_name)(state, extra \\ []) do
        case is_function(unquote(function), 1) do
          true  -> unquote(function).(state)
          false -> apply(unquote(function), [state, extra])
        end
      end
    end
  end

  defp step_name({:sigil_r, _, [{_, _, [string]}, _]}) do
    String.to_atom("regex_step_" <> string)
  end
  defp step_name(step_text) do
    String.to_atom("step_" <> step_text)
  end

end

defmodule WhiteBread.Context do

  @steps_to_macro [:given_, :when_, :then_, :and_, :but_]

  @doc false
  defmacro __using__(_opts) do
    quote do
      import WhiteBread.Context
      import ExUnit.Assertions

      @string_steps HashDict.new

      # List of tuples {regex, function}
      @regex_steps []

      @sub_context_modules []

      @scenario_state_definied false
      @scenario_finalize_defined false
      @feature_state_definied false

      @before_compile WhiteBread.Context
    end
  end

  @doc false
  defmacro __before_compile__(_env) do

    quote do
      def execute_step(step, state) do
        {get_string_steps, get_regex_steps}
        |> WhiteBread.Context.StepExecutor.execute_step(step, state)
      end

      def get_string_steps do
        :get_string_steps
          |> get_from_submodule_function
          |> Enum.into(@string_steps)
      end

      def get_regex_steps do
        :get_regex_steps
          |> get_from_submodule_function
          |> Enum.into(@regex_steps)
      end

      unless @feature_state_definied do
        def feature_state() do
          # Always default to an empty map
          %{}
        end
      end

      unless @scenario_state_definied do
        def starting_state(state) do
          state
        end
      end

      unless @scenario_finalize_defined do
        def finalize(), do: nil
      end

      defp get_from_submodule_function(function) do
        @sub_context_modules
        |> Enum.map(fn(sub_module) -> apply(sub_module, function, []) end)
        |> Enum.flat_map(fn(x) -> x end)
      end

    end
  end

  for step <- @steps_to_macro do

    defmacro unquote(step)(step_text, do: block) do
      define_block_step(step_text, block)
    end

    defmacro unquote(step)(step_text, step_function) do
      define_function_step(step_text, step_function)
    end
  end

  defmacro feature_starting_state(function) do
    quote do
      @feature_state_definied true
      def feature_state() do
        unquote(function).()
      end
    end
  end

  defmacro scenario_starting_state(function) do
    quote do
      @scenario_state_definied true
      def starting_state(state) do
        unquote(function).(state)
      end
    end
  end

  defmacro scenario_finalize(function) do
    quote do
      @scenario_finalize_defined true
      def finalize() do
        unquote(function).()
      end
    end
  end

  defmacro subcontext(context_module) do
    quote do
      @sub_context_modules [unquote(context_module) | @sub_context_modules]
    end
  end

  # Regexes
  defp define_block_step({:sigil_r, _, _} = step_regex, block) do
    function_name = step_name(step_regex)
    quote do
      @regex_steps [{unquote(step_regex), &__MODULE__.unquote(function_name)/2}
                    | @regex_steps]
      def unquote(function_name)(state, _extra \\ []) do
        unquote(block)
        {:ok, state}
      end
    end
  end

  defp define_block_step(step_text, block) do
    function_name = step_name(step_text)
    quote do
      @string_steps @string_steps
        |> Dict.put(unquote(step_text), &__MODULE__.unquote(function_name)/2)
      def unquote(function_name)(state, _extra \\ []) do
        unquote(block)
        {:ok, state}
      end
    end
  end

  # regexes
  defp define_function_step({:sigil_r, _, _} = step_regex, function) do
    function_name = step_name(step_regex)
    quote do
      @regex_steps [{unquote(step_regex), &__MODULE__.unquote(function_name)/2}
                    | @regex_steps]
      def unquote(function_name)(state, extra \\ []) do
        case is_function(unquote(function), 1) do
          true  -> unquote(function).(state)
          false -> apply(unquote(function), [state, extra])
        end
      end
    end
  end

  defp define_function_step(step_text, function) do
    function_name = step_name(step_text)
    quote do
      @string_steps @string_steps
       |> Dict.put(unquote(step_text), &__MODULE__.unquote(function_name)/2)
      def unquote(function_name)(state, extra \\ []) do
        case is_function(unquote(function), 1) do
          true  -> unquote(function).(state)
          false -> apply(unquote(function), [state, extra])
        end
      end
    end
  end

  defp step_name({:sigil_r, _, [{_, _, [string]}, _]}) do
    String.to_atom("regex_step_" <> string)
  end
  defp step_name(step_text) do
    String.to_atom("step_" <> step_text)
  end

end


Add additional information to prod.secret.exs

Insist on documenting or automating the creation of the file

use Mix.Config

# In this file, we keep production configuration that
# you likely want to automate and keep it away from
# your version control system.
#
# You should most probably document the content of this
# file or create a script for recreating it, since it's
# kept out of version control and might be hard to recover
# or recreate for your teammates (or you later on).
config :<%= application_name %>, <%= application_module %>.Endpoint,
  secret_key_base: "<%= prod_secret_key_base %>"

use Mix.Config

# In this file, we keep production configuration that
# you likely want to automate and keep it away from
# your version control system.
config :<%= application_name %>, <%= application_module %>.Endpoint,
  secret_key_base: "<%= prod_secret_key_base %>"


movie_id can't be null anymore 😇

movie_id can't be null anymore 😇

defmodule CinemaApi.Repo.Migrations.CreateShowtimesTable do
  @moduledoc """
  showtimes table migration
  """
  use Ecto.Migration

  def change do
    create table(:showtimes, primary_key: false) do
      add(:id, :bigserial, primary_key: true)
      add(:movie_id, references(:movies), null: false)
      add(:imdb_id, :string)
      add(:title, :string, null: false)
      add(:showtime, :utc_datetime, null: false)
      add(:cinemahall, :string, null: false)

      timestamps()
    end

    create(
      unique_index(
        :showtimes,
        [:movie_id, :showtime, :cinemahall],
        name: "mid-showtime-cinemahall-comp-key"
      )
    )
  end
end

defmodule CinemaApi.Repo.Migrations.CreateShowtimesTable do
  use Ecto.Migration

  def change do
    create table(:showtimes, primary_key: false) do
      add(:id, :bigserial, primary_key: true)
      add(:movie_id, references(:movies))
      add(:imdb_id, :string)
      add(:title, :string, null: false)
      add(:showtime, :utc_datetime, null: false)
      add(:cinemahall, :string, null: false)

      timestamps()
    end

    create(
      unique_index(
        :showtimes,
        [:movie_id, :showtime, :cinemahall],
        name: "mid-showtime-cinemahall-comp-key"
      )
    )
  end
end


Woot! We handle multi-row result struct correctly already.

Not sure how to cause a runtime error in Sqlite.

defmodule QueryTest do
  # IMPORTANT: This is closely modeled on Postgrex's query_test.exs file.
  # We strive to avoid structural differences between that file and this one.

  use ExUnit.Case, async: true
  import Sqlite.DbConnection.TestHelper
  alias Sqlite.DbConnection.Connection, as: P

  setup do
    opts = [ database: ":memory:", backoff_type: :stop ]
    {:ok, pid} = P.start_link(opts)
    {:ok, [pid: pid]}
  end

  test "iodata", context do
    assert [[123]] = query(["S", ?E, ["LEC"|"T"], " ", '123'], [])
  end

  test "decode basic types", context do
    assert [[nil]] = query("SELECT NULL", [])
    # assert [[true, false]] = query("SELECT true, false", [])
      # ^^ doesn't exist in SQLite
    assert [["e"]] = query("SELECT 'e'", [])
    assert [["ẽ"]] = query("SELECT 'ẽ'", [])
    assert [[42]] = query("SELECT 42", [])
    assert [[42.0]] = query("SELECT cast(42 as float)", [])
    # assert [[:NaN]] = query("SELECT 'NaN'::float", [])
    # assert [[:inf]] = query("SELECT 1/0", [])
    # assert [[:"-inf"]] = query("SELECT -1/0", [])
      # ^^ doesn't exist in SQLite
    assert [["ẽric"]] = query("SELECT 'ẽric'", [])
    assert [[<<1, 2, 3>>]] = query("SELECT cast(x'010203' as blob)", [])
  end

  # Note: Most of the decoding tests don't apply because those types don't exist
  # in SQLite.

  test "encode basic types", context do
    assert [[nil, nil]] = query("SELECT cast($1 as text), cast($2 as int)", [nil, nil])
    assert [[1, 0]] = query("SELECT $1, $2", [true, false])
    assert [["ẽ"]] = query("SELECT cast($1 as char)", ["ẽ"])
    assert [[42]] = query("SELECT cast($1 as int)", [42])
    assert [[42.0, 43.0]] = query("SELECT cast($1 as float), cast($2 as float)", [42, 43.0])
    # assert [[:NaN]] = query("SELECT $1::float", [:NaN])
    # assert [[:inf]] = query("SELECT $1::float", [:inf])
    # assert [[:"-inf"]] = query("SELECT $1::float", [:"-inf"])
      # ^^ doesn't exist in SQLite
    assert [["ẽric"]] = query("SELECT cast($1 as varchar)", ["ẽric"])
    assert [[<<1, 2, 3>>]] = query("SELECT cast($1 as blob)", [<<1, 2, 3>>])
  end

  test "fail on parameter length mismatch", context do
    assert_raise ArgumentError, "parameters must match number of placeholders in query", fn ->
      query("SELECT $1::integer", [1, 2])
    end

    assert_raise ArgumentError, "parameters must match number of placeholders in query", fn ->
      query("SELECT 42", [1])
    end

    assert [[42]] = query("SELECT 42", [])
  end

  test "non data statement", context do
    assert :ok = query("BEGIN", [])
    assert :ok = query("COMMIT", [])
  end

  test "result struct", context do
    assert {:ok, res} = P.query(context[:pid], "SELECT 123 AS a, 456 AS b", [])
    assert %Sqlite.DbConnection.Result{} = res
    assert res.command == :select
    assert res.columns == ["a", "b"]
    assert res.num_rows == 1
  end

  # Disabled: I don't know of a way to trigger a runtime error in Sqlite.
  # test "error struct", context do
  #   assert {:error, %Postgrex.Error{}} = P.query(context[:pid], "SELECT 123 + 'a'", [])
  # end

  test "multi row result struct", context do
    assert {:ok, res} = P.query(context[:pid], "VALUES (1, 2), (3, 4)", [])
    assert res.num_rows == 2
    assert res.rows == [[1, 2], [3, 4]]
  end
end

defmodule QueryTest do
  # IMPORTANT: This is closely modeled on Postgrex's query_test.exs file.
  # We strive to avoid structural differences between that file and this one.

  use ExUnit.Case, async: true
  import Sqlite.DbConnection.TestHelper
  alias Sqlite.DbConnection.Connection, as: P

  setup do
    opts = [ database: ":memory:", backoff_type: :stop ]
    {:ok, pid} = P.start_link(opts)
    {:ok, [pid: pid]}
  end

  test "iodata", context do
    assert [[123]] = query(["S", ?E, ["LEC"|"T"], " ", '123'], [])
  end

  test "decode basic types", context do
    assert [[nil]] = query("SELECT NULL", [])
    # assert [[true, false]] = query("SELECT true, false", [])
      # ^^ doesn't exist in SQLite
    assert [["e"]] = query("SELECT 'e'", [])
    assert [["ẽ"]] = query("SELECT 'ẽ'", [])
    assert [[42]] = query("SELECT 42", [])
    assert [[42.0]] = query("SELECT cast(42 as float)", [])
    # assert [[:NaN]] = query("SELECT 'NaN'::float", [])
    # assert [[:inf]] = query("SELECT 1/0", [])
    # assert [[:"-inf"]] = query("SELECT -1/0", [])
      # ^^ doesn't exist in SQLite
    assert [["ẽric"]] = query("SELECT 'ẽric'", [])
    assert [[<<1, 2, 3>>]] = query("SELECT cast(x'010203' as blob)", [])
  end

  # Note: Most of the decoding tests don't apply because those types don't exist
  # in SQLite.

  test "encode basic types", context do
    assert [[nil, nil]] = query("SELECT cast($1 as text), cast($2 as int)", [nil, nil])
    assert [[1, 0]] = query("SELECT $1, $2", [true, false])
    assert [["ẽ"]] = query("SELECT cast($1 as char)", ["ẽ"])
    assert [[42]] = query("SELECT cast($1 as int)", [42])
    assert [[42.0, 43.0]] = query("SELECT cast($1 as float), cast($2 as float)", [42, 43.0])
    # assert [[:NaN]] = query("SELECT $1::float", [:NaN])
    # assert [[:inf]] = query("SELECT $1::float", [:inf])
    # assert [[:"-inf"]] = query("SELECT $1::float", [:"-inf"])
      # ^^ doesn't exist in SQLite
    assert [["ẽric"]] = query("SELECT cast($1 as varchar)", ["ẽric"])
    assert [[<<1, 2, 3>>]] = query("SELECT cast($1 as blob)", [<<1, 2, 3>>])
  end

  test "fail on parameter length mismatch", context do
    assert_raise ArgumentError, "parameters must match number of placeholders in query", fn ->
      query("SELECT $1::integer", [1, 2])
    end

    assert_raise ArgumentError, "parameters must match number of placeholders in query", fn ->
      query("SELECT 42", [1])
    end

    assert [[42]] = query("SELECT 42", [])
  end

  test "non data statement", context do
    assert :ok = query("BEGIN", [])
    assert :ok = query("COMMIT", [])
  end

  test "result struct", context do
    assert {:ok, res} = P.query(context[:pid], "SELECT 123 AS a, 456 AS b", [])
    assert %Sqlite.DbConnection.Result{} = res
    assert res.command == :select
    assert res.columns == ["a", "b"]
    assert res.num_rows == 1
  end
end


Adaptation test controller to use Weber.Controller; Set layout to false.

defmodule Example.Test do
  use Weber.Controller

  layout false
  def render_site(_) do
    {:render, [], []}
  end

end

defmodule Example.Test do

  def render_site(_) do
    {:render, [], []}
  end

end


remove compile warning in utils

remove compile warning in utils

defmodule GRPC.Transport.Utils do
  @us_ceiling 100_000_000
  @ms_ceiling @us_ceiling * 1000
  @second_ceiling @ms_ceiling * 1000
  @minute_ceiling @second_ceiling * 60
  @hour_ceiling @minute_ceiling * 60

  def encode_timeout(deadline, now) when not is_integer(deadline) do
    encode_timeout(GRPC.TimeUtils.to_relative(deadline, now))
  end

  def encode_timeout(deadline) when not is_integer(deadline) do
    encode_timeout(deadline, DateTime.utc_now)
  end
  def encode_timeout(timeout) when timeout <= 0, do: "0u"
  def encode_timeout(timeout) when timeout < @us_ceiling do
    to_string(timeout) <> "u"
  end
  def encode_timeout(timeout) when timeout < @ms_ceiling do
    to_string(div(timeout, 1000)) <> "m"
  end
  def encode_timeout(timeout) when timeout < @second_ceiling do
    to_string(div(timeout, 1000_000)) <> "S"
  end
  def encode_timeout(timeout) when timeout < @minute_ceiling do
    to_string(div(timeout, 1000_000 * 60)) <> "M"
  end
  def encode_timeout(timeout) when timeout < @hour_ceiling do
    to_string(div(timeout, 1000_000 * 3600)) <> "H"
  end
  def encode_timeout(_timeout) do
    to_string(@us_ceiling - 1) <> "H"
  end
end

defmodule GRPC.Transport.Utils do
  @us_ceiling 100_000_000
  @ms_ceiling @us_ceiling * 1000
  @second_ceiling @ms_ceiling * 1000
  @minute_ceiling @second_ceiling * 60
  @hour_ceiling @minute_ceiling * 60

  def encode_timeout(deadline) when not is_integer(deadline) do
    encode_timeout(deadline, DateTime.utc_now)
  end
  def encode_timeout(deadline, now) when not is_integer(deadline) do
    encode_timeout(GRPC.TimeUtils.to_relative(deadline, now))
  end
  def encode_timeout(timeout) when timeout <= 0, do: "0u"
  def encode_timeout(timeout) when timeout < @us_ceiling do
    to_string(timeout) <> "u"
  end
  def encode_timeout(timeout) when timeout < @ms_ceiling do
    to_string(div(timeout, 1000)) <> "m"
  end
  def encode_timeout(timeout) when timeout < @second_ceiling do
    to_string(div(timeout, 1000_000)) <> "S"
  end
  def encode_timeout(timeout) when timeout < @minute_ceiling do
    to_string(div(timeout, 1000_000 * 60)) <> "M"
  end
  def encode_timeout(timeout) when timeout < @hour_ceiling do
    to_string(div(timeout, 1000_000 * 3600)) <> "H"
  end
  def encode_timeout(timeout) do
    to_string(@us_ceiling - 1) <> "H"
  end
end


store redis connection in state

store redis connection in state

defmodule Yodado.Web.Admin.FeatureStatusHandler do

  defrecord State, c: nil, feature: nil

  def init(_transport, _req, _opts) do
    {:upgrade, :protocol, :cowboy_rest}
  end

  def rest_init(req, _opts) do
    state = State.new(c: Yodado.FeaturePersistence.new)
    {:ok, req, state}
  end

  def allowed_methods(req, state) do
    {["GET", "PUT", "DELETE"], req, state}
  end

  def service_available(req, state) do
    persistence_ok = Yodado.FeaturePersistence.ping(state.c)
    {persistence_ok, req, state}
  end

  def resource_exists(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    case Yodado.FeaturePersistence.find_feature(feature_id, state.c) do
      :not_found ->     {false, req, state}
      {:ok, feature} -> state = State.feature(feature)
                        {true, req, state}
    end
  end

  def content_types_provided(req, state) do
    providers = [
      {"*", :render_feature}
    ]
    {providers, req, state}
  end

  def render_feature(req, state) do
    req = :cowboy_req.set_resp_header("content-type", "application/json; charset=utf-8", req)
    body = state.feature |> Yodado.Feature.json |> JSEX.encode!(indent: 2)
    {body, req, state}
  end

  def content_types_accepted(req, state) do
    acceptors = [
      {"application/json", :save_feature}
    ]
    {acceptors, req, state}
  end

  def save_feature(req, state) do
    {:ok, feature_json, req} = :cowboy_req.body(req)
    feature = feature_json |> JSEX.decode!(labels: :atom) |> Yodado.Feature.from_json
    Yodado.FeaturePersistence.save_feature(feature, state.c)
    {true, req, state}
  end

  def delete_resource(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    Yodado.FeaturePersistence.delete_feature(feature_id, state.c)
    {true, req, state}
  end
end

defmodule Yodado.Web.Admin.FeatureStatusHandler do
  def init(_transport, _req, _opts) do
    {:upgrade, :protocol, :cowboy_rest}
  end

  def rest_init(req, _opts) do
    {:ok, req, []}
  end

  def allowed_methods(req, state) do
    {["GET", "PUT", "DELETE"], req, state}
  end

  def service_available(req, state) do
    persistence_ok = Yodado.FeaturePersistence.ping
    {persistence_ok, req, state}
  end

  def resource_exists(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    case Yodado.FeaturePersistence.find_feature(feature_id) do
      :not_found ->     {false, req, state}
      {:ok, feature} -> state = Keyword.put(state, :feature, feature)
                        {true, req, state}
    end
  end

  def content_types_provided(req, state) do
    providers = [
      {"*", :render_feature}
    ]
    {providers, req, state}
  end

  def render_feature(req, state) do
    req = :cowboy_req.set_resp_header("content-type", "application/json; charset=utf-8", req)
    body = state[:feature] |> Yodado.Feature.json |> JSEX.encode!(indent: 2)
    {body, req, state}
  end

  def content_types_accepted(req, state) do
    acceptors = [
      {"application/json", :save_feature}
    ]
    {acceptors, req, state}
  end

  def save_feature(req, state) do
    {:ok, feature_json, req} = :cowboy_req.body(req)
    feature = feature_json |> JSEX.decode!(labels: :atom) |> Yodado.Feature.from_json
    Yodado.FeaturePersistence.save_feature(feature)
    {true, req, state}
  end

  def delete_resource(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    Yodado.FeaturePersistence.delete_feature(feature_id)
    {true, req, state}
  end
end


Finalize ignore_strings for MaxLineLength

Thx @devoneste

defmodule Credo.Check.Readability.MaxLineLength do
  @moduledoc """
  Checks for the length of lines.

  Ignores function definitions and (multi-)line strings by default.
  """

  @explanation [
    check: @moduledoc,
    params: [
      max_length: "The maximum number of characters a line may consist of.",
      ignore_definitions: "Set to `true` to ignore lines including function definitions.",
      ignore_specs: "Set to `true` to ignore lines including `@spec`s.",
      ignore_strings: "Set to `true` to ignore lines that are strings or in heredocs",
    ]
  ]
  @default_params [
    max_length: 80,
    ignore_definitions: true,
    ignore_specs: false,
    ignore_strings: true
  ]

  @def_ops [:def, :defp, :defmacro]

  use Credo.Check, base_priority: :low

  def run(%SourceFile{ast: ast, source: source} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)
    max_length = Params.get(params, :max_length, @default_params)
    ignore_definitions = Params.get(params, :ignore_definitions, @default_params)
    ignore_specs = Params.get(params, :ignore_specs, @default_params)
    ignore_strings = Params.get(params, :ignore_strings, @default_params)

    definitions = Credo.Code.prewalk(ast, &find_definitions/2)
    specs = Credo.Code.prewalk(ast, &find_specs/2)

    source =
      if ignore_strings do
        Credo.Code.Strings.replace_with_spaces(source, "")
      else
        source
      end

    lines = Credo.Code.to_lines(source)

    Enum.reduce(lines, [], fn({line_no, line}, issues) ->
      if String.length(line) > max_length do
        if refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs) do
          issues
        else
          [issue_for(line_no, max_length, line, issue_meta) | issues]
        end
      else
        issues
      end
    end)
  end

  for op <- @def_ops do
    defp find_definitions({unquote(op), meta, arguments} = ast, definitions) when is_list(arguments) do
      {ast, [meta[:line] | definitions]}
    end
  end
  defp find_definitions(ast, definitions) do
    {ast, definitions}
  end

  defp find_specs({:spec, meta, arguments} = ast, specs) when is_list(arguments) do
    {ast, [meta[:line] | specs]}
  end
  defp find_specs(ast, specs) do
    {ast, specs}
  end

  defp refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs) do
    ignore_definitions? =
      if ignore_definitions, do: Enum.member?(definitions, line_no), else: false

    ignore_specs? =
      if ignore_specs, do: Enum.member?(specs, line_no), else: false

    ignore_definitions? || ignore_specs?
  end

  defp issue_for(line_no, max_length, line, issue_meta) do
    line_length = String.length(line)
    column = max_length + 1
    trigger = String.slice(line, max_length, line_length - max_length)

    format_issue issue_meta,
      message: "Line is too long (max is #{max_length}, was #{line_length}).",
      line_no: line_no,
      column: column,
      trigger: trigger
  end
end

defmodule Credo.Check.Readability.MaxLineLength do
  @moduledoc """
  Checks for the length of lines.

  Ignores function definitions and (multi-)line strings by default.
  """

  @explanation [
    check: @moduledoc,
    params: [
      max_length: "The maximum number of characters a line may consist of.",
      ignore_definitions: "Set to `true` to ignore lines including function definitions.",
      ignore_specs: "Set to `true` to ignore lines including `@spec`s.",
      ignore_strings: "Set to `true` to ignore lines that are strings or in heredocs",
    ]
  ]
  @default_params [
    max_length: 80,
    ignore_definitions: true,
    ignore_specs: false,
    ignore_strings: true
  ]

  @def_ops [:def, :defp, :defmacro]

  use Credo.Check, base_priority: :low

  def run(%SourceFile{ast: ast, lines: lines} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)
    max_length = Params.get(params, :max_length, @default_params)
    ignore_definitions = Params.get(params, :ignore_definitions, @default_params)
    ignore_specs = Params.get(params, :ignore_specs, @default_params)
    ignore_strings = Params.get(params, :ignore_strings, @default_params)

    definitions = Credo.Code.prewalk(ast, &find_definitions/2)
    specs = Credo.Code.prewalk(ast, &find_specs/2)
    [_in_heredoc, strings] = Enum.reduce(lines, [false, []], &find_strings/2)

    Enum.reduce(lines, [], fn({line_no, line}, issues) ->
      if String.length(line) > max_length do
        if refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs, strings, ignore_strings) do
          issues
        else
          [issue_for(line_no, max_length, line, issue_meta) | issues]
        end
      else
        issues
      end
    end)
  end

  for op <- @def_ops do
    defp find_definitions({unquote(op), meta, arguments} = ast, definitions) when is_list(arguments) do
      {ast, [meta[:line] | definitions]}
    end
  end
  defp find_definitions(ast, definitions) do
    {ast, definitions}
  end

  defp find_specs({:spec, meta, arguments} = ast, specs) when is_list(arguments) do
    {ast, [meta[:line] | specs]}
  end
  defp find_specs(ast, specs) do
    {ast, specs}
  end

  defp find_strings({line_no, line}, [true, strings]) do
    if heredoc_delimiter?(line) do
      [false, strings]
    else
      [true, strings ++ [line_no]]
    end
  end

  defp find_strings({line_no, line}, [false, strings]) do
    cond do
      heredoc_delimiter?(line) ->
        [true, strings]
      string_literal?(line) ->
        [false, strings ++ [line_no]]
      true ->
        [false, strings]
    end
  end

  defp string_literal?(line) do
    String.match?(line, ~r/\A\s*\\*\".+\\*\"\s*\z/)
  end

  defp heredoc_delimiter?(line) do
    String.match?(line, ~r/\A\s*\\?\"\\?\"\\?\"\s*\z/)
  end

  defp refute_issue?(line_no, definitions, ignore_definitions, specs, ignore_specs, strings, ignore_strings) do
    ignore_definitions? =
      if ignore_definitions, do: Enum.member?(definitions, line_no), else: false

    ignore_specs? =
      if ignore_specs, do: Enum.member?(specs, line_no), else: false

    ignore_strings? =
      if ignore_strings, do: Enum.member?(strings, line_no), else: false

    ignore_definitions? || ignore_specs? || ignore_strings?
  end

  defp issue_for(line_no, max_length, line, issue_meta) do
    line_length = String.length(line)
    column = max_length + 1
    trigger = String.slice(line, max_length, line_length - max_length)

    format_issue issue_meta,
      message: "Line is too long (max is #{max_length}, was #{line_length}).",
      line_no: line_no,
      column: column,
      trigger: trigger
  end
end


Rename complete to ready; send owner a message

defmodule Romeo.Transports.TCP do
  @moduledoc false

  @default_port 5222
  @ssl_opts [reuse_sessions: true]
  @socket_opts [packet: :raw, mode: :binary, active: :once]

  @type state :: Romeo.Connection.t

  alias Romeo.JID
  alias Romeo.Auth
  alias Romeo.Stanza
  alias Romeo.Connection.Features
  alias Romeo.Connection, as: Conn

  require Logger

  import Kernel, except: [send: 2]

  @spec connect(Keyword.t) :: {:ok, state} | {:error, any}
  def connect(%Conn{host: host, port: port, socket_opts: socket_opts} = conn) do
    host = (host || host(conn.jid)) |> to_char_list
    port = (port || @default_port)

    conn = %{conn | host: host, port: port, socket_opts: socket_opts}

    case :gen_tcp.connect(host, port, socket_opts ++ @socket_opts, conn.timeout) do
      {:ok, socket} ->
        Logger.info fn -> "Connected to server" end
        {:ok, parser} = :exml_stream.new_parser
        start_protocol(%{conn | parser: parser, socket: {:gen_tcp, socket}})
      {:error, _} = error ->
        error
    end
  end

  def disconnect(info, {mod, socket}) do
    :ok = mod.close(socket)
    case info do
      {:close, from} ->
        Connection.reply(from, :ok)
      {:error, :closed} ->
        :error_logger.format("Connection closed~n", [])
      {:error, reason} ->
        reason = :inet.format_error(reason)
        :error_logger.format("Connection error: ~s~n", [reason])
    end
  end

  defp start_protocol(%Conn{} = conn) do
    conn
    |> start_stream
    |> negotiate_features
    |> start_tls
    |> authenticate
    |> bind
    |> session
    |> ready
  end

  defp start_stream(%Conn{jid: jid} = conn) do
    stanza = jid |> host |> Romeo.Stanza.start_stream

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlstreamstart() | []] -> conn end)
  end

  defp negotiate_features(%Conn{} = conn) do
    recv(conn, fn conn, [xmlel(name: "stream:features") = packet | []] ->
      %{conn | features: Features.parse_stream_features(packet)}
    end)
  end

  defp start_tls(%Conn{features: %Features{tls?: true}} = conn) do
    conn
    |> send(Stanza.start_tls)
    |> recv(fn conn, [xmlel(name: "proceed") | []] -> conn end)
    |> upgrade_to_tls
    |> start_stream
    |> negotiate_features
  end
  defp start_tls(%Conn{} = conn), do: conn

  defp upgrade_to_tls(%Conn{socket: {:gen_tcp, socket}} = conn) do
    Logger.info fn -> "Upgrading connection to TLS" end
    {:ok, socket} = :ssl.connect(socket, conn.ssl_opts ++ @ssl_opts)
    {:ok, parser} = :exml_stream.new_parser
    Logger.info fn -> "Connection secured" end
    %{conn | socket: {:ssl, socket}, parser: parser}
  end

  defp authenticate(%Conn{} = conn) do
    conn
    |> Auth.authenticate!
    |> reset_parser
    |> start_stream
  end

  defp bind(%Conn{resource: resource} = conn) do
    stanza = Romeo.Stanza.bind(resource)
    id = Romeo.XML.attr(stanza, "id")

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlel(name: "iq") = stanza | []] ->
      "result" = Romeo.XML.attr(stanza, "type")
      ^id = Romeo.XML.attr(stanza, "id")

      %Romeo.JID{resource: resource} =
        stanza
        |> Romeo.XML.subelement("bind")
        |> Romeo.XML.subelement("jid")
        |> Romeo.XML.cdata
        |> Romeo.JID.parse

      %{conn | resource: resource}
    end)
  end

  defp session(%Conn{} = conn) do
    stanza = Romeo.Stanza.session
    id = Romeo.XML.attr(stanza, "id")

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlel(name: "iq") = stanza | []] ->
      "result" = Romeo.XML.attr(stanza, "type")
      ^id = Romeo.XML.attr(stanza, "id")

      conn
    end)
  end

  defp ready(%Conn{owner: owner} = conn) do
    _ = Kernel.send(owner, :connection_ready)
    {:ok, conn}
  end

  defp reset_parser(%Conn{parser: parser} = conn) do
    {:ok, parser} = :exml_stream.reset_parser(parser)
    %{conn | parser: parser}
  end

  defp parse_stanza(%Conn{jid: jid, owner: owner, parser: parser} = conn, data) do
    {:ok, parser, stanzas} = :exml_stream.parse(parser, data)
    for stanza <- stanzas do
      _ = Kernel.send owner, {:stanza_received, stanza}
    end
    {:ok, %Conn{conn | parser: parser}, stanzas}
  end

  def send(%Conn{socket: {mod, socket}} = conn, stanza) do
    stanza = Stanza.to_xml(stanza)
    Logger.debug fn -> "OUT > #{inspect stanza}" end
    :ok = mod.send(socket, stanza)
    {:ok, conn}
  end

  def recv({:ok, conn}, fun), do: recv(conn, fun)
  def recv(%Conn{socket: {:gen_tcp, socket}, timeout: timeout} = conn, fun) do
    receive do
      {:tcp, ^socket, stanza} ->
        :inet.setopts(socket, active: :once)
        {:ok, conn, stanzas} = parse_stanza(conn, stanza)
        fun.(conn, stanzas)
      {:tcp_closed, ^socket} ->
        {:error, :closed}
      {:tcp_error, ^socket, reason} ->
        {:error, reason}
    after timeout ->
      _ = Kernel.send(self, {:error, :timeout})
      conn
    end
  end
  def recv(%Conn{socket: {:ssl, socket}, timeout: timeout} = conn, fun) do
    receive do
      {:ssl, ^socket, stanza} ->
        :ssl.setopts(socket, active: :once)
        {:ok, conn, stanzas} = parse_stanza(conn, stanza)
        fun.(conn, stanzas)
      {:ssl_closed, ^socket} ->
        {:error, :closed}
      {:ssl_error, ^socket, reason} ->
        {:error, reason}
    after timeout ->
      _ = Kernel.send(self, {:error, :timeout})
      conn
    end
  end

  def handle_message({:tcp, socket, data}, %{socket: {:gen_tcp, socket}} = conn) do
    {:ok, _, _} = handle_data(data, conn)
  end
  def handle_message({:tcp_closed, socket}, %{socket: {:gen_tcp, socket}}) do
    {:error, :closed}
  end
  def handle_message({:tcp_error, socket, reason}, %{socket: {:gen_tcp, socket}}) do
    {:error, reason}
  end
  def handle_message({:ssl, socket, data}, %{socket: {:ssl, socket}} = conn) do
    {:ok, _, _} = handle_data(data, conn)
  end
  def handle_message({:ssl_closed, socket}, %{socket: {:ssl, socket}}) do
    {:error, :closed}
  end
  def handle_message({:ssl_error, socket, reason}, %{socket: {:ssl, socket}}) do
    {:error, reason}
  end
  def handle_message(_, _), do: :unknown

  defp handle_data(msg, %{socket: socket} = conn) do
    :ok = activate(socket)
    {:ok, _conn, _stanza} = parse_stanza(conn, msg)
  end

  defp activate({:gen_tcp, socket}) do
    case :inet.setopts(socket, [active: :once]) do
      :ok ->
        :ok
      {:error, :closed} ->
        _ = Kernel.send(self, {:tcp_closed, socket})
        :ok
      {:error, reason} ->
        _ = Kernel.send(self, {:tcp_error, socket, reason})
        :ok
    end
  end
  defp activate({:ssl, socket}) do
    case :ssl.setopts(socket, [active: :once]) do
      :ok ->
        :ok
      {:error, :closed} ->
        _ = Kernel.send(self, {:ssl_closed, socket})
        :ok
      {:error, reason} ->
        _ = Kernel.send(self, {:ssl_error, socket, reason})
        :ok
    end
  end

  defp host(jid) do
    Romeo.JID.parse(jid).server
  end
end

defmodule Romeo.Transports.TCP do
  @moduledoc false

  @default_port 5222
  @ssl_opts [reuse_sessions: true]
  @socket_opts [packet: :raw, mode: :binary, active: :once]

  @type state :: Romeo.Connection.t

  alias Romeo.JID
  alias Romeo.Auth
  alias Romeo.Stanza
  alias Romeo.Connection.Features
  alias Romeo.Connection, as: Conn

  require Logger

  import Kernel, except: [send: 2]

  @spec connect(Keyword.t) :: {:ok, state} | {:error, any}
  def connect(%Conn{host: host, port: port, socket_opts: socket_opts} = conn) do
    host = (host || host(conn.jid)) |> to_char_list
    port = (port || @default_port)

    conn = %{conn | host: host, port: port, socket_opts: socket_opts}

    case :gen_tcp.connect(host, port, socket_opts ++ @socket_opts, conn.timeout) do
      {:ok, socket} ->
        Logger.info fn -> "Connected to server" end
        {:ok, parser} = :exml_stream.new_parser
        start_protocol(%{conn | parser: parser, socket: {:gen_tcp, socket}})
      {:error, _} = error ->
        error
    end
  end

  def disconnect(info, {mod, socket}) do
    :ok = mod.close(socket)
    case info do
      {:close, from} ->
        Connection.reply(from, :ok)
      {:error, :closed} ->
        :error_logger.format("Connection closed~n", [])
      {:error, reason} ->
        reason = :inet.format_error(reason)
        :error_logger.format("Connection error: ~s~n", [reason])
    end
  end

  defp start_protocol(%Conn{} = conn) do
    conn
    |> start_stream
    |> negotiate_features
    |> start_tls
    |> authenticate
    |> bind
    |> session
    |> complete
  end

  defp start_stream(%Conn{jid: jid} = conn) do
    stanza = jid |> host |> Romeo.Stanza.start_stream

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlstreamstart() | []] -> conn end)
  end

  defp negotiate_features(%Conn{} = conn) do
    recv(conn, fn conn, [xmlel(name: "stream:features") = packet | []] ->
      %{conn | features: Features.parse_stream_features(packet)}
    end)
  end

  defp start_tls(%Conn{features: %Features{tls?: true}} = conn) do
    conn
    |> send(Stanza.start_tls)
    |> recv(fn conn, [xmlel(name: "proceed") | []] -> conn end)
    |> upgrade_to_tls
    |> start_stream
    |> negotiate_features
  end
  defp start_tls(%Conn{} = conn), do: conn

  defp upgrade_to_tls(%Conn{socket: {:gen_tcp, socket}} = conn) do
    Logger.info fn -> "Upgrading connection to TLS" end
    {:ok, socket} = :ssl.connect(socket, conn.ssl_opts ++ @ssl_opts)
    {:ok, parser} = :exml_stream.new_parser
    Logger.info fn -> "Connection secured" end
    %{conn | socket: {:ssl, socket}, parser: parser}
  end

  defp authenticate(%Conn{} = conn) do
    conn
    |> Auth.authenticate!
    |> reset_parser
    |> start_stream
  end

  defp bind(%Conn{resource: resource} = conn) do
    stanza = Romeo.Stanza.bind(resource)
    id = Romeo.XML.attr(stanza, "id")

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlel(name: "iq") = stanza | []] ->
      "result" = Romeo.XML.attr(stanza, "type")
      ^id = Romeo.XML.attr(stanza, "id")

      %Romeo.JID{resource: resource} =
        stanza
        |> Romeo.XML.subelement("bind")
        |> Romeo.XML.subelement("jid")
        |> Romeo.XML.cdata
        |> Romeo.JID.parse

      %{conn | resource: resource}
    end)
  end

  defp session(%Conn{} = conn) do
    stanza = Romeo.Stanza.session
    id = Romeo.XML.attr(stanza, "id")

    conn
    |> send(stanza)
    |> recv(fn conn, [xmlel(name: "iq") = stanza | []] ->
      "result" = Romeo.XML.attr(stanza, "type")
      ^id = Romeo.XML.attr(stanza, "id")

      conn
    end)
  end

  defp complete(conn) do
    {:ok, conn}
  end

  defp reset_parser(%Conn{parser: parser} = conn) do
    {:ok, parser} = :exml_stream.reset_parser(parser)
    %{conn | parser: parser}
  end

  defp parse_stanza(%Conn{jid: jid, owner: owner, parser: parser} = conn, data) do
    {:ok, parser, stanzas} = :exml_stream.parse(parser, data)
    for stanza <- stanzas do
      _ = Kernel.send owner, {:stanza_received, stanza}
    end
    {:ok, %Conn{conn | parser: parser}, stanzas}
  end

  def send(%Conn{socket: {mod, socket}} = conn, stanza) do
    stanza = Stanza.to_xml(stanza)
    Logger.debug fn -> "OUT > #{inspect stanza}" end
    :ok = mod.send(socket, stanza)
    {:ok, conn}
  end

  def recv({:ok, conn}, fun), do: recv(conn, fun)
  def recv(%Conn{socket: {:gen_tcp, socket}, timeout: timeout} = conn, fun) do
    receive do
      {:tcp, ^socket, stanza} ->
        :inet.setopts(socket, active: :once)
        {:ok, conn, stanzas} = parse_stanza(conn, stanza)
        fun.(conn, stanzas)
      {:tcp_closed, ^socket} ->
        {:error, :closed}
      {:tcp_error, ^socket, reason} ->
        {:error, reason}
    after timeout ->
      _ = Kernel.send(self, {:error, :timeout})
      conn
    end
  end
  def recv(%Conn{socket: {:ssl, socket}, timeout: timeout} = conn, fun) do
    receive do
      {:ssl, ^socket, stanza} ->
        :ssl.setopts(socket, active: :once)
        {:ok, conn, stanzas} = parse_stanza(conn, stanza)
        fun.(conn, stanzas)
      {:ssl_closed, ^socket} ->
        {:error, :closed}
      {:ssl_error, ^socket, reason} ->
        {:error, reason}
    after timeout ->
      _ = Kernel.send(self, {:error, :timeout})
      conn
    end
  end

  def handle_message({:tcp, socket, data}, %{socket: {:gen_tcp, socket}} = conn) do
    {:ok, _, _} = handle_data(data, conn)
  end
  def handle_message({:tcp_closed, socket}, %{socket: {:gen_tcp, socket}}) do
    {:error, :closed}
  end
  def handle_message({:tcp_error, socket, reason}, %{socket: {:gen_tcp, socket}}) do
    {:error, reason}
  end
  def handle_message({:ssl, socket, data}, %{socket: {:ssl, socket}} = conn) do
    {:ok, _, _} = handle_data(data, conn)
  end
  def handle_message({:ssl_closed, socket}, %{socket: {:ssl, socket}}) do
    {:error, :closed}
  end
  def handle_message({:ssl_error, socket, reason}, %{socket: {:ssl, socket}}) do
    {:error, reason}
  end
  def handle_message(_, _), do: :unknown

  defp handle_data(msg, %{socket: socket} = conn) do
    :ok = activate(socket)
    {:ok, _conn, _stanza} = parse_stanza(conn, msg)
  end

  defp activate({:gen_tcp, socket}) do
    case :inet.setopts(socket, [active: :once]) do
      :ok ->
        :ok
      {:error, :closed} ->
        _ = Kernel.send(self, {:tcp_closed, socket})
        :ok
      {:error, reason} ->
        _ = Kernel.send(self, {:tcp_error, socket, reason})
        :ok
    end
  end
  defp activate({:ssl, socket}) do
    case :ssl.setopts(socket, [active: :once]) do
      :ok ->
        :ok
      {:error, :closed} ->
        _ = Kernel.send(self, {:ssl_closed, socket})
        :ok
      {:error, reason} ->
        _ = Kernel.send(self, {:ssl_error, socket, reason})
        :ok
    end
  end

  defp host(jid) do
    Romeo.JID.parse(jid).server
  end
end


Cleanup Util.Table documentation.

Cleanup Util.Table documentation.

defmodule Wobserver.Table do
  @moduledoc ~S"""
  Table (ets) information and listing.
  """

  import Wobserver.Util.Helper, only: [string_to_module: 1]

  @doc ~S"""
  Lists all tables with basic information.

  Note: data is not included.
  """
  @spec list :: list(map)
  def list do
    :ets.all
    |> Enum.map(&info/1)
  end

  @doc """
  Creates an overview of table information based on the given `table` atom or number.

  If `include_data` is set to `true`, it will also contain the table data.
  """
  @spec info(table :: atom | integer, include_data :: boolean) :: map
  def info(table, include_data \\ false)

  def info(table, false) do
    %{
      id: table,
      name: :ets.info(table, :name),
      type: :ets.info(table, :type),
      size: :ets.info(table, :size),
      memory: :ets.info(table, :memory),
      owner: :ets.info(table, :owner),
      protection: :ets.info(table, :protection),
      meta: %{
        read_concurrency: :ets.info(table, :read_concurrency),
        write_concurrency: :ets.info(table, :write_concurrency),
        compressed: :ets.info(table, :compressed),
      },
    }
  end

  def info(table, true) do
    table
    |> info(false)
    |> Map.put(:data, data(table))
  end

  @doc ~S"""
  Sanitizes a `table` name and returns either the table id or name.

  Example:
  ```bash
  iex> Wobserver.Table.sanitize :code
  :code
  ```
  ```bash
  iex> Wobserver.Table.sanitize 1
  1
  ```
  ```bash
  iex> Wobserver.Table.sanitize "code"
  :code
  ```
  ```bash
  iex> Wobserver.Table.sanitize "1"
  1
  ```
  """
  @spec sanitize(table :: atom | integer | String.t) :: atom | integer
  def sanitize(table) when is_atom(table), do: table
  def sanitize(table) when is_integer(table), do: table
  def sanitize(table) when is_binary(table) do
    case Integer.parse(table) do
      {nr, ""} -> nr
      _ -> table |> string_to_module()
    end
  end

  # Helpers

  defp data(table) do
    case :ets.info(table, :protection) do
      :private ->
        []
      _ ->
        table
        |> :ets.match(:"$1")
        |> Enum.map(&data_row/1)
    end
  end

  defp data_row([row]) do
    row
    |> Tuple.to_list
    |> Enum.map(&(to_string(:io_lib.format("~tp", [&1]))))
  end
end

defmodule Wobserver.Table do
  @moduledoc ~S"""
  Table (ets) information and listing.
  """

  import Wobserver.Util.Helper, only: [string_to_module: 1]

  @doc ~S"""
  Lists all tables with basic information.

  Note: data is not included.
  """
  @spec list :: list(map)
  def list do
    :ets.all
    |> Enum.map(&info/1)
  end

  @doc """
  Retreives table information.

  If `include_data` is set to `true`, it will also contain the table data.
  """
  @spec info(table :: atom | integer, include_data :: boolean) :: map
  def info(table, include_data \\ false)

  def info(table, false) do
    %{
      id: table,
      name: :ets.info(table, :name),
      type: :ets.info(table, :type),
      size: :ets.info(table, :size),
      memory: :ets.info(table, :memory),
      owner: :ets.info(table, :owner),
      protection: :ets.info(table, :protection),
      meta: %{
        read_concurrency: :ets.info(table, :read_concurrency),
        write_concurrency: :ets.info(table, :write_concurrency),
        compressed: :ets.info(table, :compressed),
      },
    }
  end

  def info(table, true) do
    table
    |> info(false)
    |> Map.put(:data, data(table))
  end

  @doc ~S"""
  Sanitizes a table name and returns either the table id or name.

  Example:
  ```bash
  iex> Wobserver.Table.sanitize :code
  :code
  ```
  ```bash
  iex> Wobserver.Table.sanitize 1
  1
  ```
  ```bash
  iex> Wobserver.Table.sanitize "code"
  :code
  ```
  ```bash
  iex> Wobserver.Table.sanitize "1"
  1
  ```
  """
  @spec sanitize(table :: atom | integer | String.t) :: atom | integer
  def sanitize(table) when is_atom(table), do: table
  def sanitize(table) when is_integer(table), do: table
  def sanitize(table) when is_binary(table) do
    case Integer.parse(table) do
      {nr, ""} -> nr
      _ -> table |> string_to_module()
    end
  end

  # Helpers

  defp data(table) do
    case :ets.info(table, :protection) do
      :private ->
        []
      _ ->
        table
        |> :ets.match(:"$1")
        |> Enum.map(&data_row/1)
    end
  end

  defp data_row([row]) do
    row
    |> Tuple.to_list
    |> Enum.map(&(to_string(:io_lib.format("~tp", [&1]))))
  end
end