s/ExprPipe/BlockPipe

s/ExprPipe/BlockPipe

# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
  #
  # You can have as many configs as you like in the `configs:` field.
  configs: [
    %{
      #
      # Run any config using `mix credo -C <name>`. If no config name is given
      # "default" is used.
      #
      name: "default",
      #
      # These are the files included in the analysis:
      files: %{
        #
        # You can give explicit globs or simply directories.
        # In the latter case `**/*.{ex,exs}` will be used.
        #
        included: [
          "lib/",
          "src/",
          "test/",
          "web/",
          "apps/*/lib/",
          "apps/*/src/",
          "apps/*/test/",
          "apps/*/web/"
        ],
        excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
      },
      #
      # Load and configure plugins here:
      #
      plugins: [],
      #
      # If you create your own checks, you must specify the source files for
      # them here, so they can be loaded by Credo before running the analysis.
      #
      requires: [],
      #
      # If you want to enforce a style guide and need a more traditional linting
      # experience, you can change `strict` to `true` below:
      #
      strict: false,
      #
      # To modify the timeout for parsing files, change this value:
      #
      parse_timeout: 5000,
      #
      # If you want to use uncolored output by default, you can change `color`
      # to `false` below:
      #
      color: true,
      #
      # You can customize the parameters of any check by adding a second element
      # to the tuple.
      #
      # To disable a check put `false` as second element:
      #
      #     {Credo.Check.Design.DuplicatedCode, false}
      #
      checks: [
        #
        ## Consistency Checks
        #
        {Credo.Check.Consistency.ExceptionNames, []},
        {Credo.Check.Consistency.LineEndings, []},
        {Credo.Check.Consistency.ParameterPatternMatching, []},
        {Credo.Check.Consistency.SpaceAroundOperators, []},
        {Credo.Check.Consistency.SpaceInParentheses, []},
        {Credo.Check.Consistency.TabsOrSpaces, []},

        #
        ## Design Checks
        #
        # You can customize the priority of any check
        # Priority values are: `low, normal, high, higher`
        #
        {Credo.Check.Design.AliasUsage,
         [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
        # You can also customize the exit_status of each check.
        # If you don't want TODO comments to cause `mix credo` to fail, just
        # set this value to 0 (zero).
        #
        {Credo.Check.Design.TagTODO, [exit_status: 2]},
        {Credo.Check.Design.TagFIXME, []},

        #
        ## Readability Checks
        #
        {Credo.Check.Readability.AliasOrder, []},
        {Credo.Check.Readability.FunctionNames, []},
        {Credo.Check.Readability.LargeNumbers, []},
        {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
        {Credo.Check.Readability.ModuleAttributeNames, []},
        {Credo.Check.Readability.ModuleDoc, []},
        {Credo.Check.Readability.ModuleNames, []},
        {Credo.Check.Readability.ParenthesesInCondition, []},
        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
        {Credo.Check.Readability.PredicateFunctionNames, []},
        {Credo.Check.Readability.PreferImplicitTry, []},
        {Credo.Check.Readability.RedundantBlankLines, []},
        {Credo.Check.Readability.Semicolons, []},
        {Credo.Check.Readability.SeparateAliasRequire, []},
        {Credo.Check.Readability.SpaceAfterCommas, []},
        {Credo.Check.Readability.StringSigils, []},
        {Credo.Check.Readability.TrailingBlankLine, []},
        {Credo.Check.Readability.TrailingWhiteSpace, []},
        {Credo.Check.Readability.UnnecessaryAliasExpansion, []},
        {Credo.Check.Readability.VariableNames, []},

        #
        ## Refactoring Opportunities
        #
        {Credo.Check.Refactor.CondStatements, []},
        {Credo.Check.Refactor.CyclomaticComplexity, []},
        {Credo.Check.Refactor.FunctionArity, []},
        {Credo.Check.Refactor.LongQuoteBlocks, []},
        {Credo.Check.Refactor.MapInto, []},
        {Credo.Check.Refactor.MatchInCondition, []},
        {Credo.Check.Refactor.NegatedConditionsInUnless, []},
        {Credo.Check.Refactor.NegatedConditionsWithElse, []},
        {Credo.Check.Refactor.Nesting, []},
        {Credo.Check.Refactor.UnlessWithElse, []},
        {Credo.Check.Refactor.WithClauses, []},

        #
        ## Warnings
        #
        {Credo.Check.Warning.BoolOperationOnSameValues, []},
        {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
        {Credo.Check.Warning.IExPry, []},
        {Credo.Check.Warning.IoInspect, []},
        {Credo.Check.Warning.LazyLogging, []},
        {Credo.Check.Warning.MixEnv, false},
        {Credo.Check.Warning.OperationOnSameValues, []},
        {Credo.Check.Warning.OperationWithConstantResult, []},
        {Credo.Check.Warning.RaiseInsideRescue, []},
        {Credo.Check.Warning.UnusedEnumOperation, []},
        {Credo.Check.Warning.UnusedFileOperation, []},
        {Credo.Check.Warning.UnusedKeywordOperation, []},
        {Credo.Check.Warning.UnusedListOperation, []},
        {Credo.Check.Warning.UnusedPathOperation, []},
        {Credo.Check.Warning.UnusedRegexOperation, []},
        {Credo.Check.Warning.UnusedStringOperation, []},
        {Credo.Check.Warning.UnusedTupleOperation, []},
        {Credo.Check.Warning.UnsafeExec, []},

        #
        # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)

        #
        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
        #
        {Credo.Check.Readability.StrictModuleLayout, false},
        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
        {Credo.Check.Consistency.UnusedVariableNames, false},
        {Credo.Check.Design.DuplicatedCode, false},
        {Credo.Check.Readability.AliasAs, false},
        {Credo.Check.Readability.MultiAlias, false},
        {Credo.Check.Readability.Specs, false},
        {Credo.Check.Readability.SinglePipe, false},
        {Credo.Check.Readability.BlockPipe, false},
        {Credo.Check.Readability.WithCustomTaggedTuple, false},
        {Credo.Check.Refactor.ABCSize, false},
        {Credo.Check.Refactor.AppendSingleItem, false},
        {Credo.Check.Refactor.DoubleBooleanNegation, false},
        {Credo.Check.Refactor.ModuleDependencies, false},
        {Credo.Check.Refactor.NegatedIsNil, false},
        {Credo.Check.Refactor.PipeChainStart, false},
        {Credo.Check.Refactor.VariableRebinding, false},
        {Credo.Check.Warning.LeakyEnvironment, false},
        {Credo.Check.Warning.MapGetUnsafePass, false},
        {Credo.Check.Warning.UnsafeToAtom, false}

        #
        # Custom checks can be created using `mix credo.gen.check`.
        #
      ]
    }
  ]
}

# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
  #
  # You can have as many configs as you like in the `configs:` field.
  configs: [
    %{
      #
      # Run any config using `mix credo -C <name>`. If no config name is given
      # "default" is used.
      #
      name: "default",
      #
      # These are the files included in the analysis:
      files: %{
        #
        # You can give explicit globs or simply directories.
        # In the latter case `**/*.{ex,exs}` will be used.
        #
        included: [
          "lib/",
          "src/",
          "test/",
          "web/",
          "apps/*/lib/",
          "apps/*/src/",
          "apps/*/test/",
          "apps/*/web/"
        ],
        excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
      },
      #
      # Load and configure plugins here:
      #
      plugins: [],
      #
      # If you create your own checks, you must specify the source files for
      # them here, so they can be loaded by Credo before running the analysis.
      #
      requires: [],
      #
      # If you want to enforce a style guide and need a more traditional linting
      # experience, you can change `strict` to `true` below:
      #
      strict: false,
      #
      # To modify the timeout for parsing files, change this value:
      #
      parse_timeout: 5000,
      #
      # If you want to use uncolored output by default, you can change `color`
      # to `false` below:
      #
      color: true,
      #
      # You can customize the parameters of any check by adding a second element
      # to the tuple.
      #
      # To disable a check put `false` as second element:
      #
      #     {Credo.Check.Design.DuplicatedCode, false}
      #
      checks: [
        #
        ## Consistency Checks
        #
        {Credo.Check.Consistency.ExceptionNames, []},
        {Credo.Check.Consistency.LineEndings, []},
        {Credo.Check.Consistency.ParameterPatternMatching, []},
        {Credo.Check.Consistency.SpaceAroundOperators, []},
        {Credo.Check.Consistency.SpaceInParentheses, []},
        {Credo.Check.Consistency.TabsOrSpaces, []},

        #
        ## Design Checks
        #
        # You can customize the priority of any check
        # Priority values are: `low, normal, high, higher`
        #
        {Credo.Check.Design.AliasUsage,
         [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
        # You can also customize the exit_status of each check.
        # If you don't want TODO comments to cause `mix credo` to fail, just
        # set this value to 0 (zero).
        #
        {Credo.Check.Design.TagTODO, [exit_status: 2]},
        {Credo.Check.Design.TagFIXME, []},

        #
        ## Readability Checks
        #
        {Credo.Check.Readability.AliasOrder, []},
        {Credo.Check.Readability.FunctionNames, []},
        {Credo.Check.Readability.LargeNumbers, []},
        {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
        {Credo.Check.Readability.ModuleAttributeNames, []},
        {Credo.Check.Readability.ModuleDoc, []},
        {Credo.Check.Readability.ModuleNames, []},
        {Credo.Check.Readability.ParenthesesInCondition, []},
        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
        {Credo.Check.Readability.PredicateFunctionNames, []},
        {Credo.Check.Readability.PreferImplicitTry, []},
        {Credo.Check.Readability.RedundantBlankLines, []},
        {Credo.Check.Readability.Semicolons, []},
        {Credo.Check.Readability.SeparateAliasRequire, []},
        {Credo.Check.Readability.SpaceAfterCommas, []},
        {Credo.Check.Readability.StringSigils, []},
        {Credo.Check.Readability.TrailingBlankLine, []},
        {Credo.Check.Readability.TrailingWhiteSpace, []},
        {Credo.Check.Readability.UnnecessaryAliasExpansion, []},
        {Credo.Check.Readability.VariableNames, []},

        #
        ## Refactoring Opportunities
        #
        {Credo.Check.Refactor.CondStatements, []},
        {Credo.Check.Refactor.CyclomaticComplexity, []},
        {Credo.Check.Refactor.FunctionArity, []},
        {Credo.Check.Refactor.LongQuoteBlocks, []},
        {Credo.Check.Refactor.MapInto, []},
        {Credo.Check.Refactor.MatchInCondition, []},
        {Credo.Check.Refactor.NegatedConditionsInUnless, []},
        {Credo.Check.Refactor.NegatedConditionsWithElse, []},
        {Credo.Check.Refactor.Nesting, []},
        {Credo.Check.Refactor.UnlessWithElse, []},
        {Credo.Check.Refactor.WithClauses, []},

        #
        ## Warnings
        #
        {Credo.Check.Warning.BoolOperationOnSameValues, []},
        {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
        {Credo.Check.Warning.IExPry, []},
        {Credo.Check.Warning.IoInspect, []},
        {Credo.Check.Warning.LazyLogging, []},
        {Credo.Check.Warning.MixEnv, false},
        {Credo.Check.Warning.OperationOnSameValues, []},
        {Credo.Check.Warning.OperationWithConstantResult, []},
        {Credo.Check.Warning.RaiseInsideRescue, []},
        {Credo.Check.Warning.UnusedEnumOperation, []},
        {Credo.Check.Warning.UnusedFileOperation, []},
        {Credo.Check.Warning.UnusedKeywordOperation, []},
        {Credo.Check.Warning.UnusedListOperation, []},
        {Credo.Check.Warning.UnusedPathOperation, []},
        {Credo.Check.Warning.UnusedRegexOperation, []},
        {Credo.Check.Warning.UnusedStringOperation, []},
        {Credo.Check.Warning.UnusedTupleOperation, []},
        {Credo.Check.Warning.UnsafeExec, []},

        #
        # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)

        #
        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
        #
        {Credo.Check.Readability.StrictModuleLayout, false},
        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
        {Credo.Check.Consistency.UnusedVariableNames, false},
        {Credo.Check.Design.DuplicatedCode, false},
        {Credo.Check.Readability.AliasAs, false},
        {Credo.Check.Readability.MultiAlias, false},
        {Credo.Check.Readability.Specs, false},
        {Credo.Check.Readability.SinglePipe, false},
        {Credo.Check.Readability.ExprPipe, false},
        {Credo.Check.Readability.WithCustomTaggedTuple, false},
        {Credo.Check.Refactor.ABCSize, false},
        {Credo.Check.Refactor.AppendSingleItem, false},
        {Credo.Check.Refactor.DoubleBooleanNegation, false},
        {Credo.Check.Refactor.ModuleDependencies, false},
        {Credo.Check.Refactor.NegatedIsNil, false},
        {Credo.Check.Refactor.PipeChainStart, false},
        {Credo.Check.Refactor.VariableRebinding, false},
        {Credo.Check.Warning.LeakyEnvironment, false},
        {Credo.Check.Warning.MapGetUnsafePass, false},
        {Credo.Check.Warning.UnsafeToAtom, false}

        #
        # Custom checks can be created using `mix credo.gen.check`.
        #
      ]
    }
  ]
}


List updated

List updated

defmodule MyList do

    # basecase: an empty list
    def flatten([]), do : []

    # non-empty list with more than one element
    def flatten([tail | head]) do
        flatten(head) ++ flatten(tail)
    end

    # single element list
    def flatten(head), do : [head]
end

defmodule MyList do
    def flatten([]), do : []

end


Series of template interfaces as unordered list

defmodule ExDocEPUB.Formatter.EPUB.Templates do
  @moduledoc """
  Handle all template interfaces for the EPUB formatter.

  ## Functions

  * `content_template/5`: Creates the [Package Document Definition][package],
  this definition encapsulates the publication metadata and the resource
  information that constitute the EPUB publication. This definition also
  includes the default reading order.
  * `detail_template/2`: Returns the details of an individual *function*,
  *macro* or *callback*.  This function is required used by
  `module_template/6`.
  * `module_template/6`: Creates a chapter which contains all the details
  about an individual module, this chapter can include the following
  sections: *functions*, *macros*, *types*, *callbacks*.
  * `nav_template/3`: Creates the table of contents. This template follows the
  [EPUB Navigation Document Definition][navigation].
  * `readme_template/2`: Creates a new chapter when the user provides a
  `README` file.
  * `summary_template/1`: Creates a summary of the *functions* and *macros*
  available for an individual module, this function is required by
  `module_template/6`.
  * `title_template/1`: Creates the cover page for the EPUB document.
  * `toc_template/4`: Creates an *Navigation Center eXtended* document (as
  defined in OPF 2.0.1), this is for compatibility purposes with EPUB 2
  Reading Systems.  EPUB 3 Reading Systems must ignore the NCX in favor of
  the [EPUB Navigation Document][navigation].
  * `type_detail_template/1`: Returns all the details of an individual
  *type*. This function is required by `module_template/6`.

  [package]: http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-package-def
  [navigation]: http://www.idpf.org/epub/30/spec/epub30-contentdocs.html#sec-xhtml-nav
  """

  require EEx

  @doc """
  Generate content from the module template for a given `node`
  """
  def module_page(config, node) do
    types       = node.typespecs
    functions   = Enum.filter node.docs, & &1.type in [:def]
    macros      = Enum.filter node.docs, & &1.type in [:defmacro]
    callbacks   = Enum.filter node.docs, & &1.type in [:defcallback, :defmacrocallback]
    module_template(config, node, types, functions, macros, callbacks)
  end

  # Get the full specs from a function, already in HTML form.
  defp get_specs(%ExDoc.FunctionNode{specs: specs}) when is_list(specs) do
    presence specs
  end

  defp get_specs(_node), do: nil

  # Convert markdown to HTML.
  defp to_html(nil), do: nil
  defp to_html(bin) when is_binary(bin), do: ExDoc.Markdown.to_html(bin)

  # Get the pretty name of a function node
  defp pretty_type(%ExDoc.FunctionNode{type: t}) do
    case t do
      :def              -> "function"
      :defmacro         -> "macro"
      :defcallback      -> "callback"
      :defmacrocallback -> "macro callback"
      :type             -> "type"
    end
  end

  # Generate a link id
  defp link_id(node), do: link_id(node.id, node.type)
  defp link_id(id, type) do
    case type do
      :defmacrocallback -> "c:#{id}"
      :defcallback      -> "c:#{id}"
      :type             -> "t:#{id}"
      _                 -> "#{id}"
    end
  end

  # Get the first paragraph of the documentation of a node, if any.
  defp synopsis(nil), do: nil
  defp synopsis(doc) do
    String.split(doc, ~r/\n\s*\n/) |> hd |> String.strip() |> String.rstrip(?.)
  end

  defp presence([]),    do: nil
  defp presence(other), do: other

  defp h(binary) do
    escape_map = [{"&", "&"}, {"<", "<"}, {">", ">"}, {"\"", """}]
    Enum.reduce escape_map, binary, fn({pattern, escape}, acc) ->
      String.replace(acc, pattern, escape)
    end
  end

  # Keep only valid characters for an XHTML ID element
  defp valid_id(binary) do
    String.replace(binary, ~r/[^A-Za-z0-9:_.-]/, "")
  end

  templates = [
    content_template: [:config, :nodes, :uuid, :datetime, :has_readme],
    detail_template: [:node, :_module],
    module_template: [:config, :module, :types, :functions, :macros, :callbacks],
    nav_template: [:config, :nodes, :has_readme],
    readme_template: [:config, :content],
    summary_template: [:node],
    title_template: [:config],
    toc_template: [:config, :nodes, :uuid, :has_readme],
    type_detail_template: [:node]
  ]

  Enum.each templates, fn({ name, args }) ->
    filename = Path.expand("templates/#{name}.eex", __DIR__)
    EEx.function_from_file :def, name, filename, args
  end
end

defmodule ExDocEPUB.Formatter.EPUB.Templates do
  @moduledoc """
  Handle all template interfaces for the EPUB formatter.

  ## Functions

  `content_template/5`: Creates the [Package Document Definition][package],
  this definition encapsulates the publication metadata and the resource
  information that constitute the EPUB publication. This definition also
  includes the default reading order.

  `detail_template/2`: Returns the details of an individual *function*,
  *macro* or *callback*.  This function is required used by
  `module_template/6`.

  `module_template/6`: Creates a chapter which contains all the details
  about an individual module, this chapter can include the following
  sections: *functions*, *macros*, *types*, *callbacks*.

  `nav_template/3`: Creates the table of contents. This template follows the
  [EPUB Navigation Document Definition][navigation].

  `readme_template/2`: Creates a new chapter when the user provides a
  `README` file.

  `summary_template/1`: Creates a summary of the *functions* and *macros*
  available for an individual module, this function is required by
  `module_template/6`.

  `title_template/1`: Creates the cover page for the EPUB document.

  `toc_template/4`: Creates an *Navigation Center eXtended* document (as
  defined in OPF 2.0.1), this is for compatibility purposes with EPUB 2
  Reading Systems.  EPUB 3 Reading Systems must ignore the NCX in favor of
  the [EPUB Navigation Document][navigation].

  `type_detail_template/1`: Returns all the details of an individual
  *type*. This function is required by `module_template/6`.

  [package]: http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-package-def
  [navigation]: http://www.idpf.org/epub/30/spec/epub30-contentdocs.html#sec-xhtml-nav
  """

  require EEx

  @doc """
  Generate content from the module template for a given `node`
  """
  def module_page(config, node) do
    types       = node.typespecs
    functions   = Enum.filter node.docs, & &1.type in [:def]
    macros      = Enum.filter node.docs, & &1.type in [:defmacro]
    callbacks   = Enum.filter node.docs, & &1.type in [:defcallback, :defmacrocallback]
    module_template(config, node, types, functions, macros, callbacks)
  end

  # Get the full specs from a function, already in HTML form.
  defp get_specs(%ExDoc.FunctionNode{specs: specs}) when is_list(specs) do
    presence specs
  end

  defp get_specs(_node), do: nil

  # Convert markdown to HTML.
  defp to_html(nil), do: nil
  defp to_html(bin) when is_binary(bin), do: ExDoc.Markdown.to_html(bin)

  # Get the pretty name of a function node
  defp pretty_type(%ExDoc.FunctionNode{type: t}) do
    case t do
      :def              -> "function"
      :defmacro         -> "macro"
      :defcallback      -> "callback"
      :defmacrocallback -> "macro callback"
      :type             -> "type"
    end
  end

  # Generate a link id
  defp link_id(node), do: link_id(node.id, node.type)
  defp link_id(id, type) do
    case type do
      :defmacrocallback -> "c:#{id}"
      :defcallback      -> "c:#{id}"
      :type             -> "t:#{id}"
      _                 -> "#{id}"
    end
  end

  # Get the first paragraph of the documentation of a node, if any.
  defp synopsis(nil), do: nil
  defp synopsis(doc) do
    String.split(doc, ~r/\n\s*\n/) |> hd |> String.strip() |> String.rstrip(?.)
  end

  defp presence([]),    do: nil
  defp presence(other), do: other

  defp h(binary) do
    escape_map = [{"&", "&"}, {"<", "<"}, {">", ">"}, {"\"", """}]
    Enum.reduce escape_map, binary, fn({pattern, escape}, acc) ->
      String.replace(acc, pattern, escape)
    end
  end

  # Keep only valid characters for an XHTML ID element
  defp valid_id(binary) do
    String.replace(binary, ~r/[^A-Za-z0-9:_.-]/, "")
  end

  templates = [
    content_template: [:config, :nodes, :uuid, :datetime, :has_readme],
    detail_template: [:node, :_module],
    module_template: [:config, :module, :types, :functions, :macros, :callbacks],
    nav_template: [:config, :nodes, :has_readme],
    readme_template: [:config, :content],
    summary_template: [:node],
    title_template: [:config],
    toc_template: [:config, :nodes, :uuid, :has_readme],
    type_detail_template: [:node]
  ]

  Enum.each templates, fn({ name, args }) ->
    filename = Path.expand("templates/#{name}.eex", __DIR__)
    EEx.function_from_file :def, name, filename, args
  end
end


added prod config

added prod config

config :dynamo,
  # In production, modules are compiled up-front.
  compile_on_demand: false,
  reload_modules: false

config :server,
  port: 8888,
  acceptors: 100,
  max_connections: 10000
  dispatch: [{ :_, [
    {"/websocket", WhiteboardServer.Websocket, [] },
    {:_, Dynamo.Cowboy.Handler, __MODULE__ }
  ] }]

# config :ssl,
#  port: 8889,
#  keyfile: "/var/www/key.pem",
#  certfile: "/var/www/cert.pem"

config :dynamo,
  # In production, modules are compiled up-front.
  compile_on_demand: false,
  reload_modules: false

config :server,
  port: 8888,
  acceptors: 100,
  max_connections: 10000

# config :ssl,
#  port: 8889,
#  keyfile: "/var/www/key.pem",
#  certfile: "/var/www/cert.pem"


Change path in test to reflect new route

defmodule VisitorViewsPostTest do
  use Tilex.IntegrationCase, async: Application.get_env(:tilex, :async_feature_test)

  alias TilexWeb.Endpoint

  alias Tilex.Integration.Pages.{
    PostShowPage
  }

  test "the page shows a post", %{session: session} do
    Application.put_env(:tilex, :date_display_tz, "")

    developer = Factory.insert!(:developer)
    channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        title: "A special post",
        body: "This is how to be super awesome!",
        inserted_at: Timex.to_datetime({2018, 2, 2}),
        developer: developer,
        channel: channel
      )

    session
    |> PostShowPage.navigate(post)
    |> PostShowPage.expect_post_attributes(%{
      title: "A special post",
      body: "This is how to be super awesome!",
      channel: "#command-line",
      likes_count: 1
    })

    assert post_date(session) == "February 2, 2018"

    assert page_title(session) == "A special post - Today I Learned"

    last_request_query =
      from(r in "requests",
        select: %{page: r.page},
        order_by: [desc: r.request_time],
        limit: 1
      )

    assert %{page: path} = Repo.one(last_request_query)

    assert path =~ post.slug
  end

  test "the page shows a post with the correct timezone if given", %{session: session} do
    Application.put_env(:tilex, :date_display_tz, "America/Chicago")

    developer = Factory.insert!(:developer)
    channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        title: "A special post",
        body: "This is how to be super awesome!",
        inserted_at: Timex.to_datetime({2018, 2, 2}),
        developer: developer,
        channel: channel
      )

    session |> PostShowPage.navigate(post)

    assert post_date(session) == "February 1, 2018"
  end

  test "and sees marketing copy, if it exists", %{session: session} do
    marketing_channel = Factory.insert!(:channel, name: "elixir")
    post_in_marketing_channel = Factory.insert!(:post, channel: marketing_channel)

    copy =
      session
      |> visit(post_path(Endpoint, :show, post_in_marketing_channel))
      |> find(Query.css(".more-info"))
      |> Element.text()

    {:ok, marketing_content} = File.read("lib/tilex_web/templates/shared/_elixir.html.eex")
    assert copy =~ String.slice(marketing_content, 0, 10)
  end

  test "and sees a special slug", %{session: session} do
    post = Factory.insert!(:post, title: "Super Sluggable Title")

    url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> current_url

    assert url =~ "#{post.slug}-super-sluggable-title"

    changeset = Post.changeset(post, %{title: "Alternate Also Cool Title"})
    Repo.update!(changeset)
    post = Repo.get(Post, post.id)

    url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> current_url

    assert url =~ "#{post.slug}-alternate-also-cool-title"
  end

  test "and sees a channel specific twitter card and a post specific twitter description", %{
    session: session
  } do
    popular_channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        channel: popular_channel,
        body: "One sentence that sets up the post.\nAnother sentence that is more informative"
      )

    image_url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> find(Query.css("meta[name='twitter:image']", visible: false))
      |> Element.attr("content")

    assert image_url =~ "command_line_twitter_card.png"

    twitter_description =
      session
      |> find(Query.css("meta[name='twitter:description']", visible: false))
      |> Element.attr("content")

    assert twitter_description =~ "One sentence that sets up the post."
    refute twitter_description =~ "Another sentence"
  end

  test "and sees a post specific twitter description WITHOUT markdown", %{
    session: session
  } do
    popular_channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        channel: popular_channel,
        body:
          "One [sentence](http://www.example.com) that is a link. \nAnother sentence that is more informative"
      )

    twitter_description =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> find(Query.css("meta[name='twitter:description']", visible: false))
      |> Element.attr("content")

    assert twitter_description =~ "One sentence that is a link."
    refute twitter_description =~ "Another sentence"
  end

  test "and clicks 'like' for that post", %{session: session} do
    Tilex.DateTimeMock.start_link([])
    developer = Factory.insert!(:developer)
    post = Factory.insert!(:post, title: "A special post", developer: developer, likes: 1)

    session
    |> visit(post_path(Endpoint, :show, post))
    |> find(Query.css("header[data-likes-loaded=true]"))

    link = find(session, Query.css(".post .js-like-action"))

    Element.click(link)

    session
    |> assert_has(Query.css("header[data-likes-loaded=true]"))
    |> assert_has(Query.css(".post .js-like-action.liked"))

    post = Repo.get(Post, post.id)
    assert post.likes == 2
    assert post.max_likes == 2

    Element.click(link)

    session
    |> assert_has(Query.css("header[data-likes-loaded=true]"))
    |> assert_has(Query.css(".post .js-like-action"))
    |> refute_has(Query.css(".post .js-like-action.liked"))

    post = Repo.get(Post, post.id)
    assert post.likes == 1
    assert post.max_likes == 2
  end

  test "sees raw markdown version", %{session: session} do
    title = "A special post"

    body = """
    # title
    **some text**
    [hashrocket](http://hashrocket.com)
    """

    developer = Factory.insert!(:developer)

    post =
      Factory.insert!(
        :post,
        title: title,
        body: body,
        developer: developer
      )

    session
    |> visit("#{post_path(Endpoint, :show, post)}.md")

    assert text(session) ==
             String.trim("""
             #{title}

             #{body}

             #{developer.username}

             #{TilexWeb.SharedView.display_date(post)}
             """)
  end

  test "via the random url", %{session: session} do
    post = Factory.insert!(:post)

    session
    |> visit(post_path(Endpoint, :random))
    |> PostShowPage.expect_post_attributes(%{
      title: post.title,
      body: post.body,
      channel: post.channel.name,
      likes_count: 1
    })

    assert page_title(session) == "#{post.title} - Today I Learned"
  end

  test "via the random by channel url", %{session: session} do
    post = Factory.insert!(:post)

    session
    |> visit(channel_path(Endpoint, :random_by_channel, post.channel.name))
    |> PostShowPage.expect_post_attributes(%{
      title: post.title,
      body: post.body,
      channel: post.channel.name,
      likes_count: 1
    })
    |> assert_text(Query.css(".post__tag-link"), "##{String.upcase(post.channel.name)}")
  end

  defp post_date(session) do
    session
    |> find(Query.css("footer .post__permalink"))
    |> Element.text()
  end
end

defmodule VisitorViewsPostTest do
  use Tilex.IntegrationCase, async: Application.get_env(:tilex, :async_feature_test)

  alias TilexWeb.Endpoint

  alias Tilex.Integration.Pages.{
    PostShowPage
  }

  test "the page shows a post", %{session: session} do
    Application.put_env(:tilex, :date_display_tz, "")

    developer = Factory.insert!(:developer)
    channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        title: "A special post",
        body: "This is how to be super awesome!",
        inserted_at: Timex.to_datetime({2018, 2, 2}),
        developer: developer,
        channel: channel
      )

    session
    |> PostShowPage.navigate(post)
    |> PostShowPage.expect_post_attributes(%{
      title: "A special post",
      body: "This is how to be super awesome!",
      channel: "#command-line",
      likes_count: 1
    })

    assert post_date(session) == "February 2, 2018"

    assert page_title(session) == "A special post - Today I Learned"

    last_request_query =
      from(r in "requests",
        select: %{page: r.page},
        order_by: [desc: r.request_time],
        limit: 1
      )

    assert %{page: path} = Repo.one(last_request_query)

    assert path =~ post.slug
  end

  test "the page shows a post with the correct timezone if given", %{session: session} do
    Application.put_env(:tilex, :date_display_tz, "America/Chicago")

    developer = Factory.insert!(:developer)
    channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        title: "A special post",
        body: "This is how to be super awesome!",
        inserted_at: Timex.to_datetime({2018, 2, 2}),
        developer: developer,
        channel: channel
      )

    session |> PostShowPage.navigate(post)

    assert post_date(session) == "February 1, 2018"
  end

  test "and sees marketing copy, if it exists", %{session: session} do
    marketing_channel = Factory.insert!(:channel, name: "elixir")
    post_in_marketing_channel = Factory.insert!(:post, channel: marketing_channel)

    copy =
      session
      |> visit(post_path(Endpoint, :show, post_in_marketing_channel))
      |> find(Query.css(".more-info"))
      |> Element.text()

    {:ok, marketing_content} = File.read("lib/tilex_web/templates/shared/_elixir.html.eex")
    assert copy =~ String.slice(marketing_content, 0, 10)
  end

  test "and sees a special slug", %{session: session} do
    post = Factory.insert!(:post, title: "Super Sluggable Title")

    url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> current_url

    assert url =~ "#{post.slug}-super-sluggable-title"

    changeset = Post.changeset(post, %{title: "Alternate Also Cool Title"})
    Repo.update!(changeset)
    post = Repo.get(Post, post.id)

    url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> current_url

    assert url =~ "#{post.slug}-alternate-also-cool-title"
  end

  test "and sees a channel specific twitter card and a post specific twitter description", %{
    session: session
  } do
    popular_channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        channel: popular_channel,
        body: "One sentence that sets up the post.\nAnother sentence that is more informative"
      )

    image_url =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> find(Query.css("meta[name='twitter:image']", visible: false))
      |> Element.attr("content")

    assert image_url =~ "command_line_twitter_card.png"

    twitter_description =
      session
      |> find(Query.css("meta[name='twitter:description']", visible: false))
      |> Element.attr("content")

    assert twitter_description =~ "One sentence that sets up the post."
    refute twitter_description =~ "Another sentence"
  end

  test "and sees a post specific twitter description WITHOUT markdown", %{
    session: session
  } do
    popular_channel = Factory.insert!(:channel, name: "command-line")

    post =
      Factory.insert!(
        :post,
        channel: popular_channel,
        body:
          "One [sentence](http://www.example.com) that is a link. \nAnother sentence that is more informative"
      )

    twitter_description =
      session
      |> visit(post_path(Endpoint, :show, post))
      |> find(Query.css("meta[name='twitter:description']", visible: false))
      |> Element.attr("content")

    assert twitter_description =~ "One sentence that is a link."
    refute twitter_description =~ "Another sentence"
  end

  test "and clicks 'like' for that post", %{session: session} do
    Tilex.DateTimeMock.start_link([])
    developer = Factory.insert!(:developer)
    post = Factory.insert!(:post, title: "A special post", developer: developer, likes: 1)

    session
    |> visit(post_path(Endpoint, :show, post))
    |> find(Query.css("header[data-likes-loaded=true]"))

    link = find(session, Query.css(".post .js-like-action"))

    Element.click(link)

    session
    |> assert_has(Query.css("header[data-likes-loaded=true]"))
    |> assert_has(Query.css(".post .js-like-action.liked"))

    post = Repo.get(Post, post.id)
    assert post.likes == 2
    assert post.max_likes == 2

    Element.click(link)

    session
    |> assert_has(Query.css("header[data-likes-loaded=true]"))
    |> assert_has(Query.css(".post .js-like-action"))
    |> refute_has(Query.css(".post .js-like-action.liked"))

    post = Repo.get(Post, post.id)
    assert post.likes == 1
    assert post.max_likes == 2
  end

  test "sees raw markdown version", %{session: session} do
    title = "A special post"

    body = """
    # title
    **some text**
    [hashrocket](http://hashrocket.com)
    """

    developer = Factory.insert!(:developer)

    post =
      Factory.insert!(
        :post,
        title: title,
        body: body,
        developer: developer
      )

    session
    |> visit("#{post_path(Endpoint, :show, post)}.md")

    assert text(session) ==
             String.trim("""
             #{title}

             #{body}

             #{developer.username}

             #{TilexWeb.SharedView.display_date(post)}
             """)
  end

  test "via the random url", %{session: session} do
    post = Factory.insert!(:post)

    session
    |> visit(post_path(Endpoint, :random))
    |> PostShowPage.expect_post_attributes(%{
      title: post.title,
      body: post.body,
      channel: post.channel.name,
      likes_count: 1
    })

    assert page_title(session) == "#{post.title} - Today I Learned"
  end

  test "via the random by channel url", %{session: session} do
    post = Factory.insert!(:post)

    session
    |> visit(post_path(Endpoint, :random_by_channel, post.channel.name))
    |> PostShowPage.expect_post_attributes(%{
      title: post.title,
      body: post.body,
      channel: post.channel.name,
      likes_count: 1
    })
    |> assert_text(Query.css(".post__tag-link"), "##{String.upcase(post.channel.name)}")
  end

  defp post_date(session) do
    session
    |> find(Query.css("footer .post__permalink"))
    |> Element.text()
  end
