GithubHelp home page GithubHelp logo

elixir-path's People

Contributors

cristhianrodriguezmolina avatar

Watchers

 avatar

elixir-path's Issues

Ecto Elixir path (Chapter 1 to 3)

Ecto chapter 1 - Introduction

There are diferent adapter for ecto in Elixir, keeping this in mind for this project the postgresql adapter was used, defining it in the dependencies of a new project:

{:ecto_sql, "~> 3.2"}, # Ecto
{:postgrex, "~> 0.15"} # Adapter

Database configuration for the Friends application:

config :friends, Friends.Repo,
  database: "friends_repo",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"

Migration to create the people table:

defmodule Friends.Repo.Migrations.CreatePeople do
  use Ecto.Migration

  def change do
    create table(:people) do
      add :name, :string, null: false
      add :age, :integer, default: 0
    end
  end
end

And finally the schema to access to that table:

defmodule Friends.Person do
  use Ecto.Schema

  schema "people" do
    field :name, :string
    field :age, :integer, default: 0
  end
end

Changesets

We can do changes over a struct schema with the Changeset functions, cast/3 and change/2, like the example below, where we change the name of a Person struct:

iex> Ecto.Changeset.cast(%App.Person{name: "Bob"}, %{"name" => "Jack"}, [:name, :age])
%Ecto.Changeset<
  action: nil,
  changes: %{name: "Jack"},
  errors: [],
  data: %Friends.Person<>,
  valid?: true
>

Validations

There are some validations for changesets, in order to guarantee that the information given to the changeset is valid:

def test_changeset(struct, params) do
  struct
  # Fields allowed to go through and be modified
  |> cast(params, [:name, :age])
  # ensures age is included in the range 0 to 99 (enumerable)
  |> validate_inclusion(:age, 0..99)
  |> validate_exclusion(:name, ~w(max ben))
  # ensures that the name is always required
  |> validate_required([:name])
  # ensures that the name length is always above 2
  |> validate_length(:name, min: 4)
  |> validate_fictional_name()
end

Ecto Elixir path (Chapter 4 to 5)

Associations

Created and tested the associations Belongs to/Has many, Belongs to/Has one and Many to Many.

Then create a new Movie to test the insertion, association, etc:

movie = %Movie{title: "Jurassic Park", tagline: "Just dinosaurs"}

And then insert it to the database with the Repo module:

movie = Repo.insert!(movie)

Associating a new character to the movie created above and inserting it to the database:

character = Ecto.build_assoc(movie, :characters, %{name: "Alan Grant"})
Repo.insert!(character)

Querying

With the data already in the database we can now query that data with the following functions:

get and get_by

Using Repo.get/3 for IDs:

movie = Repo.get(Movie, 1)

Using Repo.get_by/3 for given criterias:

movie = Repo.get(Movie, title: "Jurassic Park")

from

We can use Ecto.Query.from/2 macro to create a query and then execute it with Repo.all/2 like so:

query = from(Movie, where: [title: "Jurassic Park"], select: [:title, :tagline])
Repo.all(query)

The code above will give us the title and tagline of the movies call explicitly Jurassic Park.

query = from m in Movie,
              join: c in Character,
              on: m.id == c.movie_id,
              where: c.name == "Alant Grant",
              select: {m.title, c.name}
Repo.all(query)

The code above will give us just the title of a movie and the name of a character where the movie id is equals in both, and additional where the name of the character is Alant Grant.

Metaprogramming (Chapter 1 to 3)

Quote and unquote

We can see what is the building block of a code in elixir with the quote macro like so:

iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}

And even get something more complex, like nested representations:

iex> quote do: String.upcase("foo")
{{:., [], [{:__aliases__, [alias: false], [:String]}, :upcase]}, [], ["foo"]}

All this quotes are represented in the format: {atom | tuple, list, list | atom}.

To get the representation with the value of number and not with the variable itself ("11 + number").

iex> number = 13
13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"

Macros

Defined some macros with defmacro and tested it:

