2018/04/23

Elixir5 ProjectTool

mix

mix 是管理 elixir project 的工具

$ mix help
mix                   # Runs the default task (current: "mix run")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application files
mix cmd               # Executes the given command
mix compile           # Compiles source files
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies
mix do                # Executes the tasks separated by comma
mix escript           # Lists installed escripts
mix escript.build     # Builds an escript for the project
mix escript.install   # Installs an escript locally
mix escript.uninstall # Uninstalls escripts
mix help              # Prints help information for tasks
mix loadconfig        # Loads and persists the given configuration
mix local             # Lists local tasks
mix local.hex         # Installs Hex locally
mix local.public_keys # Manages public keys
mix local.rebar       # Installs Rebar locally
mix new               # Creates a new Elixir project
mix profile.cprof     # Profiles the given file or expression with cprof
mix profile.fprof     # Profiles the given file or expression with fprof
mix run               # Runs the given file or expression
mix test              # Runs a project's tests
mix xref              # Performs cross reference checks
iex -S mix            # Starts IEx and runs the default task

建立新 project

$ mix new test
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/test.ex
* creating test
* creating test/test_helper.exs
* creating test/test_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd test
    mix test

Run "mix help" for more commands.

如果要放到 git

$ git init
$ git add .
$ git commit -m "Initial commit of new project"

project 結構:

config/
  內含 config.exs, 設定資料
lib/
  內含 test.ex,這是 top-level module
mix.exs
  project 設定
README.md
  project 說明文件
test/
  內含 test_helper.exs, test_test.exs 測試程式

如何處理 command line

lib/issues/cli.ex

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """


  def run(argv) do
    parse_args(argv)
  end

  @doc """
  `argv` can be -h or --help, which returns :help.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format.

  Return a tuple of `{ user, project, count }`, or `:help` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help

    end
  end
end

簡單的測試 test/issues_test.exs

defmodule IssuesTest do
  use ExUnit.Case

  test "the truth" do
    assert(true)
  end
end

test/cli_test.exs

defmodule CliTest do
  use ExUnit.Case

  test "nil returned by option parsing with -h and --help options" do
    assert Issues.CLI.parse_args(["-h",     "anything"]) == :help
    assert Issues.CLI.parse_args(["--help", "anything"]) == :help
  end

  test "three values returned if three given" do
    assert Issues.CLI.parse_args(["user", "project", "99"]) == { "user", "project", 99 }
  end

  test "count is defaulted if two values given" do
    assert Issues.CLI.parse_args(["user", "project"]) == { "user", "project", 4 }
  end
end

mix.exs

defmodule Issues.Mixfile do
  use Mix.Project

  def project do
    [ app:     :issues,
      version: "0.0.1",
      deps:    deps 
    ]
  end

  # Configuration for the OTP application
  def application do
    []
  end

  # Returns the list of dependencies in the format:
  # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
  defp deps do
    []
  end
end

列印 project deps 相關 libs

$ mix deps

取得 deps

$ mix deps.get

執行測試

$ mix test
warning: variable "deps" does not exist and is being expanded to "deps()", please use parentheses to remove the ambiguity or change the variable name
  mix.exs:7

Compiling 2 files (.ex)
Generated issues app
warning: this check/guard will always yield the same result
  test/issues_test.exs:5

....

Finished in 0.05 seconds
4 tests, 0 failures

Randomized with seed 80914

在 mix.exs 裡面定義使用 library

mix.exs 增加 defp deps 的部分

defmodule Issues.Mixfile do
  use Mix.Project

  def project do
    [ app:     :issues,
      version: "0.0.1",
      deps:    deps 
    ]
  end

  # Configuration for the OTP application
  def application do
    [ applications: [:httpotion ] ]
  end

  # Returns the list of dependencies in the format:
  # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
  defp deps do
    [
      { :httpotion,  github: "myfreeweb/httpotion" }
    ]
  end
end

修改 cli.exs

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """

  def run(argv) do
    argv
      |> parse_args
      |> process
  end

  @doc """
  `argv` can be -h or --help, which returns   `:help`.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format

  Return a tuple of `{ user, project, count }`, or `nil` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help
    end
  end

  def process(:help) do
    IO.puts """
    usage:  issues <user> <project> [ count | #{@default_count} ]
    """
    System.halt(0)
  end

  def process({user, project, count}) do
    Issues.GithubIssues.fetch(user, project)
  end
end

增加 lib/issues/github_issues.ex