end


Temporarily require ecto in deps until final release

defmodule <%= web_namespace %>.MixProject do
  use Mix.Project

  def project do
    [
      app: :<%= web_app_name %>,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {<%= web_namespace %>.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      <%= phoenix_dep %>,
      {:phoenix_pubsub, "~> 1.1"},<%= if ecto do %>
      # TODO: temporarily requires ecto until final release
      {:ecto, "~> 3.0-rc", override: true},
      {:phoenix_ecto, "~> 3.5"},<% end %><%= if html do %>
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2-rc", only: :dev},<% end %>
      {:gettext, "~> 0.11"},<%= if app_name != web_app_name do %>
      {:<%= app_name %>, in_umbrella: true},<% end %>
      {:jason, "~> 1.0"},
      {:cowboy, "~> 1.0"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.<%= if ecto do %>
  # For example, we extend the test task to create and migrate the database.<% end %>
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [<%= if ecto do %>test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %>]
  end
end

defmodule <%= web_namespace %>.MixProject do
  use Mix.Project

  def project do
    [
      app: :<%= web_app_name %>,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {<%= web_namespace %>.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      <%= phoenix_dep %>,
      {:phoenix_pubsub, "~> 1.1"},<%= if ecto do %>
      {:phoenix_ecto, "~> 3.5"},<% end %><%= if html do %>
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2-rc", only: :dev},<% end %>
      {:gettext, "~> 0.11"},<%= if app_name != web_app_name do %>
      {:<%= app_name %>, in_umbrella: true},<% end %>
      {:jason, "~> 1.0"},
      {:cowboy, "~> 1.0"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.<%= if ecto do %>
  # For example, we extend the test task to create and migrate the database.<% end %>
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [<%= if ecto do %>test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %>]
  end
end


Auto-publish docs when we publish to hex.

defmodule PublicSuffix.Mixfile do
  use Mix.Project

  def project do
    [app: :public_suffix,
     version: "0.3.0",
     elixir: "~> 1.2",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     aliases: aliases,
     description: description,
     package: package,
     deps: deps]
  end

  def application do
    [
      # :idna is intentionally NOT included in this list because it is
      # only used at compile time, as part of processing the publicsuffix.org
      # rules file. So it is not needed at runtime.
      applications: [
        :logger,
      ]
    ]
  end

  defp deps do
    [
      {:idna, ">= 1.2.0 and < 3.0.0"},
      # ex_doc and earmark are necessary to publish docs to hexdocs.pm.
      {:ex_doc, ">= 0.0.0", only: :dev},
      {:earmark, ">= 0.0.0", only: :dev},
    ]
  end

  defp description do
    """
    Operate on domain names using the public suffix rules provided by https://publicsuffix.org/.
    """
  end

  defp package do
    [
      licenses: ["MIT"],
      maintainers: ["Myron Marston", "Ben Kirzhner"],
      links: %{"GitHub" => "https://github.com/seomoz/publicsuffix-elixir",
               "Public Suffix List" => "https://publicsuffix.org/"},
      files: ["lib", "priv", "data/public_suffix_list.dat",
              "mix.exs", "README.md", "LICENSE", "CHANGELOG.md"],
    ]
  end

  defp aliases do
    [
      "hex.publish": ["hex.publish", &tag_version/1, "hex.docs"],
    ]
  end

  defp tag_version(_args) do
    version = Keyword.fetch!(project, :version)
    System.cmd("git", ["tag", "-a", "-m", "Version #{version}", "v#{version}"])
    System.cmd("git", ["push", "origin"])
    System.cmd("git", ["push", "origin", "--tags"])
  end
end

defmodule Mix.Tasks.PublicSuffix.SyncFiles do
  use Mix.Task

  @shortdoc "Syncs the files from publicsuffix.org"
  @data_dir Path.expand("data", __DIR__)

  def run(_) do
    File.mkdir_p!(@data_dir)
    sync_file "https://publicsuffix.org/list/public_suffix_list.dat", "public_suffix_list.dat"
    sync_file "https://raw.githubusercontent.com/publicsuffix/list/master/tests/tests.txt", "tests.txt"
  end

  defp sync_file(remote_url, local_path) do
    local_path = Path.join(@data_dir, local_path)

    [
      "curl",
      "-s",
      remote_url,
      "--output",
      local_path,
    ]
    |> Enum.join(" ")
    |> Mix.shell.cmd

    IO.puts "Synced #{remote_url} to #{local_path}"
  end
end

defmodule PublicSuffix.Mixfile do
  use Mix.Project

  def project do
    [app: :public_suffix,
     version: "0.3.0",
     elixir: "~> 1.2",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     aliases: aliases,
     description: description,
     package: package,
     deps: deps]
  end

  def application do
    [
      # :idna is intentionally NOT included in this list because it is
      # only used at compile time, as part of processing the publicsuffix.org
      # rules file. So it is not needed at runtime.
      applications: [
        :logger,
      ]
    ]
  end

  defp deps do
    [
      {:idna, ">= 1.2.0 and < 3.0.0"},
      # ex_doc and earmark are necessary to publish docs to hexdocs.pm.
      {:ex_doc, ">= 0.0.0", only: :dev},
      {:earmark, ">= 0.0.0", only: :dev},
    ]
  end

  defp description do
    """
    Operate on domain names using the public suffix rules provided by https://publicsuffix.org/.
    """
  end

  defp package do
    [
      licenses: ["MIT"],
      maintainers: ["Myron Marston", "Ben Kirzhner"],
      links: %{"GitHub" => "https://github.com/seomoz/publicsuffix-elixir",
               "Public Suffix List" => "https://publicsuffix.org/"},
      files: ["lib", "priv", "data/public_suffix_list.dat",
              "mix.exs", "README.md", "LICENSE", "CHANGELOG.md"],
    ]
  end

  defp aliases do
    [
      "hex.publish": ["hex.publish", &tag_version/1],
    ]
  end

  defp tag_version(_args) do
    version = Keyword.fetch!(project, :version)
    System.cmd("git", ["tag", "-a", "-m", "Version #{version}", "v#{version}"])
    System.cmd("git", ["push", "origin"])
    System.cmd("git", ["push", "origin", "--tags"])
  end
end

defmodule Mix.Tasks.PublicSuffix.SyncFiles do
  use Mix.Task

  @shortdoc "Syncs the files from publicsuffix.org"
  @data_dir Path.expand("data", __DIR__)

  def run(_) do
    File.mkdir_p!(@data_dir)
    sync_file "https://publicsuffix.org/list/public_suffix_list.dat", "public_suffix_list.dat"
    sync_file "https://raw.githubusercontent.com/publicsuffix/list/master/tests/tests.txt", "tests.txt"
  end

  defp sync_file(remote_url, local_path) do
    local_path = Path.join(@data_dir, local_path)

    [
      "curl",
      "-s",
      remote_url,
      "--output",
      local_path,
    ]
    |> Enum.join(" ")
    |> Mix.shell.cmd

    IO.puts "Synced #{remote_url} to #{local_path}"
  end
end


version bump

version bump

defmodule CSV.Mixfile do
  use Mix.Project

  def project do
    [
        app: :csv,
        version: "1.2.4",
        elixir: "~> 1.1",
        deps: deps,
        package: package,
        docs: &docs/0,
        name: "CSV",
        consolidate_protocols: true,
        source_url: "https://github.com/beatrichartz/csv",
        description: "CSV Decoding and Encoding for Elixir"
    ]
  end

  defp package do
    [
        maintainers: ["Beat Richartz"],
        licenses: ["MIT"],
        links: %{github: "https://github.com/beatrichartz/csv" }
    ]
  end

  defp deps do
    [
      {:benchfella, only: :bench},
      {:ex_csv, only: :bench},
      {:csvlixir, only: :bench},
      {:cesso, only: :bench},
      {:ex_doc, "0.9.0", only: :docs},
      {:inch_ex, only: :docs},
      {:earmark, "0.1.19", only: :docs}
    ]
  end

  defp docs do
    {ref, 0} = System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"])

    [
        source_ref: ref,
        main: "overview"
    ]
  end
end

defmodule CSV.Mixfile do
  use Mix.Project

  def project do
    [
        app: :csv,
        version: "1.2.3",
        elixir: "~> 1.1",
        deps: deps,
        package: package,
        docs: &docs/0,
        name: "CSV",
        consolidate_protocols: true,
        source_url: "https://github.com/beatrichartz/csv",
        description: "CSV Decoding and Encoding for Elixir"
    ]
  end

  defp package do
    [
        maintainers: ["Beat Richartz"],
        licenses: ["MIT"],
        links: %{github: "https://github.com/beatrichartz/csv" }
    ]
  end

  defp deps do
    [
      {:benchfella, only: :bench},
      {:ex_csv, only: :bench},
      {:csvlixir, only: :bench},
      {:cesso, only: :bench},
      {:ex_doc, "0.9.0", only: :docs},
      {:inch_ex, only: :docs},
      {:earmark, "0.1.19", only: :docs}
    ]
  end

  defp docs do
    {ref, 0} = System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"])

    [
        source_ref: ref,
        main: "overview"
    ]
  end
end


Bump version to v0.7.0

Bump version to v0.7.0

defmodule Hound.Mixfile do
  use Mix.Project

  def project do
    [ app: :hound,
      version: "0.7.0",
      elixir: ">= 1.0.2",
      description: description,
      deps: deps(Mix.env),
      package: package,
      docs: [readme: true, main: "README.md"]
    ]
  end

  def application do
    [
      applications: [:hackney, :httpoison],
      mod: { Hound, [] },
      description: 'Integration testing and browser automation library',
    ]
  end

  defp deps do
    [
      {:httpoison, "~> 0.5.0"},
      {:poison,    "~> 1.4.0"}
    ]
  end

  defp deps(:docs) do
    deps ++ [
      { :ex_doc,  github: "elixir-lang/ex_doc" },
      { :earmark, github: "pragdave/earmark" }
    ]
  end

  defp deps(_) do
    deps
  end

  defp package do
    [
      contributors: ["Akash Manohar J"],
      licenses: ["MIT"],
      links: %{ "GitHub" => "https://github.com/HashNuke/hound" }
    ]
  end

  defp description do
    """
    Elixir library for browser automation and writing integration tests in Elixir.

    ## Features

    * Can run __multiple browser sessions__ simultaneously. [See example](https://github.com/HashNuke/hound/blob/master/test/multiple_browser_session_test.exs).

    * Supports Selenium (Firefox, Chrome), ChromeDriver and PhantomJs.

    * Supports Javascript-heavy apps. Retries a few times before reporting error.

    * Implements the WebDriver Wire Protocol.

    **Internet Explorer may work under Selenium, but hasn't been tested.
    """
  end
end

defmodule Hound.Mixfile do
  use Mix.Project

  def project do
    [ app: :hound,
      version: "0.6.1",
      elixir: ">= 1.0.2",
      description: description,
      deps: deps(Mix.env),
      package: package,
      docs: [readme: true, main: "README.md"]
    ]
  end

  def application do
    [
      applications: [:hackney, :httpoison],
      mod: { Hound, [] },
      description: 'Browser automation library',
    ]
  end

  defp deps do
    [
      {:httpoison, "~> 0.5.0"},
      {:poison,    "~> 1.4.0"}
    ]
  end

  defp deps(:docs) do
    deps ++ [
      { :ex_doc,  github: "elixir-lang/ex_doc" },
      { :earmark, github: "pragdave/earmark" }
    ]
  end

  defp deps(_) do
    deps
  end

  defp package do
    [
      contributors: ["Akash Manohar J"],
      licenses: ["MIT"],
      links: %{ "GitHub" => "https://github.com/HashNuke/hound" }
    ]
  end

  defp description do
    """
    Elixir library for browser automation and writing integration tests in Elixir.

    ## Features

    * Can run __multiple browser sessions__ simultaneously. [See example](https://github.com/HashNuke/hound/blob/master/test/multiple_browser_session_test.exs).

    * Supports Selenium (Firefox, Chrome), ChromeDriver and PhantomJs.

    * Supports Javascript-heavy apps. Retries a few times before reporting error.

    * Implements the WebDriver Wire Protocol.

    **Internet Explorer may work under Selenium, but hasn't been tested.
    """
  end
end


Clean ClientSocket documentation.

Clean ClientSocket documentation.

defmodule Wobserver.Web.ClientSocket do
  @moduledoc ~S"""
  Low level WebSocket handler

  Connects to the Javascript websocket and parses all requests.

  Example:
    ```elixir
    defmodule Wobserver.Web.Client do
      use Wobserver.Web.ClientSocket

      alias Wobserver.System

      def client_init do
        {:ok, %{}}
      end

      def client_handle(:hello, state) do
        {:reply, :ehlo, state}
      end

      def client_info(:update, state) do
        {:noreply, state}
      end
    end
    ```
  """

  require Logger

  alias Wobserver.Util.Node.Discovery
  alias Wobserver.Util.Node.Remote
  alias Wobserver.Web.ClientSocket

  @typedoc "Response to browser."
  @type response ::
    {:reply, atom | list(atom), any, any}
    | {:reply, atom | list(atom), any}
    | {:noreply, any}

  @doc ~S"""
  Initalizes the WebSocket.

  Return {`:ok`, initial state} or {`:ok`, initial state, socket timeout}.
  """
  @callback client_init :: {:ok, any} | {:ok, any, non_neg_integer}
  @doc ~S"""
  Handles messages coming from the WS client.

  Return browser response.
  """
  @callback client_handle(atom | {atom, any}, any) :: ClientSocket.response
  @doc ~S"""
  Handles messages coming from other processes.

  Return browser response.
  """
  @callback client_info(any, any) :: ClientSocket.response

  defmacro __using__(_) do
    quote do
      import Wobserver.Web.ClientSocket, only: :functions

      @behaviour :cowboy_websocket_handler
      @behaviour Wobserver.Web.ClientSocket

      @timeout 60_000

      # Callbacks

      ## Init / Shutdown

      @doc ~S"""
      Initialize the websocket connection.

      The `req` cowboy request and `options` are passed, but unused in the setup.
      """
      @spec init(any, :cowboy_req.req, any) ::
        {:upgrade, :protocol, :cowboy_websocket}
      def init(_, _req, _options) do
        {:upgrade, :protocol, :cowboy_websocket}
      end

      @doc ~S"""
      Initialize the websocket connection by calling the implementing client.

      The `req` cowboy request is passed along to the client.

      The `type` and `options` are passed, but unused in the setup.
      """
      @spec websocket_init(any, req :: :cowboy_req.req, any) ::
        {:ok, :cowboy_req.req, any, non_neg_integer}
      def websocket_init(_type, req, _options) do
        case client_init() do
          {:ok, state, timeout} ->
            {:ok, req, %{state: state, proxy: nil}, timeout}
          {:ok, state} ->
            {:ok, req, %{state: state, proxy: nil}, @timeout}
        end
      end

      @doc ~S"""
      Gracefully handles websocked terminate.

      The `reason`, `req` and `state` are ignored.
      """
      @spec websocket_terminate({atom, any}, :cowboy_req.req, any) :: :ok
      def websocket_terminate(_reason, _req, _state) do
        :ok
      end

      ## Incoming from client / browser
      @doc ~S"""
      Handles incoming messages from the websocket client.

      The `message` is parsed and passed on to the client, which responds with an update `state` and possible reply.

      The `req` is ignored.
      """
      @spec websocket_handle(
        {:text, String.t},
        req :: :cowboy_req.req,
        state :: any
      ) :: {:reply, {:text, String.t}, :cowboy_req.req, any}
        | {:ok, :cowboy_req.req, any}
      def websocket_handle(message, req, state)

      def websocket_handle({:text, command}, req, state = %{proxy: nil}) do
        case parse_command(command) do
          {:setup_proxy, name} ->
            setup_proxy(name, state, req)
          :nodes ->
            {:reply, :nodes, Discovery.discover, state.state}
            |> send_response(state, req)
          parsed_command ->
            parsed_command
            |> client_handle(state.state)
            |> send_response(state, req)
        end
      end

      def websocket_handle({:text, command}, req, state) do
        case parse_command(command) do
          {:setup_proxy, name} ->
            setup_proxy(name, state, req)
          :nodes ->
            {:reply, :nodes, Discovery.discover, state.state}
            |> send_response(state, req)
          parsed_command ->
            send state.proxy, {:proxy, command}
            {:ok, req, state}
        end
      end

      ## Outgoing
      @doc ~S"""
      Handles incoming messages from processes.

      The `message` is passed on to the client, which responds with an update `state` and possible reply.

      The `req` is ignored.
      """
      @spec websocket_info(
        message :: any,
        req :: :cowboy_req.req,
        state :: any
      ) :: {:reply, {:text, String.t}, :cowboy_req.req, any}
        | {:ok, :cowboy_req.req, any}
      def websocket_info(message, req, state)

      def websocket_info({:proxy, data}, req, state) do
        {:reply, {:text, data}, req, state}
      end

      def websocket_info(:proxy_disconnect, req, state) do
        {:reply, :proxy_disconnect, state.state, req}
        |> send_response(%{state | proxy: nil}, req)
      end

      def websocket_info(message, req, state) do
        message
        |> client_info(state.state)
        |> send_response(state, req)
      end
    end
  end

  # Helpers

  ## Command
  @doc ~S"""
  Parses the JSON `payload` to an atom command and map data.
  """
  @spec parse_command(payload :: String.t) :: atom | {atom, any}
  def parse_command(payload) do
    command_data = Poison.decode!(payload)

    command =
      case String.split(command_data["command"], "/") do
        [one_command] -> one_command |> String.to_atom
        list_of_commands -> list_of_commands |> Enum.map(&String.to_atom/1)
      end

    case command_data["data"] do
      "" -> command
      nil -> command
      data -> {command, data}
    end
  end

  @doc ~S"""
  Send a JSON encoded to the websocket client.

  The given `message` is JSON encoded (exception: `:noreply`).
  The `socket_state` is used updated to reflect changes made by the client.
  The cowboy `req` is returned untouched.
  """
  @spec send_response(
    message ::  {:noreply, any}
              | {:reply, atom | list(atom), any}
              | {:reply, atom | list(atom), map | list | String.t | nil, any},
    socket_state :: map,
    req :: :cowboy_req.req
  ) ::
    {:reply, {:text, String.t}, :cowboy_req.req, map}
    | {:ok, :cowboy_req.req, map}
  def send_response(message, socket_state, req)

  def send_response({:noreply, state}, socket_state, req) do
    {:ok, req, %{socket_state | state: state}}
  end

  def send_response({:reply, type, message, state}, socket_state, req) do
    data = %{
      type: uniform_type(type),
      timestamp: :os.system_time(:seconds),
      data: message,
    }

    case Poison.encode(data) do
      {:ok, payload} ->
        {:reply, {:text, payload}, req, %{socket_state | state: state}}
      {:error, error} ->
        Logger.warn "Wobserver.Web.ClientSocket: Can't send message #{inspect message}, reason: #{inspect error}"

        {:ok, req, %{socket_state | state: state}}
    end
  end

  def send_response({:reply, type, state}, socket_state, req) do
    send_response({:reply, type, nil, state}, socket_state, req)
  end

  @doc """
  Sets up a websocket proxy to a given `proxy`.

  The `state` is modified to add in the new proxy, while `req` is returned untouched.
  """
  @spec setup_proxy(proxy :: String.t, state :: map, req :: :cowboy_req.req) ::
    {:reply, {:text, String.t}, :cowboy_req.req, map}
    | {:ok, :cowboy_req.req, map}
  def setup_proxy(proxy, state, req) do
    connected =
      proxy
      |> Discovery.find
      |> Remote.socket_proxy

    case connected do
      {:error, message} ->
        {:reply, :setup_proxy, %{error: message}, state.state}
        |> send_response(state, req)
      {pid, "local"} ->
        if state.proxy != nil, do: send state.proxy, :disconnect

        name = Discovery.local.name

        {
          :reply,
          :setup_proxy,
          %{success: "Connected to: #{name}", node: name},
          state.state
        }
        |> send_response(%{state | proxy: pid}, req)
      {pid, name} ->
        {
          :reply,
          :setup_proxy,
          %{success: "Connected to: #{name}", node: name},
          state.state
        }
        |> send_response(%{state | proxy: pid}, req)
    end
  end

  @spec uniform_type(type :: atom | list(atom)) :: String.t
  defp uniform_type(type)

  defp uniform_type(type) when is_atom(type), do: type |> Atom.to_string

  defp uniform_type(type) when is_list(type) do
    type
    |> Enum.map(&Atom.to_string/1)
    |> Enum.join("/")
  end
end

defmodule Wobserver.Web.ClientSocket do
  @moduledoc ~S"""
  Low level WebSocket handler

  Connects to the Javascript websocket and parses all requests.

  Example:
    ```elixir
    defmodule Wobserver.Web.Client do
      use Wobserver.Web.ClientSocket

      alias Wobserver.System

      def client_init do
        {:ok, %{}}
      end

      def client_handle(:hello, state) do
        {:reply, :ehlo, state}
      end

      def client_info(:update, state) do
        {:noreply, state}
      end
    end
    ```
  """

  require Logger

  alias Wobserver.Util.Node.Discovery
  alias Wobserver.Util.Node.Remote
  alias Wobserver.Web.ClientSocket

  @typedoc "Response to browser."
  @type response ::
    {:reply, atom | list(atom), any, any}
    | {:reply, atom | list(atom), any}
    | {:noreply, any}

  @doc ~S"""
  Initalizes the WebSocket.

  Return {`:ok`, initial state} or {`:ok`, initial state, socket timeout}.
  """
  @callback client_init :: {:ok, any} | {:ok, any, non_neg_integer}
  @doc ~S"""
  Handles messages coming from the WS client.

  Return browser response.
  """
  @callback client_handle(atom | {atom, any}, any) :: ClientSocket.response
  @doc ~S"""
  Handles messages coming from other processes.

  Return browser response.
  """
  @callback client_info(any, any) :: ClientSocket.response

  defmacro __using__(_) do
    quote do
      import Wobserver.Web.ClientSocket, only: :functions

      @behaviour :cowboy_websocket_handler
      @behaviour Wobserver.Web.ClientSocket

      @timeout 60_000

      # Callbacks

      ## Init / Shutdown

      @doc ~S"""
      Initialize the websocket connection.

      The `req` cowboy request and `options` are passed, but unused in the setup.
      """
      @spec init(any, :cowboy_req.req, any) ::
        {:upgrade, :protocol, :cowboy_websocket}
      def init(_, _req, _options) do
        {:upgrade, :protocol, :cowboy_websocket}
      end

      @doc ~S"""
      Initialize the websocket connection by calling the implementing client.

      The `req` cowboy request is passed along to the client.

      The `type` and `options` are passed, but unused in the setup.
      """
      @spec websocket_init(any, req :: :cowboy_req.req, any) ::
        {:ok, :cowboy_req.req, any, non_neg_integer}
      def websocket_init(_type, req, _options) do
        case client_init() do
          {:ok, state, timeout} ->
            {:ok, req, %{state: state, proxy: nil}, timeout}
          {:ok, state} ->
            {:ok, req, %{state: state, proxy: nil}, @timeout}
        end
      end

      @doc ~S"""
      Gracefully handles websocked terminate.

      The `reason`, req` and `options` are ignored.
      """
      @spec websocket_terminate({atom, any}, :cowboy_req.req, any) :: :ok
      def websocket_terminate(_reason, _req, _state) do
        :ok
      end

      ## Incoming from client / browser
      @spec websocket_handle(
        {:text, String.t},
        req :: :cowboy_req.req,
        state :: any
      ) :: {:reply, {:text, String.t}, :cowboy_req.req, any}
        | {:ok, :cowboy_req.req, any}
      def websocket_handle(message, req, state)

      def websocket_handle({:text, command}, req, state = %{proxy: nil}) do
        case parse_command(command) do
          {:setup_proxy, name} ->
            setup_proxy(name, state, req)
          :nodes ->
            {:reply, :nodes, Discovery.discover, state.state}
            |> send_response(state, req)
          parsed_command ->
            parsed_command
            |> client_handle(state.state)
            |> send_response(state, req)
        end
      end

      def websocket_handle({:text, command}, req, state) do
        case parse_command(command) do
          {:setup_proxy, name} ->
            setup_proxy(name, state, req)
          :nodes ->
            {:reply, :nodes, Discovery.discover, state.state}
            |> send_response(state, req)
          parsed_command ->
            send state.proxy, {:proxy, command}
            {:ok, req, state}
        end
      end

      ## Outgoing
      @spec websocket_info(
        message :: any,
        req :: :cowboy_req.req,
        state :: any
      ) :: {:reply, {:text, String.t}, :cowboy_req.req, any}
        | {:ok, :cowboy_req.req, any}
      def websocket_info(message, req, state)

      def websocket_info({:proxy, data}, req, state) do
        {:reply, {:text, data}, req, state}
      end

      def websocket_info(:proxy_disconnect, req, state) do
        {:reply, :proxy_disconnect, state.state, req}
        |> send_response(%{state | proxy: nil}, req)
      end

      def websocket_info(message, req, state) do
        message
        |> client_info(state.state)
        |> send_response(state, req)
      end
    end
  end

  # Helpers

  ## Command
  @spec parse_command(payload :: String.t) :: atom | {atom, any}
  def parse_command(payload) do
    command_data = Poison.decode!(payload)

    command =
      case String.split(command_data["command"], "/") do
        [one_command] -> one_command |> String.to_atom
        list_of_commands -> list_of_commands |> Enum.map(&String.to_atom/1)
      end

    case command_data["data"] do
      "" -> command
      nil -> command
      data -> {command, data}
    end
  end

  @spec send_response(
    message ::  {:noreply, any}
              | {:reply, atom | list(atom), any}
              | {:reply, atom | list(atom), map | list | String.t | nil, any},
    socket_state :: map,
    req :: :cowboy_req.req
  ) ::
    {:reply, {:text, String.t}, :cowboy_req.req, map}
    | {:ok, :cowboy_req.req, map}
  def send_response(message, socket_state, req)

  def send_response({:noreply, state}, socket_state, req) do
    {:ok, req, %{socket_state | state: state}}
  end

  def send_response({:reply, type, message, state}, socket_state, req) do
    data = %{
      type: uniform_type(type),
      timestamp: :os.system_time(:seconds),
      data: message,
    }

    case Poison.encode(data) do
      {:ok, payload} ->
        {:reply, {:text, payload}, req, %{socket_state | state: state}}
      {:error, error} ->
        Logger.warn "Wobserver.Web.ClientSocket: Can't send message #{inspect message}, reason: #{inspect error}"

        {:ok, req, %{socket_state | state: state}}
    end
  end

  def send_response({:reply, type, state}, socket_state, req) do
    send_response({:reply, type, nil, state}, socket_state, req)
  end

  @spec setup_proxy(proxy :: String.t, state :: map, req :: :cowboy_req.req) ::
    {:reply, {:text, String.t}, :cowboy_req.req, map}
    | {:ok, :cowboy_req.req, map}
  def setup_proxy(proxy, state, req) do
    connected =
      proxy
      |> Discovery.find
      |> Remote.socket_proxy

    case connected do
      {:error, message} ->
        {:reply, :setup_proxy, %{error: message}, state.state}
        |> send_response(state, req)
      {pid, "local"} ->
        if state.proxy != nil, do: send state.proxy, :disconnect

        name = Discovery.local.name

        {
          :reply,
          :setup_proxy,
          %{success: "Connected to: #{name}", node: name},
          state.state
        }
        |> send_response(%{state | proxy: pid}, req)
      {pid, name} ->
        {
          :reply,
          :setup_proxy,
          %{success: "Connected to: #{name}", node: name},
          state.state
        }
        |> send_response(%{state | proxy: pid}, req)
    end
  end

  @spec uniform_type(type :: atom | list(atom)) :: String.t
  defp uniform_type(type)

  defp uniform_type(type) when is_atom(type), do: type |> Atom.to_string

  defp uniform_type(type) when is_list(type) do
    type
    |> Enum.map(&Atom.to_string/1)
    |> Enum.join("/")
  end
end