defmodule Unless do
  # Unless representation with macros
  defmacro unless_macro(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

And tested it like so:

IO.inspect(Unless.unless_macro(true, do: IO.puts("This is not showing up")))

Also tested the interference with macros, using two macros in a function :

defmodule Hygiene do
  defmacro no_interference do
    quote do: a = 1
  end

  # This produces an interference due to the var function which changes the value of a in the context where the macro is used
  defmacro interference do
    quote do: var!(a) = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.no_interference()
    IO.puts("This has no interference #{a}")

    # This has a warning cause this value is never used
    a = 13
    Hygiene.interference()
    IO.puts("This has interferenece #{a}")
  end
end

Domain Specific Lenguages (DSL)

Created a test module and tested it with some test cases:

defmodule UsingTestCase do
  use TestCase

  test "New test" do
    5 = 4 + 1
  end

  test "New test with map" do
    %{a: 1, b: 3} = Map.put(%{a: 1}, :b, 3)
  end

  # This will throw an error
  test "New test with list" do
    [1, 2, 3] = [1, 2] ++ [4]
  end
end

Advanced chapter 10 to 11

Distributed tasks and tags

Distributed tasks

Starting the use of distributed nodes with distributed iex sessions, each one with a diferent name reference, like so:

iex --sname bar

Using async/await with Task:

task = Task.async(fn -> compute_something_expensive() end)
res  = compute_something_else()
res + Task.await(task)

Keeping this in mind, a new process to the supervision tree of the kv application was added, a task supervisor to perform processess in diferent nodes:

{Task.Supervisor, name: KV.RouterTasks},

And a new file for routing was created in /kv/lib/router.ex, routing to different nodes based on the bucket names like so:

[{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}]

Finally changing the lookup and run with the :create action methods in /kv_server/lib/command.ex to include the router in the server behaviour:

def run({:create, bucket}, pid) do
    case KV.Router.route(bucket, KV.Registry, :create, [KV.Registry, bucket]) do
      pid when is_pid(pid) -> {:ok, "OK\r\n"}
      _ -> {:error, "FAILED TO CREATE BUCKET"}
    end
  end

defp lookup(bucket, callback, server) do
    case KV.Router.route(bucket, KV.Registry, :lookup, [KV.Registry, bucket]) do
      {:ok, pid} -> callback.(pid)
      :error -> {:error, :not_found}
    end
  end

tags

Finally adding a new tag @tag :distributed to the router test, like so:

@tag :distributed
  test "route requests across nodes" do
    assert KV.Router.route("hello", Kernel, :node, []) ==
             :"foo@cristh-MS-7B85"

    assert KV.Router.route("world", Kernel, :node, []) ==
             :"bar@cristh-MS-7B85"
  end

And adding the exclusion to the test_helper.exs file:

exclude = if Node.alive?(), do: [], else: [distributed: true]

ExUnit.start(exclude: exclude)

This to avoid an error due to the need of two nodes foo and bar for the test.

Configuration and releases

Finishing advanced path in Elixir with configuration and package for production.

Application enviroment

Creating an enviroment variable adding it to the application function
in the mix.exs file in a project like so:

def application do
  [
    extra_applications: [:logger],
    env: [routing_table: []],
    mod: {KV, []}
  ]
end

After this i can now access to that env variable:

Application.fetch_env!(:kv, :routing_table)

Now inside the config folder of the umbrella project in a file runtime.exs i can configure the env variable with:

import Config
# Configuring the enviroment variable `:routing_table`
config :kv, :routing_table, [{?a..?z, node()}]

Finally to pass successfully the test presented in tags with two nodes, i had to add a set_up function to set up a test env like so:

# This setup runs after all the test in the file
  setup_all do
    # Getting the current state of the env variable `:routing_table`
    current = Application.get_env(:kv, :routing_table)

    Application.put_env(:kv, :routing_table, [
      {?a..?m, :"foo@cristh-MS-7B85"},
      {?n..?z, :"bar@cristh-MS-7B85"}
    ])

    # Setting back the real value of `:routing_table` on exit
    on_exit(fn -> Application.put_env(:kv, :routing_table, current) end)
  end

We can run the test above with iex --sname bar -S mix in one terminal and PORT=4041 elixir --sname foo -S mix test --only distributed (the PORT=4041 option is because the other session runs on PORT=4040) in another one.

releases

To create a release we have to add the releases we want to the project/0 function in the mix.exs of the umbrella.

The two releases for this project was foo containing the kv_server and kv, and bar just containing the kv (what is just the necessary) to avoid problems with the ports.

And then we can run the next commands to create the releases respectively:

MIX_ENV=prod mix release foo

and

MIX_ENV=prod mix release bar

And after this just runs both of the releases that are in /_build/prod/rel folder of the umbrella with the next commands, respectively:

_build/prod/rel/foo/bin/foo

and

_build/prod/rel/bar/bin/bar

Specifics (Chapter 1 to 4)

Plug

Basic configuration of the plug to verify a request in the router

defmodule Example.Plug.VerifyRequest do
  defmodule IncompleteRequestError do
    @moduledoc """
    Error raised when a required field is missing.
    """

    defexception message: ""
  end

  def init(options), do: options

  def call(%Plug.Conn{request_path: path} = conn, opts) do
    if path in opts[:paths], do: verify_request!(conn.params, opts[:fields])
    conn
  end

Child configuration of the router for plug_cowboy dependency (The cowboy_port() gets the port defined in the environment configuration or return 8080 if there is no definition):

{Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: cowboy_port()]}