defmodule Issues.GithubIssues do
  @user_agent  [ {"User-agent", "Elixir dave@pragprog.com"} ]

  def fetch(user, project) do
    issues_url(user, project)
    |> HTTPoison.get(@user_agent)
    |> handle_response
  end

  def issues_url(user, project) do
    "https://api.github.com/repos/#{user}/#{project}/issues"
  end

  def handle_response({ :ok, %{status_code: 200, body: body}}) do
    { :ok,    body }
  end

  def handle_response({ _,   %{status_code: _,   body: body}}) do
    { :error, body }
  end
end

增加 jsonex library

  defp deps do
    [
      {:httpotion, github: "myfreeweb/httpotion"          },
      {:jsonex,    "2.0",   github: "marcelog/jsonex", tag: "2.0"  }
    ]
  end

修改 github_issues.ex

defmodule Issues.GithubIssues do

  @user_agent  [ {"User-agent", "Elixir dave@pragprog.com"} ]

  def fetch(user, project) do
    issues_url(user, project)
    |> HTTPoison.get(@user_agent)
    |> handle_response
  end

  def handle_response({:ok, %{status_code: 200, body: body}}) do
    { :ok, Poison.Parser.parse!(body) }
  end

  def handle_response({_, %{status_code: _, body: body}}) do
    { :error, Poison.Parser.parse!(body) }
  end

  # use a module attribute to fetch the value at compile time
  @github_url Application.get_env(:issues, :github_url)

  def issues_url(user, project) do
    "#{@github_url}/repos/#{user}/#{project}/issues"
  end
end

cli_test.exs

defmodule CliTest do
  use ExUnit.Case

  import Issues.CLI, only: [ parse_args: 1,
                             sort_into_ascending_order: 1 ]

  test ":help returned by option parsing with -h and --help options" do
    assert parse_args(["-h",     "anything"]) == :help
    assert parse_args(["--help", "anything"]) == :help
  end

  test "three values returned if three given" do
    assert parse_args(["user", "project", "99"]) == { "user", "project", 99 }
  end

  test "count is defaulted if two values given" do
    assert parse_args(["user", "project"]) == { "user", "project", 4 }
  end

  test "sort ascending orders the correct way" do
    result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"]))
    issues = for issue <- result, do: Map.get(issue, "created_at")
    assert issues == ~w{a b c}
  end

  defp fake_created_at_list(values) do
    for value <- values,
    do: %{"created_at" => value, "other_data" => "xxx"}
  end
end

