class: center, middle ![Indy Elixir](https://www.indyelixir.org/images/indyelixir.svg) # Building and Sharing Your First Elixir Library ### Steve Grossi @ Lessonly --- # What We’ll Learn - Mix - Hex - Elixir library structure - Example TDD flow - Writing and generating documentation - Doctests - Publishing our library and docs --- # What is Hex? - Package manager for the Erlang ecosystem - Online repository: [hex.pm](https://hex.pm) --- # What is Mix? - Build tool (compile, test, run code) - Dependency manager - Environment manager We use Mix to create Elixir libraries and Hex to package and share them --- # Creating a new Elixir project ```shell $ mix new fibonacci * creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs * creating lib * creating lib/fibonacci.ex * creating test * creating test/test_helper.exs * creating test/fibonacci_test.exs Your Mix project was created successfully. You can use "mix" to compile it, test it, and more: cd fibonacci mix test Run "mix help" for more commands. ``` --- # Elixir Library File Structure ```shell $ tree fibonacci ├── README.md ├── config │ └── config.exs ├── lib │ └── fibonacci.ex ├── mix.exs └── test ├── fibonacci_test.exs └── test_helper.exs ``` --- # Project Metadata ```elixir # mix.exs defmodule Fibonacci.Mixfile do use Mix.Project def project do [app: :fibonacci, version: "0.1.0", elixir: "~> 1.3", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps()] end ``` --- # OTP Applications ```elixir # mix.exs defmodule Fibonacci.Mixfile do # ... # Configuration for the OTP application # # Type "mix help compile.app" for more information def application do [applications: [:logger]] end ``` --- # Dependencies ```elixir defmodule Fibonacci.Mixfile do # ... # Dependencies can be Hex packages: # # {:mydep, "~> 0.3.0"} # # Or git/path repositories: # # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} # # Type "mix help deps" for more examples and options defp deps do [] end ``` --- # Test-Driven Development ```shell $ mix test . Finished in 0.02 seconds 1 test, 0 failures Randomized with seed 241771 ``` --- # Oh, hello there! ```elixir defmodule FibonacciTest do use ExUnit.Case doctest Fibonacci test "the truth" do assert 1 + 1 == 2 end end ``` --- # Oh, hello there! ```elixir defmodule FibonacciTest do use ExUnit.Case doctest Fibonacci test "the truth" do assert 1 + 1 == :nope end end ``` --- # Nope! ```shell $ mix test 1) test the truth (FibonacciTest) test/fibonacci_test.exs:5 Assertion with == failed code: 1 + 1 == :nope lhs: 2 rhs: :nope stacktrace: test/fibonacci_test.exs:6: (test) Finished in 0.03 seconds 1 test, 1 failure ``` --- # Test-Driven Development ```elixir defmodule FibonacciTest do use ExUnit.Case doctest Fibonacci test "returns list of specified length" do assert Fibonacci.series(0) == [] assert Fibonacci.series(1) == [0] assert Fibonacci.series(8) == [0, 1, 1, 2, 3, 5, 8, 13] end end ``` --- # Test-Driven Development ```shell $ mix test 1) test returns list of specified length (FibonacciTest) test/fibonacci_test.exs:5 ** (UndefinedFunctionError) function Fibonacci.series/1 is undefined or private stacktrace: (fibonacci) Fibonacci.series(9) test/fibonacci_test.exs:6: (test) Finished in 0.03 seconds 1 test, 1 failure ``` --- # Test-Driven Development ```elixir defmodule Fibonacci do def series(_length) do [] end end ``` --- # Test-Driven Development ```elixir $ mix test Compiling 1 file (.ex) 1) test returns list of specified length (FibonacciTest) test/fibonacci_test.exs:5 Assertion with == failed code: Fibonacci.series(8) == [0, 1, 1, 2, 3, 5, 8, 13] lhs: [] rhs: [0, 1, 1, 2, 3, 5, 8, 13] stacktrace: test/fibonacci_test.exs:6: (test) Finished in 0.03 seconds 1 test, 1 failure ``` --- ```elixir defmodule Fibonacci do def series(0), do: [] def series(1), do: [0] def series(length) do [number_at_position(length) | Enum.reverse(series(length - 1))] |> Enum.reverse end defp number_at_position(1), do: 0 defp number_at_position(2), do: 1 defp number_at_position(number) do number_at_position(number - 2) + number_at_position(number - 1) end end ``` --- # Green! ```shell $ mix test . Finished in 0.04 seconds 1 test, 0 failures ``` --- # What about negative lengths? ```elixir defmodule FibonacciTest do # ... test "requires non-negative length" do assert Fibonacci.series(-1) == {:error, "Length must not be negative"} end end ``` --- # What about negative lengths? ```shell $ mix test ``` Hangs indefinitely: we have an infinite loop! --- # What about negative lengths? ```elixir defmodule Fibonacci do def series(0), do: [] def series(1), do: [0] def series(length) when length > 1 do [number_at_position(length) | Enum.reverse(series(length - 1))] |> Enum.reverse end ``` --- # What about negative lengths? ```shell $ mix test Compiling 1 file (.ex) . 1) test requires non-negative length (FibonacciTest) test/fibonacci_test.exs:11 ** (FunctionClauseError) no function clause matching in Fibonacci.series/1 stacktrace: (fibonacci) lib/fibonacci.ex:3: Fibonacci.series(-1) test/fibonacci_test.exs:12: (test) Finished in 0.04 seconds 2 tests, 1 failure ``` --- # What about negative lengths? ```elixir defmodule Fibonacci do def series(0), do: [] def series(1), do: [0] def series(length) when length > 1 do [number_at_position(length) | Enum.reverse(series(length - 1))] |> Enum.reverse end def series(_), do: {:error, "Length must not be negative"} ``` --- # Success! ```shell $ mix test Compiling 1 file (.ex) .. Finished in 0.03 seconds 2 tests, 0 failures ``` --- # Let’s Try It Out Ourselves ```elixir $ iex -S mix Erlang/OTP 19 [erts-8.0] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Compiling 1 file (.ex) Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> Fibonacci.series(5) [0, 1, 1, 2, 3] iex(2)> Fibonacci.series(10) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] iex(3)> Fibonacci.series(100) # waiting...fans start spinning... ``` Clearly this can be optimized. Later. --- # Documentation ```elixir defmodule Fibonacci do @moduledoc """ Contains functions for working with the Fibonacci series. """ @doc """ Returns a list of numbers in the Fibonacci series of the specified length. """ def series(0), do: [] ``` --- # Doctests ```elixir @doc """ Returns a list of numbers in the Fibonacci series of the specified length. ## Example iex> Fibonacci.series(5) [0, 1, 1, 2, 3] """ def series(0), do: [] ``` --- # Doctests ```shell mix test Compiling 1 file (.ex) ... Finished in 0.08 seconds 3 tests, 0 failures ``` Thanks to: ```elixir defmodule FibonacciTest do doctest Fibonacci ``` --- # Generate Docs ```elixir # mix.exs defmodule Fibonacci.Mixfile do # ... defp deps do [ {:ex_doc, "~> 0.13", only: :dev}, {:earmark, "~> 1.0.1", only: :dev} ] end ``` `ex_doc` for docs framework, `earmark` for Markdown. --- # Generate Docs ```shell $ mix deps.get $ mix docs $ open docs/index.html ``` --- # Let’s Publish to Hex ```shell mix hex.user whoami ** (Mix) No user authorised on the local machine. Run `mix hex.user auth` or create a new user with `mix hex.user register` ``` If registering, you’ll have to confirm your email. --- # Let’s Publish to Hex Two more things to add to `mix.exs`: ```elixir defmodule Fibonacci.Mixfile do use Mix.Project def project do [ # ... description: description(), package: package(), ``` --- # Let’s Publish to Hex ```elixir defp description do """ An Elixir interface to the Fibonacci series. """ end ``` --- # Let’s Publish to Hex ```elixir defp package do [ files: ["lib", "mix.exs", "README.md"], maintainers: ["Steve Grossi"], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/stevegrossi/fibonacci", "Docs" => "https://hexdocs.pm/fibonacci/"} ] end ``` --- # Let’s Publish to Hex ```shell $ mix hex.publish Publishing fibonacci 0.1.0 Files: lib/fibonacci.ex mix.exs README.md App: fibonacci Name: fibonacci Description: An Elixir interface to the Fibonacci series. Version: 0.1.0 Build tools: mix Licenses: MIT Maintainers: Steve Grossi Links: Docs: http://hexdocs.pm/fibonacci/ GitHub: https://github.com/stevegrossi/fibonacci Elixir: ~> 1.3 ``` --- # Publishing Documentation Docs were automatically published to [hexdocs.pm/fibonacci](https://hexdocs.pm/fibonacci) Hexdocs is free and hosts all versions publicly. To update just the docs: ```shell $ mix hex.publish docs ``` --- # New Version? ```elixir # mix.exs def project do [ app: :fibonacci, version: "0.2.0", # Updated ``` then ```shell $ mix hex.publish ``` Probably also want to ```shell $ git commit -m "Sweet new v0.2 stuff" $ git tag v0.2.0 $ git push && git push --tags ``` --- # The End - [github.com/stevegrossi/fibonacci](https://github.com/stevegrossi/fibonacci) - [hex.pm/packages/fibonacci](https://hex.pm/packages/fibonacci) - [hexdocs.pm/fibonacci](https://hexdocs.pm/fibonacci)