Basic router definition with a main route and a 404 error route:

defmodule Example.Router do
  use Plug.Router

  # Importing the plug to the router
  alias Example.Plug.VerifyRequest

  plug(Plug.Parsers, parsers: [:urlencoded, :multipart])

  # Plug configuration for the router
  plug(VerifyRequest, fields: ["content", "mimetype"], paths: ["/upload"])

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/upload" do
    send_resp(conn, 201, "Uploaded")
  end

  match _ do
    send_resp(conn, 404, "Oops!")
  end
end

Basic testing for the route /upload which has the verification with the plug:

defmodule Example.RouterTest do
  use ExUnit.Case
  use Plug.Test

  alias Example.Router

  @content "<html><body>Hi!</body></html>"
  @mimetype "text/html"

  @opts Router.init([])

  test "returns uploaded" do
    conn =
      :get
      |> conn("/upload?content=#{@content}&mimetype=#{@mimetype}")
      |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 201
  end

EEx

Basic file greeting.eex:

<%= if (String.length name) > 3 do %>
    Hi, small <%= name %>
<% else %>
    Hi, big <%= name %>
<% end %>

Using function_from_file function of EEx in a module:

defmodule Example do
  require EEx

  # Compiling a eex file
  EEx.function_from_file(:def, :greeting, "lib/greeting.eex", [:name])
end

Using it:

Example.greeting "Sean"

Mnesia

Starting a schema with mnesia and starting the mnesia DBMS:

:mnesia.create_schema([node()])
:mnesia.start()

Creating a table Person:

:mnesia.create_table(Person, [attributes: [:id, :name, :job]])

Adding some data to the Person table with the mnesia.transaction and mnesia.write:

data_to_write = fn ->
  :mnesia.write({Person, 1, "Cristhian Rodriguez", "developer"})
  :mnesia.write({Person, 2, "John Doe", "unknown"})
end
:mnesia.transaction(data_to_write)

And reading that information:

data_to_read = fn ->
  :mnesia.read({Person, 1})
  :mnesia.read({Person, 2})
end
:mnesia.transaction(data_to_read)

Debugging

The following is a testing file with a module that has a require of our debugging module IEx, and a call to IEx.pry which works as a breakpoint in the code:

defmodule TestMod do
  require IEx

  def sum([a, b]) do
    b = 0
    # Works like a breakpoint
    IEx.pry()

    a + b
  end
end

To test this module, the iex -r test-exs should have be called.

Debugger

To use the debugger first of all we have to start an iex of our app:

iex -S mix

Basic operations for debugger:

  • Start the debugger
iex> :debugger.start()
  • Attach the modules to the debugger
iex> :int.ni(Example)
  • Create breakpoints in the specific lines
iex> :int.break(Example, 8)
  • Create breakpoints in the specific lines
iex> :int.break(Example, 8)
  • Delete a breakpoint in the specific line
iex> :int.delete_break(Example, 8)

Basics chapter 19 to 22

Chapter 19

Theme:

  • try, catch, and rescue.
  • Typespecs and behaviours.
  • Debugging.
  • Erlang libraries

Advanced chapter 4 to 6

Supervisor and application

Continuing the process management with supervisors and applications.

Supervisor

Creating supervisors in a module with the init/1 macro provided by:

use Supervisor

The init macro starts the modules (children) in a list in init:

@impl true
  def init(:ok) do
    children = [
      {KV.Registry, name: KV.Registry}
    ]

    # The strategy :one_for_one means that if a child in the supervisor dies it will be the only one restarted
    # This opcion will call the start_link implemetation of KV.Registry with the parameter "name: KV.Registry"
    Supervisor.init(children, strategy: :one_for_one)
  end

Application

Creating an application to start the supervisor once the system is on, with the module:

use Application

The start macro starts the supervisor with the name KV.Supervisor:

@impl true
def start(_type, _args) do
    KV.Supervisor.start_link(name: KV.Supervisor)
end

Dynamic supervisors

DynamicSupervisor

Continuing the process management with DynamicSupervisor.

To start a DynamicSupervisor we put it in the Supervidor implemented in chapter 4
as a children:

children = [
      # Inititializating the DynamicSupervisor for buckets
      # We should place the BucketSupervisor before the Registry cause it invokes the BucketSupervisor
      # The strategy :one_for_one means that if a child in the supervisor dies it will be the only one restarted
      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
      {KV.Registry, name: KV.Registry}
]