cli.ex

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """

  def run(argv) do
    argv
      |> parse_args
      |> process
  end

  @doc """
  `argv` can be -h or --help, which returns   `:help`.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format

  Return a tuple of `{ user, project, count }`, or `nil` if help was given.
  """

  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help
    end
  end

  def process(:help) do
    IO.puts """
    usage:  issues <user> <project> [ count | #{@default_count} ]
    """
    System.halt(0)
  end

  def process({user, project, count}) do
    Issues.GithubIssues.fetch(user, project)
      |> decode_response
      |> convert_to_list_of_hashdicts
      |> sort_into_ascending_order
  end

  def decode_response({:ok, body}), do: Jsonex.decode(body)
  def decode_response({:error, msg}) do
    error = Jsonex.decode(msg)["message"]
    IO.puts "Error fetching from Github: #{error}"
    System.halt(2)
  end

  def convert_to_list_of_hashdicts(list) do
    list |> Enum.map(&HashDict.new/1)
  end

  def sort_into_ascending_order(list_of_issues) do
    Enum.sort list_of_issues,
              fn i1, i2 -> i1["created_at"] <= i2["created_at"] end
  end

end

編譯時會出現 jsonex 錯誤

could not compile dependency :jsonex, "mix compile" failed. You can recompile this dependency with "mix deps.compile jsonex", update it with "mix deps.update jsonex" or clean it with "mix deps.clean jsonex"
==> issues
** (Mix) Expected :version to be a SemVer version, got: "2.0"

ref: Heroku compile issue (Elixir Buildpaack) ** (Mix) Expected :version to be a SemVer version

必須修改 Jsonex 的 mix.exs

defmodule Jsonex.Mixfile do
  use Mix.Project

  def project do
    [ app: :jsonex,
      version: "2.0.0",
      deps: deps ]
  end

同時要修改 issues 的 mix.exs

  defp deps do
    [
      {:httpotion,         github: "myfreeweb/httpotion"          },
      {:jsonex,    "2.0.0",  github: "marcelog/jsonex", tag: "2.0"  }
    ]
  end


ref: Building an Elixir CLI application

產生新的 project

$ mix new elixir_calc
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/elixir_calc.ex
* creating test
* creating test/test_helper.exs
* creating test/elixir_calc_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd elixir_calc
    mix test

Run "mix help" for more commands.

修改 mix.exs

:ex_doc 及 :earmark 是用來產生 docs 的 library

defmodule ElixirCalc.Mixfile do
  use Mix.Project

  def project do
    [
      app: :elixir_calc,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      build_embedded: Mix.env == :prod,
      escript: [main_module: ElixirCalc],
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},

      {:ex_doc, "~> 0.12"},
      {:earmark, "~> 1.0", override: true}
    ]
  end
end

elixir_cals.ex: 處理 command line args

加上了 Logger.info,前面必須要先 require Logger @moduledoc 是 docs

defmodule ElixirCalc do
  require Logger

  defmodule Parser do
    def parse(args) do
      {options, args, _} = OptionParser.parse(args)
      {options, args}
    end

    def parse_args({[fib: x], _}) do
      Logger.info "arg fib= #{x} "
      IO.puts x |> String.to_integer |> ElixirCalc.Calculator.fib
    end
  end

  @moduledoc """
  main function USAGE: ./elixir_calc --fib num
  """

  def main(args) do
    args
    |> Parser.parse
    |> Parser.parse_args
  end
end

config/config.exs: 要加上 :logger 的設定值

use Mix.Config
config :logger, compile_time_purge_level: :info

lib/calculator/calculator.ex: fib 的主程式

defmodule ElixirCalc.Calculator do
  def fib(0) do 0 end
  def fib(1) do 1 end
  def fib(n) do
    fib(n - 1) + fib(n - 2)
  end
end

test/elixircalctest.exs

defmodule ElixirCalcTest do
  use ExUnit.Case
  doctest ElixirCalc

  test "fibonacci of 1 is 1" do
    assert ElixirCalc.Calculator.fib(1) == 1
  end

  test "fibonacci of 2 is 1" do
    assert ElixirCalc.Calculator.fib(2) == 1
  end

  test "fibonacci of 10 is 55" do
    assert ElixirCalc.Calculator.fib(10) == 55
  end
end
## 取得 deps libraries
$ mix deps.get
Running dependency resolution...
Dependency resolution completed:
  earmark 1.2.3
  ex_doc 0.16.3
* Getting ex_doc (Hex package)
  Checking package (https://repo.hex.pm/tarballs/ex_doc-0.16.3.tar)
  Using locally cached package
* Getting earmark (Hex package)
  Checking package (https://repo.hex.pm/tarballs/earmark-1.2.3.tar)
  Using locally cached package

## 編譯 deps
$ mix deps.compile
==> earmark
Compiling 3 files (.erl)
Compiling 24 files (.ex)
Generated earmark app
==> ex_doc
Compiling 15 files (.ex)
Generated ex_doc app

## 單元測試
$ mix test
==> earmark
Compiling 3 files (.erl)
Compiling 24 files (.ex)
Generated earmark app
==> ex_doc
Compiling 15 files (.ex)
Generated ex_doc app
==> elixir_calc
Compiling 2 files (.ex)
Generated elixir_calc app
...

Finished in 0.04 seconds
3 tests, 0 failures

Randomized with seed 704861

直接在 shell 測試

$ iex -S mix

iex(1)> ElixirCalc.main(["--fib", "10"])

22:51:00.013 [info]  arg fib= 10
55
:ok

產生獨立的執行檔及文件

## 產生獨立執行的 binary 執行檔
$ mix escript.build
Compiling 2 files (.ex)
Generated elixir_calc app
Generated escript elixir_calc with MIX_ENV=dev

## 執行 elixir_calc
$ ./elixir_calc --fib 10
55

## 產生文件
$ mix docs
Docs successfully generated.
View them at "doc/index.html".

mix 的一些指令

  • mix xref unreachable

    列出沒有被呼叫的 functions

  • mix xref warnings

    列出跟 dependencies 有關的 warnings(ex: 呼叫unknown functions)

  • mix xref callers Mod | Mod.func | Mod.func/arity

    列出呼叫 module/function 的 callers

    $ mix xref callers Logger
    lib/elixir_calc.ex:11: Logger.bare_log/3
    lib/elixir_calc.ex:11: Logger.info/1
  • mix xref graph

    列印 application dependency tree

    $ mix xref graph
    lib/calculator/calulator.ex
    lib/elixir_calc.ex
    └── lib/calculator/calulator.ex
    mix xref graph --format dot
    dot -Grankdir=LR -Epenwidth=2 -Ecolor=#a0a0a0 -Tpng xref_graph.dot -o xref_graph.png

server monitor

iex> :observer.start()

References

Programming Elixir

沒有留言:

張貼留言