# The strategy :one_for_all means that if a child dies the supervisor will kill and restart all of its childs
# This opcion will call the start_link implemetation of KV.Registry with the parameter "name: KV.Registry"
Supervisor.init(children, strategy: :one_for_all)

And then we start the buckets in the cast callback of the registry like so:

# With this every bucket starts with the DynamicSupervisor
{:ok, bucket} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)

ETS

Continuing with process management mixing the ETS to the GenServer to do a cache mechanism

To mix the ETS with the GenServer i had to modify the registry file like so,
this is an example in the init method of GenServer where now im creating a
ets table directly for the registry:

names = :ets.new(table, [:named_table, read_concurrency: true])

To search now in the registry we fetch directly in the ets:

case :ets.lookup(server, name) do
    # The `^` is to force the name to be equals to the provided
    [{^name, pid}] -> {:ok, pid}
    [] -> :error
end

And in the test we have now to start the registry with a name provided by the test context:

# This is accessing to a non-shared partition of the state of KV.Registry
_ = start_supervised!({KV.Registry, name: context.test})

Advanced chapter 1 to 3

  • Starting a project with mix.
  • Learning the basics of mix.
  • Proving do tests with mix.
  • Starting with the use of Agents
  • Improving the use of tests
  • GenServer

Advanced chapter 7 to 9

Dependencies and umbrellas

Starting with dependencies and umbrella projects

Dependencies

The external dependencies in a project can be like so, with the name of the dependency and the version associated:

def deps do
  [{:plug, "~> 1.0"}]
end

You can also fetch dependencies and compile it with the tasks provided by mix:

mix deps.get 

and

mix deps.update

And the internal dependencies can be placed like so, in this case importing the kv module:

def deps do
  [{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
end

Umbrella projects

I started a new umbrella project where will be all the apps for this practice:

mix new kv_umbrella --umbrella

Every project in /apps needs to have the shared configurations of the umbrella project, like so:

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

Task and gen_tcp

Wrote an basic server using tasks and gen_tcp, firstly creating a method to accept connections in a port, and then serving that connections with a Task.Supervisor:

defp loop_acceptor(socket) do
    {:ok, client} = :gen_tcp.accept(socket)
    # We start a serve task with the Task Supervisor
    {:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)

    # Without this the acceptor will bring down all the clients if it crashed (Like when quitting a client)
    :ok = :gen_tcp.controlling_process(client, pid)
    loop_acceptor(socket)
end

The serve function inside the task just reads the line from the socket and then replies it to the socket in an infinite loop:

# This method reads a line from the socket and then replies the same line to the socket
  defp serve(socket) do
    socket
    |> read_line()
    |> write_line(socket)

    serve(socket)
end

Doctest, patterns and with

Doctests

Using doctest to document and make tests at the same time in a specific function, considering a test and a function like so:

@doc ~S"""
  Parses the given `line` into a command.

  ## Examples

    iex> KVServer.Command.parse "CREATE shopping\r\n"
    {:ok, {:create, "shopping"}}
  """
  def parse(line) do
    case String.split(line) do
      ["CREATE", bucket] -> {:ok, {:create, bucket}}
    end
  end

The example above can be tested with ExUnit and will only pass if the examples in the doc get the same result in the function, we can test the doc like so:

defmodule KVServer.CommandTest do
  use ExUnit.Case, async: true
  doctest KVServer.Command
end

with

The with allow us to do something similar to a pipeline (|>), to short our code,
for example, doing a flow like this:

msg =
      with {:ok, data} <- read_line(socket),
           {:ok, command} <- KVServer.Command.parse(data),
           do: KVServer.Command.run(command, KV.Registry)

testing the server with gen_tcp

To test the server we need to clean the state of the application like so:

setup do
    Application.stop(:kv)
    :ok = Application.start(:kv)
  end

And then create a socket connection to play like a cliente connecting to the server:

setup do
    opts = [:binary, packet: :line, active: false]
    # This connects to the server in the port 4040
    {:ok, socket} = :gen_tcp.connect('localhost', 4040, opts)
    %{socket: socket}
  end

And then make the requests:

test "server interaction", %{socket: socket} do
    assert send_and_recv(socket, "UNKNOWN shopping\r\n") == "UNKNOWN COMMAND\r\n"
  end

  # Method to send and receive messages working as a client
  defp send_and_recv(socket, command) do
    :ok = :gen_tcp.send(socket, command)
    {:ok, data} = :gen_tcp.recv(socket, 0, 1000)
    data
  end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.