├── class3 ├── myapp │ ├── test │ │ ├── test_helper.exs │ │ ├── myapp_test.exs │ │ └── exercises │ │ │ ├── exercise5_test.exs │ │ │ ├── exercise4_test.exs │ │ │ ├── exercise3_test.exs │ │ │ ├── exercise2_test.exs │ │ │ └── exercise1_test.exs │ ├── lib │ │ ├── myapp │ │ │ ├── item.ex │ │ │ ├── application.ex │ │ │ ├── supervisor.ex │ │ │ ├── stack_supervisor.ex │ │ │ ├── stack.ex │ │ │ └── shop_inventory.ex │ │ └── myapp.ex │ ├── .formatter.exs │ ├── README.md │ ├── .gitignore │ └── mix.exs ├── otp_additional.livemd └── otp.livemd ├── class4 ├── phoenix_hello │ ├── srv │ │ └── index.html │ ├── test │ │ ├── exercises │ │ │ └── exercise1_test.exs │ │ ├── test_helper.exs │ │ ├── phoenix_hello_web │ │ │ └── controllers │ │ │ │ ├── page_controller_test.exs │ │ │ │ ├── error_json_test.exs │ │ │ │ └── error_html_test.exs │ │ └── support │ │ │ └── conn_case.ex │ ├── .iex.exs │ ├── lib │ │ ├── phoenix_hello │ │ │ ├── mailer.ex │ │ │ ├── tests.ex │ │ │ ├── receiver.ex │ │ │ ├── manager.ex │ │ │ ├── application.ex │ │ │ └── manager_supervisor.ex │ │ ├── phoenix_hello.ex │ │ ├── phoenix_hello_web │ │ │ ├── controllers │ │ │ │ ├── page_controller.ex │ │ │ │ ├── page_html.ex │ │ │ │ ├── manager_controller.ex │ │ │ │ ├── error_json.ex │ │ │ │ └── error_html.ex │ │ │ ├── plug │ │ │ │ └── health_check.ex │ │ │ ├── components │ │ │ │ ├── layouts.ex │ │ │ │ └── layouts │ │ │ │ │ ├── root.html.heex │ │ │ │ │ └── app.html.heex │ │ │ ├── live │ │ │ │ ├── counter_live.ex │ │ │ │ └── time_live.ex │ │ │ ├── gettext.ex │ │ │ ├── router.ex │ │ │ ├── endpoint.ex │ │ │ └── telemetry.ex │ │ └── phoenix_hello_web.ex │ ├── priv │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── robots.txt │ │ │ └── images │ │ │ │ └── logo.svg │ │ └── gettext │ │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ │ └── errors.pot │ ├── Dockerfile.caddy │ ├── .formatter.exs │ ├── Dockerfile.phoenix │ ├── Caddyfile │ ├── config │ │ ├── prod.exs │ │ ├── test.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ └── runtime.exs │ ├── assets │ │ ├── css │ │ │ └── app.css │ │ ├── js │ │ │ └── app.js │ │ ├── tailwind.config.js │ │ └── vendor │ │ │ └── topbar.js │ ├── .gitignore │ └── mix.exs └── README.md ├── class2 ├── exercises │ ├── test │ │ ├── test_helper.exs │ │ ├── exercises_web │ │ │ └── controllers │ │ │ │ ├── page_controller_test.exs │ │ │ │ ├── error_json_test.exs │ │ │ │ └── error_html_test.exs │ │ ├── exercises │ │ │ ├── exercise1_test.exs │ │ │ ├── exercise2_test.exs │ │ │ ├── exercise4_test.exs │ │ │ ├── exercise5_test.exs │ │ │ ├── exercise9_test.exs │ │ │ ├── exercise8_test.exs │ │ │ ├── exercise3_test.exs │ │ │ ├── exercise7_test.exs │ │ │ └── exercise6_test.exs │ │ └── support │ │ │ └── conn_case.ex │ ├── lib │ │ ├── exercises │ │ │ ├── mailer.ex │ │ │ └── application.ex │ │ ├── exercises_web │ │ │ ├── components │ │ │ │ ├── layouts.ex │ │ │ │ └── layouts │ │ │ │ │ ├── root.html.heex │ │ │ │ │ └── app.html.heex │ │ │ ├── controllers │ │ │ │ ├── page_html.ex │ │ │ │ ├── page_controller.ex │ │ │ │ ├── error_json.ex │ │ │ │ └── error_html.ex │ │ │ ├── gettext.ex │ │ │ ├── router.ex │ │ │ ├── endpoint.ex │ │ │ └── telemetry.ex │ │ ├── exercises.ex │ │ ├── exercise1.ex │ │ ├── exercise2.ex │ │ ├── exercise4.ex │ │ ├── exercise5.ex │ │ ├── exercise9.ex │ │ ├── exercise8.ex │ │ ├── exercise3.ex │ │ ├── exercise7.ex │ │ ├── exercise6.ex │ │ └── exercises_web.ex │ ├── priv │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── robots.txt │ │ │ └── images │ │ │ │ └── logo.svg │ │ └── gettext │ │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ │ └── errors.pot │ ├── .formatter.exs │ ├── assets │ │ ├── css │ │ │ └── app.css │ │ ├── js │ │ │ └── app.js │ │ ├── tailwind.config.js │ │ └── vendor │ │ │ └── topbar.js │ ├── config │ │ ├── test.exs │ │ ├── prod.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ └── runtime.exs │ ├── README.md │ ├── .gitignore │ ├── mix.exs │ └── mix.lock ├── processes_DIY.livemd └── processes.livemd ├── class1 ├── solutions │ ├── recursive.ex │ ├── scrabble.ex │ └── fizz_buzz.ex ├── exercises.livemd ├── data_types.livemd └── flow_control.livemd └── README.md /class3/myapp/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /class4/phoenix_hello/srv/index.html: -------------------------------------------------------------------------------- 1 | Hello srv 2 | -------------------------------------------------------------------------------- /class2/exercises/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/exercises/exercise1_test.exs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /class4/phoenix_hello/.iex.exs: -------------------------------------------------------------------------------- 1 | alias PhoenixHello.Receiver 2 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/item.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.Item do 2 | defstruct name: nil, 3 | price: nil 4 | end 5 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Mailer do 2 | use Swoosh.Mailer, otp_app: :exercises 3 | end 4 | -------------------------------------------------------------------------------- /class2/exercises/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LKlemens/ElixirSchool2025/HEAD/class2/exercises/priv/static/favicon.ico -------------------------------------------------------------------------------- /class3/myapp/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.Mailer do 2 | use Swoosh.Mailer, otp_app: :phoenix_hello 3 | end 4 | -------------------------------------------------------------------------------- /class4/phoenix_hello/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LKlemens/ElixirSchool2025/HEAD/class4/phoenix_hello/priv/static/favicon.ico -------------------------------------------------------------------------------- /class4/phoenix_hello/Dockerfile.caddy: -------------------------------------------------------------------------------- 1 | FROM caddy:2.7 2 | 3 | COPY Caddyfile /etc/caddy/Caddyfile 4 | # COPY srv/index.html:/usr/share/caddy/index.html 5 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.Layouts do 2 | use ExercisesWeb, :html 3 | 4 | embed_templates "layouts/*" 5 | end 6 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.PageHTML do 2 | use ExercisesWeb, :html 3 | 4 | embed_templates "page_html/*" 5 | end 6 | -------------------------------------------------------------------------------- /class2/exercises/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /class3/myapp/test/myapp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyAppTest do 2 | use ExUnit.Case 3 | doctest MyApp 4 | 5 | test "greets the world" do 6 | assert MyApp.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /class2/exercises/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /class4/phoenix_hello/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /class2/exercises/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /class4/phoenix_hello/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp do 2 | @moduledoc """ 3 | Documentation for `MyApp`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> MyApp.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /class3/myapp/test/exercises/exercise5_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise5Test do 2 | use ExUnit.Case, async: false 3 | 4 | alias MyApp.ShopInventory 5 | 6 | @tag :exercise5 7 | test "Application should start ShopInventory" do 8 | assert [] == ShopInventory.list_items() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.PageControllerTest do 2 | use ExercisesWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises do 2 | @moduledoc """ 3 | Exercises keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/application.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.Application do 2 | use Application 3 | 4 | # =====EXERCISE 5===== 5 | @impl true 6 | def start(_type, _args) do 7 | children = [ 8 | StackSupervisor 9 | ] 10 | 11 | Supervisor.start_link(children, strategy: :one_for_one) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.PageController do 2 | use ExercisesWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello do 2 | @moduledoc """ 3 | PhoenixHello keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/phoenix_hello_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.PageControllerTest do 2 | use PhoenixHelloWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise1.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise1 do 2 | @doc """ 3 | Spawn a process and send :pong message to :ping process 4 | input: none 5 | returns: pid 6 | 7 | to test run in console: 8 | mix test --only test1 9 | """ 10 | def send_to_pong() do 11 | # write your code here 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.PageController do 2 | use PhoenixHelloWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use PhoenixHelloWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise2.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise2 do 2 | @doc """ 3 | Spawn a new process and register it under :hello name 4 | input: none 5 | returns: pid 6 | 7 | hint: take care of process lifetime 8 | 9 | to test run: 10 | mix test --only test2 11 | """ 12 | def create_registered_process() do 13 | # write your code here 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(init_arg) do 5 | Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) 6 | end 7 | 8 | # =====EXERCISE 4===== 9 | @impl true 10 | def init(_init_arg) do 11 | children = [] 12 | 13 | Supervisor.init(children, strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/controllers/manager_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ManagerController do 2 | use PhoenixHelloWeb, :controller 3 | 4 | def index(conn, _params) do 5 | PhoenixHello.ManagerSupervisor.start_random() 6 | # The home page is often custom made, 7 | # so skip the default app layout. 8 | render(conn, :index, layout: false) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise4.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise4 do 2 | @doc """ 3 | Spawn a new process, register it under :hello name, 4 | wait for :ping message and send a :timeout msg to :test process after 500ms. 5 | input: none 6 | returns: pid 7 | 8 | to test run in console: 9 | mix test --only test4 10 | """ 11 | def send_timeout() do 12 | # write your code here 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /class1/solutions/recursive.ex: -------------------------------------------------------------------------------- 1 | defmodule Recursive do 2 | def sum(n) do 3 | sum_recursive(n) 4 | end 5 | 6 | def sum_recursive(0) do 7 | 0 8 | end 9 | 10 | def sum_recursive(n) do 11 | n + sum_recursive(n - 1) 12 | end 13 | 14 | def sum_tail_recursive(0, acc) do 15 | acc 16 | end 17 | 18 | def sum_tail_recursive(n, acc) do 19 | sum_tail_recursive(n - 1, acc + n) 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /class2/exercises/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /class4/phoenix_hello/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /class2/exercises/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /class4/phoenix_hello/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/stack_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule StackSupervisor do 2 | use Supervisor 3 | 4 | def start_link(init_arg) do 5 | Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) 6 | end 7 | 8 | @impl true 9 | def init(_init_arg) do 10 | children = [ 11 | {MyApp.Stack, [:hello]} # GenServer allows us to shorten it 12 | ] 13 | 14 | Supervisor.init(children, strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise1Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test1 5 | test "Should return pid and send :ping to :pong process" do 6 | Process.register(self(), :ping) 7 | pid = Exercises.Exercise1.send_to_pong() 8 | assert is_pid(pid) == true, "Function should return pid" 9 | assert_receive :pong, 100, "Process should send :pong message to :ping proccess" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.ErrorJSONTest do 2 | use ExercisesWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert ExercisesWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert ExercisesWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise2Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test2 5 | test "Should create a new process and register it under :hello name" do 6 | pid = Exercises.Exercise2.create_registered_process() 7 | Process.sleep(200) 8 | assert is_pid(pid) == true, "Function should return pid" 9 | assert Process.whereis(:hello) != nil, "Process should be registered under :hello name" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/plug/health_check.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Plug.HealthCheck do 2 | import Plug.Conn 3 | 4 | def init(opts), do: opts 5 | 6 | def call(%Plug.Conn{request_path: "/healthcheck"} = conn, _opts) do 7 | conn 8 | |> delete_resp_header("cache-control") 9 | |> put_resp_header("content-type", "text/plain") 10 | |> send_resp(200, "{\"status\":\"ok\"}") 11 | |> halt() 12 | end 13 | 14 | def call(conn, _opts), do: conn 15 | end 16 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/phoenix_hello_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ErrorJSONTest do 2 | use PhoenixHelloWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert PhoenixHelloWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert PhoenixHelloWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise5.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise5 do 2 | @doc """ 3 | Spawn a new process, register it under :hello name, and pattern match on two messages - :hello and :world 4 | - waits for :world msg first, send it to :test process, 5 | - and after that waits for :hello msg and send it to :test process too 6 | 7 | input: none 8 | returns: pid 9 | 10 | to test run in console: 11 | mix test --only test5 12 | """ 13 | def selective_receive() do 14 | # write your code here 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.ErrorHTMLTest do 2 | use ExercisesWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(ExercisesWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(ExercisesWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/phoenix_hello_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ErrorHTMLTest do 2 | use PhoenixHelloWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(PhoenixHelloWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(PhoenixHelloWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /class3/myapp/README.md: -------------------------------------------------------------------------------- 1 | # Myapp 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `myapp` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:myapp, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use PhoenixHelloWeb, :controller` and 9 | `use PhoenixHelloWeb, :live_view`. 10 | """ 11 | use PhoenixHelloWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/tests.ex: -------------------------------------------------------------------------------- 1 | # defmodule PhoenixHello.Tests do 2 | # alias PhoenixHello.Receiver 3 | 4 | # def exercise1() do 5 | # [node | _rest] = Node.list() 6 | # {"hello", ^node} = Receiver.send_msg("hello") 7 | # end 8 | 9 | # def exercise2() do 10 | # number_of_nodes = length(Node.list()) + 1 11 | # responses = Receiver.send_msg_to_all_nodes("hello") 12 | 13 | # Enum.each(responses, fn response -> 14 | # {"hello", _} = response 15 | # end) 16 | 17 | # ^number_of_nodes = length(responses) 18 | # responses 19 | # end 20 | # end 21 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.ErrorJSON do 2 | # If you want to customize a particular status code, 3 | # you may add your own clauses, such as: 4 | # 5 | # def render("500.json", _assigns) do 6 | # %{errors: %{detail: "Internal Server Error"}} 7 | # end 8 | 9 | # By default, Phoenix returns the status message from 10 | # the template name. For example, "404.json" becomes 11 | # "Not Found". 12 | def render(template, _assigns) do 13 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /class3/myapp/test/exercises/exercise4_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise4Test do 2 | use ExUnit.Case, async: false 3 | 4 | alias MyApp.ShopInventory 5 | 6 | @tag :exercise4 7 | test "Should spawn ShopInventory" do 8 | MyApp.Supervisor.start_link([]) 9 | items = ShopInventory.list_items() 10 | assert [] == items 11 | end 12 | 13 | @tag :exercise4 14 | test "Should restart ShopInventory" do 15 | MyApp.Supervisor.start_link([]) 16 | GenServer.cast(ShopInventory, :crash) 17 | Process.sleep(100) 18 | items = ShopInventory.list_items() 19 | assert [] == items 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /class1/solutions/scrabble.ex: -------------------------------------------------------------------------------- 1 | defmodule Scrabble do 2 | def calculate_score(word) do 3 | word 4 | |> String.graphemes() 5 | |> Enum.map(&letter_score/1) 6 | |> Enum.sum() 7 | end 8 | 9 | defp letter_score(letter) do 10 | cond do 11 | String.contains?("aeioulnrst", letter) -> 1 12 | String.contains?("dg", letter) -> 2 13 | String.contains?("bcmp", letter) -> 3 14 | String.contains?("fhvwy", letter) -> 4 15 | String.contains?("k", letter) -> 5 16 | String.contains?("jx", letter) -> 8 17 | String.contains?("qz", letter) -> 10 18 | end 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/live/counter_live.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.CounterLive do 2 | use PhoenixHelloWeb, :live_view 3 | 4 | def render(assigns) do 5 | # TODO exercise9 display counter and add button "Add +1" 6 | ~H""" 7 |

todo

8 | """ 9 | end 10 | 11 | # Initialize timer when the LiveView mounts 12 | def mount(_params, _session, socket) do 13 | socket = assign(socket, :counter, 0) 14 | 15 | {:ok, socket} 16 | end 17 | 18 | def handle_event("counter", _params, socket) do 19 | # TODO exercise9 update counter here 20 | {:noreply, socket} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise9.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise9 do 2 | @doc """ 3 | Spawn :server process, which endlessly handles messages. 4 | Sever should pass messages to :test process. 5 | When server gets exit singal, then it should send :handle_exit message to :test process. 6 | Server should send :nothing_todo message to :test process after 500ms inactivity. 7 | Spawn unregistered client process which sends to server 10 messages 8 | 9 | 10 | to test run in console: 11 | mix test --only test9 12 | """ 13 | def server() do 14 | # write your code here 15 | end 16 | 17 | def client() do 18 | # write your code here 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise8.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise8 do 2 | @doc """ 3 | - Modify exercise7 by adding trap_exit to world process. 4 | - handle exit signal 5 | - send exit message to :test process 6 | - explain why :world process is alive or dead 7 | 8 | input: none 9 | returns: pid 10 | 11 | 12 | to test run in console: 13 | mix test --only test8 14 | """ 15 | def process_link() do 16 | pid_hello = 17 | spawn(fn -> 18 | receive do 19 | :bad_msg -> raise("error") 20 | :die_normally -> :ok 21 | end 22 | end) 23 | 24 | Process.register(pid_hello, :hello) 25 | 26 | # write here your code 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /class4/phoenix_hello/Dockerfile.phoenix: -------------------------------------------------------------------------------- 1 | # Use an official Elixir runtime as a parent image 2 | FROM elixir:1.15.7-otp-25 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | ENV MIX_ENV="prod" 8 | ENV SECRET_KEY_BASE="secret" 9 | 10 | # Copy the current directory contents into the container at /app 11 | COPY . /app 12 | 13 | # Install Hex and Rebar 14 | RUN mix local.hex --force && \ 15 | mix local.rebar --force 16 | 17 | # Install dependencies 18 | RUN mix deps.get 19 | 20 | # Compile the project 21 | RUN mix compile 22 | 23 | RUN mix assets.deploy 24 | 25 | RUN mix release 26 | 27 | # Run the Phoenix app 28 | CMD ["_build/prod/rel/phoenix_hello/bin/phoenix_hello", "start"] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir School 2025 2 | 3 | Welcome to the Elixir School 2025 4 | 5 | ## Goals 6 | 7 | During this lecture you will be introduced to basic Elixir syntax together with simple and complex data types. 8 | 9 | ## How to start? 10 | 11 | ### fly.io 12 | 13 | You can launch Livebook instance using https://fly.io/launch/livebook and following instructions on the website. 14 | 15 | ### Docker 16 | 17 | Alternately you can run Livebook instance in a docker container on your local machine: 18 | 19 | ```bash 20 | docker run -p 8080:8080 -p 8081:8081 --pull always -v $(pwd):/data ghcr.io/livebook-dev/livebook 21 | ``` 22 | 23 | or following the instructions from: https://github.com/livebook-dev/livebook#docker 24 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.ErrorHTML do 2 | use ExercisesWeb, :html 3 | 4 | # If you want to customize your error pages, 5 | # uncomment the embed_templates/1 call below 6 | # and add pages to the error directory: 7 | # 8 | # * lib/exercises_web/controllers/error_html/404.html.heex 9 | # * lib/exercises_web/controllers/error_html/500.html.heex 10 | # 11 | # embed_templates "error_html/*" 12 | 13 | # The default is to render a plain text page based on 14 | # the template name. For example, "404.html" becomes 15 | # "Not Found". 16 | def render(template, _assigns) do 17 | Phoenix.Controller.status_message_from_template(template) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "Exercises" %> 9 | 10 | 11 | 13 | 14 | 15 | <%= @inner_content %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /class3/myapp/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | myapp-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "PhoenixHello" %> 9 | 10 | 11 | 13 | 14 | 15 | <%= @inner_content %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /class4/phoenix_hello/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | # Enable Debug mode 3 | # debug 4 | 5 | # Disable admin API 6 | admin off 7 | 8 | } 9 | 10 | localhost:8080 { 11 | # https://caddyserver.com/docs/caddyfile/directives/log 12 | log { 13 | output stdout 14 | format console 15 | } 16 | 17 | 18 | # https://caddyserver.com/docs/caddyfile/directives/reverse_proxy 19 | reverse_proxy * { 20 | # Specify backend here 21 | to 127.0.0.1:4005 22 | to 127.0.0.1:4006 23 | 24 | lb_policy round_robin 25 | lb_try_duration 1s 26 | lb_try_interval 250ms 27 | 28 | health_uri /healthcheck # Backend health check path 29 | # health_port 80 # Default same as backend port 30 | health_interval 10s 31 | health_timeout 2s 32 | health_status 200 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.Stack do 2 | use GenServer 3 | 4 | # Client 5 | 6 | def start_link(initial_stack) when is_list(initial_stack) do 7 | GenServer.start_link(__MODULE__, initial_stack, name: __MODULE__) 8 | end 9 | 10 | def push(element) do 11 | GenServer.cast(__MODULE__, {:push, element}) 12 | end 13 | 14 | def pop() do 15 | GenServer.call(__MODULE__, :pop) 16 | end 17 | 18 | # Callbacks 19 | 20 | @impl true 21 | def init(stack) do 22 | {:ok, stack} 23 | end 24 | 25 | @impl true 26 | def handle_call(:pop, _from, [head | tail]) do 27 | {:reply, head, tail} 28 | end 29 | 30 | @impl true 31 | def handle_cast({:push, element}, state) do 32 | {:noreply, [element | state]} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise3.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise3 do 2 | @doc """ 3 | Spawn a new process, register it under :hello name, wait for :ping message and print it out. 4 | input: none 5 | returns: pid 6 | 7 | to test run in console: 8 | mix test --only test3.1 9 | """ 10 | def wait_and_print() do 11 | # write your code here 12 | end 13 | 14 | @doc """ 15 | Spawn a new process, register it under :hello name, wait for :ping message and print it out. 16 | Spawn another process and after 300ms send :shutdown signal to :hello process to terminate it. 17 | input: none 18 | returns: none 19 | 20 | to test run: 21 | mix test --only test3.2 22 | """ 23 | def terminate_process() do 24 | # write your code here 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise4_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise4Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test4 5 | test "Should send :timeout messgae after 500ms" do 6 | Process.register(self(), :test) 7 | pid = Exercises.Exercise4.send_timeout() 8 | assert is_pid(pid) == true, "Function should return pid" 9 | 10 | refute_receive :timeout, 11 | 490, 12 | ":test process should get :timeout message after 500ms - not earlier" 13 | 14 | assert Process.alive?(pid) == true, "Process should wait for a :ping message" 15 | 16 | assert_receive :timeout, 17 | 50, 18 | "Process :hello should send :timeout msg to :test process after 500ms" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /class3/myapp/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyApp.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :myapp, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {MyApp.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | end 30 | 31 | # TODO odpalic 3x counter i crash nr 2 32 | -------------------------------------------------------------------------------- /class2/exercises/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :exercises, ExercisesWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "KM4/Jf0H243Npm3lZXOwLQXy85opjIw8ZttZ+WCF8AwiNPcUGi07jR2XkNjPL34Y", 8 | server: false 9 | 10 | # In test we don't send emails. 11 | config :exercises, Exercises.Mailer, adapter: Swoosh.Adapters.Test 12 | 13 | # Disable swoosh api client as it is only required for production adapters. 14 | config :swoosh, :api_client, false 15 | 16 | # Print only warnings and errors during test 17 | config :logger, level: :warning 18 | 19 | # Initialize plugs at runtime for faster test compilation 20 | config :phoenix, :plug_init_mode, :runtime 21 | -------------------------------------------------------------------------------- /class2/exercises/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :exercises, ExercisesWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" 9 | 10 | # Configures Swoosh API Client 11 | config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Exercises.Finch 12 | 13 | # Disable Swoosh Local Memory Storage 14 | config :swoosh, local: false 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # Runtime production configuration, including reading 20 | # of environment variables, is done on config/runtime.exs. 21 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise5_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise5Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test5 5 | test "Test 5" do 6 | Process.register(self(), :test) 7 | pid = Exercises.Exercise5.selective_receive() 8 | assert is_pid(pid) == true, "Function should return pid" 9 | Process.sleep(200) 10 | IO.inspect("send world") 11 | send(:hello, :world) 12 | Process.sleep(200) 13 | IO.inspect("send hello") 14 | send(:hello, :hello) 15 | 16 | assert_receive :world, 17 | 1000, 18 | "Process :hello should send :second msg to :test process as first message" 19 | 20 | assert_receive :hello, 21 | 1000, 22 | "Process :hello should send :first msg to :test process as second message" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /class2/exercises/README.md: -------------------------------------------------------------------------------- 1 | # Exercises 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Exercises 13 | 14 | Exercises are in lib/ directory 15 | 16 | ## To run test 17 | 18 | mix test --only test1 19 | 20 | ## Learn more 21 | 22 | * Official website: https://www.phoenixframework.org/ 23 | * Guides: https://hexdocs.pm/phoenix/overview.html 24 | * Docs: https://hexdocs.pm/phoenix 25 | * Forum: https://elixirforum.com/c/phoenix-forum 26 | * Source: https://github.com/phoenixframework/phoenix 27 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import ExercisesWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext.Backend, otp_app: :exercises 24 | end 25 | -------------------------------------------------------------------------------- /class4/phoenix_hello/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Configures Swoosh API Client 12 | config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: PhoenixHello.Finch 13 | 14 | # Disable Swoosh Local Memory Storage 15 | config :swoosh, local: false 16 | 17 | # Do not print debug messages in production 18 | config :logger, level: :info 19 | 20 | # Runtime production configuration, including reading 21 | # of environment variables, is done on config/runtime.exs. 22 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import PhoenixHelloWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext.Backend, otp_app: :phoenix_hello 24 | end 25 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use PhoenixHelloWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/phoenix_hello_web/controllers/error_html/404.html.heex 14 | # * lib/phoenix_hello_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /class1/solutions/fizz_buzz.ex: -------------------------------------------------------------------------------- 1 | defmodule FizzBuzz do 2 | def fizzbuzz(n) do 3 | fizzbuzz_recursive(n) 4 | end 5 | 6 | defp fizzbuzz_recursive(0) do 7 | [] 8 | end 9 | 10 | defp fizzbuzz_recursive(n) do 11 | [map_number(n) | fizzbuzz_recursive(n - 1)] 12 | end 13 | 14 | defp fizzbuzz_tail_recursive(0, acc) do 15 | acc 16 | end 17 | 18 | defp fizzbuzz_tail_recursive(n, acc) do 19 | fizzbuzz_tail_recursive(n - 1, [map_number(n) | acc]) 20 | end 21 | 22 | defp fizzbuzz_higher_order_f(n) do 23 | 1..n//1 24 | |> Enum.map(&map_number/1) 25 | end 26 | 27 | defp fizzbuzz_comprehension(n) do 28 | for i <- 1..n//1, do: map_number(i) 29 | end 30 | 31 | defp map_number(n) do 32 | by_3 = rem(n, 3) == 0 33 | by_5 = rem(n, 5) == 0 34 | cond do 35 | by_3 and by_5 -> :fizzbuzz 36 | by_3 -> :fizz 37 | by_5 -> :buzz 38 | true -> n 39 | end 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /class4/phoenix_hello/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "Oi2xmWay2yWYK6u9eeZwIQQmWDpzl3U3gQE4yEHVrPJFcI8SyxuC7u7FosO7X0Us", 8 | server: false 9 | 10 | # In test we don't send emails. 11 | config :phoenix_hello, PhoenixHello.Mailer, adapter: Swoosh.Adapters.Test 12 | 13 | # Disable swoosh api client as it is only required for production adapters. 14 | config :swoosh, :api_client, false 15 | 16 | # Print only warnings and errors during test 17 | config :logger, level: :warning 18 | 19 | # Initialize plugs at runtime for faster test compilation 20 | config :phoenix, :plug_init_mode, :runtime 21 | 22 | config :phoenix_live_view, 23 | # Enable helpful, but potentially expensive runtime checks 24 | enable_expensive_runtime_checks: true 25 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise7.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise7 do 2 | @doc """ 3 | - Spawn a new process, 4 | - register it under :world name, 5 | - link to :hello process 6 | - after 1 second send :bad_msg to :hello process 7 | - wait for next message 8 | - spawn a new unregistered process, 9 | - wait 1500ms 10 | - check if process :world is alive and: 11 | - send to :test process ":world is alive!" if process :world is alive 12 | - send to :test process ":world is dead!" otherwise 13 | - explain why :world process is alive or dead 14 | input: none 15 | returns: pid 16 | 17 | 18 | to test run in console: 19 | mix test --only test7 20 | """ 21 | def process_link() do 22 | pid_hello = 23 | spawn(fn -> 24 | receive do 25 | :bad_msg -> raise("error") 26 | :die_normally -> :ok 27 | end 28 | end) 29 | 30 | Process.register(pid_hello, :hello) 31 | 32 | # write here your code 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /class4/phoenix_hello/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | 7 | .button { 8 | display: inline-block; 9 | padding: 10px 20px; 10 | font-size: 16px; 11 | font-weight: bold; 12 | color: #fff; 13 | background-color: #3498db; 14 | border: none; 15 | border-radius: 5px; 16 | cursor: pointer; 17 | text-decoration: none; 18 | position: relative; 19 | overflow: hidden; 20 | transition: all 0.3s ease-out; /* Transition for color change */ 21 | } 22 | 23 | .button::before { 24 | content: ""; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | height: 100%; 30 | background-color: rgba(255, 255, 255, 0.5); 31 | z-index: -1; 32 | transition: transform 0.5s ease-out; /* Transition for scale effect */ 33 | transform: scaleX(0); 34 | transform-origin: left; 35 | } 36 | 37 | .button:hover::before { 38 | transform: scaleX(1); 39 | } 40 | -------------------------------------------------------------------------------- /class2/exercises/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | exercises-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercise6.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise6 do 2 | @doc """ 3 | - Spawn a new process, 4 | - register it under :world name, 5 | - start monitoring :hello process by :world process, 6 | - after 1 second send :bad_msg to :hello process 7 | - wait for down msg from :hello process and send it to :test process 8 | - wait for next message 9 | - spawn a new unregistered process, 10 | - wait 1500ms 11 | - print ":world is alive!" if process :world is alive 12 | - print ":world is dead!" otherwise 13 | - explain why :world process is alive or dead 14 | input: none 15 | returns: pid 16 | 17 | 18 | to test run in console: 19 | mix test --only test6 20 | """ 21 | def process_monitor() do 22 | _hello = 23 | spawn(fn -> 24 | Process.register(self(), :hello) 25 | 26 | receive do 27 | :bad_msg -> raise("error") 28 | :die_normally -> :ok 29 | end 30 | end) 31 | 32 | # write here your code 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /class4/phoenix_hello/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | phoenix_hello-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise9_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise9Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test9 5 | test "Test 9" do 6 | Process.register(self(), :test) 7 | Exercises.Exercise9.server() 8 | Process.sleep(100) 9 | Exercises.Exercise9.client() 10 | pid_server = Process.whereis(:server) 11 | assert pid_server != nil, "Process :server should be started" 12 | 13 | Enum.each(1..10, fn _ -> 14 | assert_receive _, 100, "Process :test should get 10 messages from :server process" 15 | end) 16 | 17 | spawn(fn -> 18 | Process.link(pid_server) 19 | raise("error") 20 | end) 21 | 22 | assert_receive :handle_exit, 23 | 100, 24 | "Process :test should get :handle_exit message from :server process" 25 | 26 | assert_receive :nothing_todo, 27 | 600, 28 | "Process :test should get :nothing_todo message from :server process after 500ms inactivity" 29 | 30 | Process.sleep(500) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise8_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise8Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test8 5 | test "Test 8" do 6 | Process.register(self(), :test) 7 | Exercises.Exercise8.process_link() 8 | Process.sleep(100) 9 | pid_hello = Process.whereis(:hello) 10 | pid_world = Process.whereis(:world) 11 | assert pid_hello != nil, "Process :hello should be started" 12 | assert pid_world != nil, "Process :world should be started" 13 | 14 | assert Process.info(pid_hello, :links) == {:links, [pid_world]}, 15 | "Process :hello should be linked with :world process" 16 | 17 | assert Process.info(pid_world, :links) == {:links, [pid_hello]}, 18 | "Process :world should be linked with :hello process" 19 | 20 | Process.sleep(2000) 21 | assert Process.alive?(pid_hello) == false, "Process :hello should be terminated" 22 | assert Process.alive?(pid_world) == true, "Process :world should be alive" 23 | 24 | assert_receive {:EXIT, ^pid_hello, _}, 25 | 100, 26 | "Process :world should pass {:EXIT, ...} message to :test process" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise3_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise3Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :"test3.1" 5 | test "Should return pid, register it under :hello name, handle msg and termiante" do 6 | pid = Exercises.Exercise3.wait_and_print() 7 | assert is_pid(pid) == true, "Function should return pid" 8 | Process.sleep(300) 9 | assert Process.alive?(pid) == true, "Process should wait for a :ping message" 10 | send(pid, :ping) 11 | Process.sleep(300) 12 | assert Process.alive?(pid) == false, "Process should terminate after receiving msg" 13 | end 14 | 15 | @tag :"test3.2" 16 | test "Should return pid, register it under :hello name, handle msg and terminated by :shutdown signal" do 17 | Exercises.Exercise3.terminate_process() 18 | Process.sleep(100) 19 | pid = Process.whereis(:hello) 20 | assert is_pid(pid) == true, "Function should return pid" 21 | Process.sleep(100) 22 | assert Process.alive?(pid) == true, "Process should wait for a :ping message" 23 | Process.sleep(200) 24 | assert Process.alive?(pid) == false, "Process should terminate after getting :shutdown signal" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/receiver.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.Receiver do 2 | use GenServer 3 | 4 | def start_link(opts \\ nil) do 5 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 6 | end 7 | 8 | @impl GenServer 9 | def init(opts) do 10 | {:ok, opts} 11 | end 12 | 13 | @doc """ 14 | Function that sends msg to Receiver process started on connected node. 15 | """ 16 | def send_msg(_msg) do 17 | # write your code here 18 | end 19 | 20 | @doc """ 21 | Function that sends msg to all Receiver processes in a cluster including the node from which msg was sent 22 | and returns list of responses from nodes. 23 | """ 24 | def send_msg_to_all_nodes(_msg) do 25 | # write your code here 26 | end 27 | 28 | @impl GenServer 29 | def handle_call({msg, node}, _from, state) when is_atom(node) do 30 | IO.inspect( 31 | "#{inspect(msg)} message received on node #{inspect(Node.self())} from node: #{inspect(node)}" 32 | ) 33 | 34 | {:reply, {msg, Node.self()}, state} 35 | end 36 | 37 | @impl GenServer 38 | def handle_info(msg, state) do 39 | IO.inspect("#{inspect(msg)} message received on node #{inspect(Node.self())}") 40 | {:noreply, state} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /class2/exercises/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use ExercisesWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint ExercisesWeb.Endpoint 24 | 25 | use ExercisesWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import ExercisesWeb.ConnCase 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/manager.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.Manager do 2 | use GenServer 3 | 4 | def start_link(name) do 5 | GenServer.start_link(__MODULE__, name, name: via_tuple(name)) 6 | end 7 | 8 | @impl GenServer 9 | def init(name) do 10 | Process.flag(:trap_exit, true) 11 | IO.inspect("initialize manager #{inspect(self())}, name: #{inspect(name)}") 12 | {:ok, name} 13 | end 14 | 15 | def get_name(pid) do 16 | GenServer.call(pid, :get_name) 17 | end 18 | 19 | @impl GenServer 20 | def handle_call(:get_name, _from, name) do 21 | {:reply, name, name} 22 | end 23 | 24 | @impl GenServer 25 | def handle_info({:EXIT, _from, reason}, state) do 26 | IO.inspect("#{inspect(reason)} EXIT message received on node #{inspect(Node.self())}") 27 | {:stop, reason, state} 28 | end 29 | 30 | def handle_info(msg, state) do 31 | IO.inspect("#{inspect(msg)} message received on node #{inspect(Node.self())}") 32 | {:noreply, state} 33 | end 34 | 35 | @impl GenServer 36 | def terminate(reason, _state) do 37 | IO.inspect(reason, label: "terminate #{inspect(self())}") 38 | end 39 | 40 | defp via_tuple(name), do: {:via, Horde.Registry, {PhoenixHello.DistributedRegistry, name}} 41 | end 42 | -------------------------------------------------------------------------------- /class4/phoenix_hello/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use PhoenixHelloWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint PhoenixHelloWeb.Endpoint 24 | 25 | use PhoenixHelloWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import PhoenixHelloWeb.ConnCase 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v<%= Application.spec(:phoenix, :vsn) %> 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | <%= @inner_content %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /class3/myapp/test/exercises/exercise3_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise3Test do 2 | use ExUnit.Case, async: false 3 | 4 | alias MyApp.{Item, ShopInventory} 5 | 6 | @tag :exercise3 7 | test "Should list init items" do 8 | init_items = [%Item{name: "bread", price: 6}] 9 | ShopInventory.start_link(init_items) 10 | items = ShopInventory.list_items() 11 | assert init_items == items 12 | end 13 | 14 | @tag :exercise3 15 | test "Should get item by name" do 16 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 17 | ShopInventory.start_link(init_items) 18 | item = ShopInventory.get_item_by_name("bread") 19 | assert init_item == item 20 | end 21 | 22 | @tag :exercise3 23 | test "Should create an item" do 24 | ShopInventory.start_link([]) 25 | new_item = %Item{name: "bread", price: 6} 26 | ShopInventory.create_item(new_item) 27 | items = ShopInventory.list_items() 28 | assert [new_item] == items 29 | end 30 | 31 | @tag :exercise3 32 | test "Should delete an item" do 33 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 34 | ShopInventory.start_link(init_items) 35 | ShopInventory.delete_item(init_item) 36 | items = ShopInventory.list_items() 37 | assert [] == items 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v<%= Application.spec(:phoenix, :vsn) %> 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | <%= @inner_content %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | ExercisesWeb.Telemetry, 12 | {DNSCluster, query: Application.get_env(:exercises, :dns_cluster_query) || :ignore}, 13 | {Phoenix.PubSub, name: Exercises.PubSub}, 14 | # Start the Finch HTTP client for sending emails 15 | {Finch, name: Exercises.Finch}, 16 | # Start a worker by calling: Exercises.Worker.start_link(arg) 17 | # {Exercises.Worker, arg}, 18 | # Start to serve requests, typically the last entry 19 | ExercisesWeb.Endpoint 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: Exercises.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | 28 | # Tell Phoenix to update the endpoint configuration 29 | # whenever the application is updated. 30 | @impl true 31 | def config_change(changed, _new, removed) do 32 | ExercisesWeb.Endpoint.config_change(changed, removed) 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise7_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise7Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test7 5 | test "Test 7" do 6 | Process.register(self(), :test) 7 | unregistered = Exercises.Exercise7.process_link() 8 | Process.sleep(100) 9 | pid_hello = Process.whereis(:hello) 10 | pid_world = Process.whereis(:world) 11 | assert pid_hello != nil, "Process :hello should be started" 12 | assert pid_world != nil, "Process :world should be started" 13 | 14 | assert Process.info(pid_hello, :links) == {:links, [pid_world]}, 15 | "Process :hello should be linked with :world process" 16 | 17 | assert Process.info(pid_world, :links) == {:links, [pid_hello]}, 18 | "Process :world should be linked with :hello process" 19 | 20 | Process.sleep(1000) 21 | assert Process.alive?(unregistered) == true, "unregistered process should be alive" 22 | Process.sleep(1000) 23 | assert Process.alive?(pid_hello) == false, "Process :hello should be terminated" 24 | assert Process.alive?(pid_world) == false, "Process :world should be terminated" 25 | 26 | assert_receive ":world is dead!", 27 | 1000, 28 | "Process :world should be dead - :test process received no msg or wrong msg" 29 | 30 | Process.sleep(1000) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /class3/myapp/test/exercises/exercise2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise2Test do 2 | use ExUnit.Case, async: false 3 | 4 | alias MyApp.{Item, ShopInventory} 5 | 6 | @tag :exercise2 7 | test "Should list init items" do 8 | init_items = [%Item{name: "bread", price: 6}] 9 | {:ok, pid} = ShopInventory.start_link(init_items) 10 | items = ShopInventory.list_items(pid) 11 | assert init_items == items 12 | end 13 | 14 | @tag :exercise2 15 | test "Should get item by name" do 16 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 17 | {:ok, pid} = ShopInventory.start_link(init_items) 18 | item = ShopInventory.get_item_by_name(pid, "bread") 19 | assert init_item == item 20 | end 21 | 22 | @tag :exercise2 23 | test "Should create an item" do 24 | {:ok, pid} = ShopInventory.start_link([]) 25 | new_item = %Item{name: "bread", price: 6} 26 | ShopInventory.create_item(pid, new_item) 27 | items = ShopInventory.list_items(pid) 28 | assert [new_item] == items 29 | end 30 | 31 | @tag :exercise2 32 | test "Should delete an item" do 33 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 34 | {:ok, pid} = ShopInventory.start_link(init_items) 35 | ShopInventory.delete_item(pid, init_item) 36 | items = ShopInventory.list_items(pid) 37 | assert [] == items 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.Router do 2 | use ExercisesWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {ExercisesWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", ExercisesWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :home 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", ExercisesWeb do 25 | # pipe_through :api 26 | # end 27 | 28 | # Enable LiveDashboard and Swoosh mailbox preview in development 29 | if Application.compile_env(:exercises, :dev_routes) do 30 | # If you want to use the LiveDashboard in production, you should put 31 | # it behind authentication and allow only admins to access it. 32 | # If your application does not have an admins-only section yet, 33 | # you can use Plug.BasicAuth to set up some basic authentication 34 | # as long as you are also using SSL (which you should anyway). 35 | import Phoenix.LiveDashboard.Router 36 | 37 | scope "/dev" do 38 | pipe_through :browser 39 | 40 | live_dashboard "/dashboard", metrics: ExercisesWeb.Telemetry 41 | forward "/mailbox", Plug.Swoosh.MailboxPreview 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /class3/myapp/lib/myapp/shop_inventory.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.ShopInventory do 2 | use GenServer 3 | 4 | # =====EXERCISE 2===== 5 | # Client API 6 | def start_link(_opts) do 7 | GenServer.start_link(__MODULE__, []) 8 | end 9 | 10 | def create_item(_pid, _item) do 11 | # Your code here 12 | end 13 | 14 | def list_items(_pid) do 15 | # Your code here 16 | end 17 | 18 | def delete_item(_pid, _item) do 19 | # Your code here 20 | end 21 | 22 | def get_item_by_name(_pid, _name) do 23 | # Your code here 24 | end 25 | 26 | # =====EXERCISE 3===== 27 | def create_item(_item) do 28 | # Your code here 29 | end 30 | 31 | def list_items() do 32 | # Your code here 33 | end 34 | 35 | def delete_item(_item) do 36 | # Your code here 37 | end 38 | 39 | def get_item_by_name(_name) do 40 | # Your code here 41 | end 42 | 43 | # =====EXERCISE 1===== 44 | # Server API 45 | @impl true 46 | def init(_) do 47 | {:ok, []} 48 | end 49 | 50 | @impl true 51 | def handle_call(:list_items, _, state) do 52 | {:reply, :ok, state} 53 | end 54 | 55 | def handle_call({:get_item_by_name, _name}, _, state) do 56 | {:reply, :ok, state} 57 | end 58 | 59 | @impl true 60 | def handle_cast({:create_item, _item}, state) do 61 | {:noreply, state} 62 | end 63 | 64 | def handle_cast({:delete_item, _item}, state) do 65 | {:noreply, state} 66 | end 67 | 68 | # For supervisor testing 69 | def handle_cast(:crash, _state) do 70 | throw(:error) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /class2/exercises/test/exercises/exercise6_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise6Test do 2 | use ExUnit.Case, async: false 3 | 4 | @tag :test6 5 | test "Test 6" do 6 | Process.register(self(), :test) 7 | unregistered = Exercises.Exercise6.process_monitor() 8 | Process.sleep(100) 9 | pid_hello = Process.whereis(:hello) 10 | pid_world = Process.whereis(:world) 11 | assert pid_hello != nil, "Process :hello should be started" 12 | assert pid_world != nil, "Process :world should be started" 13 | 14 | assert Process.info(pid_hello, :monitored_by) == {:monitored_by, [pid_world]}, 15 | "Process :hello should be monitored by process :world" 16 | 17 | assert {:monitors, [process: ^pid_hello]} = Process.info(pid_world, :monitors), 18 | "Process :world should monitor process :hello" 19 | 20 | Process.sleep(1000) 21 | assert Process.alive?(unregistered) == true, "unregistered process should be alive" 22 | Process.sleep(1000) 23 | assert Process.alive?(pid_hello) == false, "Process :hello should be terminated" 24 | assert Process.alive?(pid_world) == true, "Process :world should be alive" 25 | 26 | assert_receive {:DOWN, _, :process, _, _}, 27 | 1000, 28 | "Process :world should pass {:DOWN, ...} message to :test process" 29 | 30 | assert_receive ":world is alive!", 31 | 1000, 32 | "Process :world should be alive - :test process received no msg or wrong msg" 33 | 34 | Process.sleep(1000) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Router do 2 | use PhoenixHelloWeb, :router 3 | 4 | pipeline :browser do 5 | plug(:accepts, ["html"]) 6 | plug(:fetch_session) 7 | plug(:fetch_live_flash) 8 | plug(:put_root_layout, html: {PhoenixHelloWeb.Layouts, :root}) 9 | plug(:protect_from_forgery) 10 | plug(:put_secure_browser_headers) 11 | end 12 | 13 | pipeline :api do 14 | plug(:accepts, ["json"]) 15 | end 16 | 17 | scope "/", PhoenixHelloWeb do 18 | pipe_through(:browser) 19 | 20 | # TODO exercise9 add live endpoint here 21 | live("/hello", TimeLive) 22 | end 23 | 24 | scope "/", PhoenixHelloWeb do 25 | pipe_through(:browser) 26 | 27 | get("/", PageController, :home) 28 | end 29 | 30 | # Other scopes may use custom stacks. 31 | # scope "/api", PhoenixHelloWeb do 32 | # pipe_through :api 33 | # end 34 | 35 | # Enable LiveDashboard and Swoosh mailbox preview in development 36 | if Application.compile_env(:phoenix_hello, :dev_routes) do 37 | # If you want to use the LiveDashboard in production, you should put 38 | # it behind authentication and allow only admins to access it. 39 | # If your application does not have an admins-only section yet, 40 | # you can use Plug.BasicAuth to set up some basic authentication 41 | # as long as you are also using SSL (which you should anyway). 42 | import Phoenix.LiveDashboard.Router 43 | 44 | scope "/dev" do 45 | pipe_through(:browser) 46 | 47 | live_dashboard("/dashboard", metrics: PhoenixHelloWeb.Telemetry) 48 | forward("/mailbox", Plug.Swoosh.MailboxPreview) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /class3/myapp/test/exercises/exercise1_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.Exercise1Test do 2 | use ExUnit.Case, async: false 3 | 4 | alias MyApp.{Item, ShopInventory} 5 | 6 | @tag :exercise1 7 | test "Should list init items" do 8 | init_items = [%Item{name: "bread", price: 6}] 9 | {:ok, pid} = GenServer.start_link(ShopInventory, init_items) 10 | items = GenServer.call(pid, :list_items) 11 | assert init_items == items 12 | end 13 | 14 | @tag :exercise1 15 | test "Should get item by name" do 16 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 17 | {:ok, pid} = GenServer.start_link(ShopInventory, init_items) 18 | item = GenServer.call(pid, {:get_item_by_name, "bread"}) 19 | assert init_item == item 20 | end 21 | 22 | @tag :exercise1 23 | test "Should return nil when no item found" do 24 | init_items = [%Item{name: "bread", price: 6}] 25 | {:ok, pid} = GenServer.start_link(ShopInventory, init_items) 26 | item = GenServer.call(pid, {:get_item_by_name, "juice"}) 27 | assert nil == item 28 | end 29 | 30 | @tag :exercise1 31 | test "Should create an item" do 32 | {:ok, pid} = GenServer.start_link(ShopInventory, []) 33 | new_item = %Item{name: "bread", price: 6} 34 | GenServer.cast(pid, {:create_item, new_item}) 35 | items = GenServer.call(pid, :list_items) 36 | assert [new_item] == items 37 | end 38 | 39 | @tag :exercise1 40 | test "Should delete an item" do 41 | init_items = [init_item] = [%Item{name: "bread", price: 6}] 42 | {:ok, pid} = GenServer.start_link(ShopInventory, init_items) 43 | GenServer.cast(pid, {:delete_item, init_item}) 44 | items = GenServer.call(pid, :list_items) 45 | assert [] == items 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :exercises 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_exercises_key", 10 | signing_salt: "x9xPfMe5", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :exercises, 25 | gzip: false, 26 | only: ExercisesWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | end 35 | 36 | plug Phoenix.LiveDashboard.RequestLogger, 37 | param_key: "request_logger", 38 | cookie_key: "request_logger" 39 | 40 | plug Plug.RequestId 41 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 42 | 43 | plug Plug.Parsers, 44 | parsers: [:urlencoded, :multipart, :json], 45 | pass: ["*/*"], 46 | json_decoder: Phoenix.json_library() 47 | 48 | plug Plug.MethodOverride 49 | plug Plug.Head 50 | plug Plug.Session, @session_options 51 | plug ExercisesWeb.Router 52 | end 53 | -------------------------------------------------------------------------------- /class2/exercises/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, { 27 | longPollFallbackMs: 2500, 28 | params: {_csrf_token: csrfToken} 29 | }) 30 | 31 | // Show progress bar on live navigation and form submits 32 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 33 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 34 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 35 | 36 | // connect if there are any LiveViews on the page 37 | liveSocket.connect() 38 | 39 | // expose liveSocket on window for web console debug logs and latency simulation: 40 | // >> liveSocket.enableDebug() 41 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 42 | // >> liveSocket.disableLatencySim() 43 | window.liveSocket = liveSocket 44 | 45 | -------------------------------------------------------------------------------- /class4/phoenix_hello/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, { 27 | longPollFallbackMs: 2500, 28 | params: {_csrf_token: csrfToken} 29 | }) 30 | 31 | // Show progress bar on live navigation and form submits 32 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 33 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 34 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 35 | 36 | // connect if there are any LiveViews on the page 37 | liveSocket.connect() 38 | 39 | // expose liveSocket on window for web console debug logs and latency simulation: 40 | // >> liveSocket.enableDebug() 41 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 42 | // >> liveSocket.disableLatencySim() 43 | window.liveSocket = liveSocket 44 | 45 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phoenix_hello 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_phoenix_hello_key", 10 | signing_salt: "W/r0UIwe", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket("/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | ) 18 | 19 | # Serve at "/" the static files from "priv/static" directory. 20 | # 21 | # You should set gzip to true if you are running phx.digest 22 | # when deploying your static files in production. 23 | plug(Plug.Static, 24 | at: "/", 25 | from: :phoenix_hello, 26 | gzip: false, 27 | only: PhoenixHelloWeb.static_paths() 28 | ) 29 | 30 | # Code reloading can be explicitly enabled under the 31 | # :code_reloader configuration of your endpoint. 32 | if code_reloading? do 33 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 34 | plug(Phoenix.LiveReloader) 35 | plug(Phoenix.CodeReloader) 36 | end 37 | 38 | plug(Phoenix.LiveDashboard.RequestLogger, 39 | param_key: "request_logger", 40 | cookie_key: "request_logger" 41 | ) 42 | 43 | plug(Plug.RequestId) 44 | plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) 45 | 46 | plug(Plug.Parsers, 47 | parsers: [:urlencoded, :multipart, :json], 48 | pass: ["*/*"], 49 | json_decoder: Phoenix.json_library() 50 | ) 51 | 52 | plug(Plug.MethodOverride) 53 | plug(Plug.Head) 54 | plug(Plug.Session, @session_options) 55 | plug(PhoenixHelloWeb.Plug.HealthCheck) 56 | plug(PhoenixHelloWeb.Router) 57 | end 58 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | # exercise7: setup libcluster strategy that automatically connects node on localhost 11 | # https://hexdocs.pm/libcluster/readme.html#strategy-configuration 12 | # topologies = [ 13 | # example: [ 14 | # strategy: choose_strategy, 15 | # config: [timeout: 1000] 16 | # ] 17 | # ] 18 | 19 | children = [ 20 | PhoenixHelloWeb.Telemetry, 21 | {DNSCluster, query: Application.get_env(:phoenix_hello, :dns_cluster_query) || :ignore}, 22 | {Phoenix.PubSub, name: PhoenixHello.PubSub}, 23 | # Start the Finch HTTP client for sending emails 24 | {Finch, name: PhoenixHello.Finch}, 25 | # exercise7: uncomment 26 | # {Cluster.Supervisor, [topologies, [name: PhoenixHello.ClusterSupervisor]]}, 27 | # exercice6: uncomment Horde.Registry & PhoenixHello.ManagerSupervisor 28 | # {Horde.Registry, [name: PhoenixHello.DistributedRegistry, keys: :unique, members: :auto]}, 29 | # {PhoenixHello.ManagerSupervisor, 30 | # strategy: :one_for_one, members: :auto, process_redistribution: :active}, 31 | # Start a worker by calling: PhoenixHello.Worker.start_link(arg) 32 | # {PhoenixHello.Worker, arg}, 33 | # Start to serve requests, typically the last entry 34 | PhoenixHelloWeb.Endpoint 35 | ] 36 | 37 | # See https://hexdocs.pm/elixir/Supervisor.html 38 | # for other strategies and supported options 39 | opts = [strategy: :one_for_one, name: PhoenixHello.Supervisor] 40 | Supervisor.start_link(children, opts) 41 | end 42 | 43 | # Tell Phoenix to update the endpoint configuration 44 | # whenever the application is updated. 45 | @impl true 46 | def config_change(changed, _new, removed) do 47 | PhoenixHelloWeb.Endpoint.config_change(changed, removed) 48 | :ok 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/live/time_live.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.TimeLive do 2 | use PhoenixHelloWeb, :live_view 3 | 4 | def render(assigns) do 5 | ~H""" 6 | {@timer_value} 7 | 8 |
9 | 10 | 11 |
12 | 17 | """ 18 | end 19 | 20 | # Initialize timer when the LiveView mounts 21 | def mount(_params, _session, socket) do 22 | list_of_running_managers = list_of_running_managers() 23 | 24 | socket = 25 | socket 26 | |> assign(:timer_value, get_time()) 27 | |> assign(:list_of_running_managers, list_of_running_managers) 28 | |> assign(:button_clicked, false) 29 | 30 | schedule_update_timer(socket) 31 | {:ok, socket} 32 | end 33 | 34 | # Handle timer messages to update the LiveView 35 | def handle_info(:update_timer, socket) do 36 | schedule_update_timer(socket) 37 | {:noreply, update_timer(socket)} 38 | end 39 | 40 | def handle_event("start_managers", _params, socket) do 41 | list_of_running_managers = PhoenixHello.ManagerSupervisor.start_random() |> Enum.sort() 42 | 43 | socket = assign(socket, :list_of_running_managers, list_of_running_managers) 44 | # Perform any desired actions when the button is clicked 45 | {:noreply, assign(socket, button_clicked: true)} 46 | end 47 | 48 | # Helper function to schedule timer updates 49 | defp schedule_update_timer(socket) do 50 | # Update every second 51 | Process.send_after(self(), :update_timer, 100) 52 | socket 53 | end 54 | 55 | # Helper function to update LiveView state 56 | defp update_timer(socket) do 57 | new_state = %{timer_value: get_time()} 58 | assign(socket, new_state) 59 | end 60 | 61 | defp list_of_running_managers() do 62 | Horde.Registry.select(PhoenixHello.DistributedRegistry, [{{:"$1", :_, :_}, [], [:"$1"]}]) 63 | |> Enum.sort() 64 | end 65 | 66 | def get_time do 67 | DateTime.utc_now() |> DateTime.truncate(:second) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /class2/exercises/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :exercises, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :exercises, ExercisesWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Bandit.PhoenixAdapter, 17 | render_errors: [ 18 | formats: [html: ExercisesWeb.ErrorHTML, json: ExercisesWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: Exercises.PubSub, 22 | live_view: [signing_salt: "aUqR6kE2"] 23 | 24 | # Configures the mailer 25 | # 26 | # By default it uses the "Local" adapter which stores the emails 27 | # locally. You can see the emails in your browser, at "/dev/mailbox". 28 | # 29 | # For production it's recommended to configure a different adapter 30 | # at the `config/runtime.exs`. 31 | config :exercises, Exercises.Mailer, adapter: Swoosh.Adapters.Local 32 | 33 | # Configure esbuild (the version is required) 34 | config :esbuild, 35 | version: "0.17.11", 36 | exercises: [ 37 | args: 38 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 39 | cd: Path.expand("../assets", __DIR__), 40 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 41 | ] 42 | 43 | # Configure tailwind (the version is required) 44 | config :tailwind, 45 | version: "3.4.0", 46 | exercises: [ 47 | args: ~w( 48 | --config=tailwind.config.js 49 | --input=css/app.css 50 | --output=../priv/static/assets/app.css 51 | ), 52 | cd: Path.expand("../assets", __DIR__) 53 | ] 54 | 55 | # Configures Elixir's Logger 56 | config :logger, :console, 57 | format: "$time $metadata[$level] $message\n", 58 | metadata: [:request_id] 59 | 60 | # Use Jason for JSON parsing in Phoenix 61 | config :phoenix, :json_library, Jason 62 | 63 | # Import environment specific config. This must remain at the bottom 64 | # of this file so it overrides the configuration defined above. 65 | import_config "#{config_env()}.exs" 66 | -------------------------------------------------------------------------------- /class4/phoenix_hello/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :phoenix_hello, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Bandit.PhoenixAdapter, 17 | render_errors: [ 18 | formats: [html: PhoenixHelloWeb.ErrorHTML, json: PhoenixHelloWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: PhoenixHello.PubSub, 22 | live_view: [signing_salt: "cLNQxQHn"] 23 | 24 | # Configures the mailer 25 | # 26 | # By default it uses the "Local" adapter which stores the emails 27 | # locally. You can see the emails in your browser, at "/dev/mailbox". 28 | # 29 | # For production it's recommended to configure a different adapter 30 | # at the `config/runtime.exs`. 31 | config :phoenix_hello, PhoenixHello.Mailer, adapter: Swoosh.Adapters.Local 32 | 33 | # Configure esbuild (the version is required) 34 | config :esbuild, 35 | version: "0.17.11", 36 | phoenix_hello: [ 37 | args: 38 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 39 | cd: Path.expand("../assets", __DIR__), 40 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 41 | ] 42 | 43 | # Configure tailwind (the version is required) 44 | config :tailwind, 45 | version: "3.4.0", 46 | phoenix_hello: [ 47 | args: ~w( 48 | --config=tailwind.config.js 49 | --input=css/app.css 50 | --output=../priv/static/assets/app.css 51 | ), 52 | cd: Path.expand("../assets", __DIR__) 53 | ] 54 | 55 | # Configures Elixir's Logger 56 | config :logger, :console, 57 | format: "$time $metadata[$level] $message\n", 58 | metadata: [:request_id, :id] 59 | 60 | # Use Jason for JSON parsing in Phoenix 61 | config :phoenix, :json_library, Jason 62 | 63 | # Import environment specific config. This must remain at the bottom 64 | # of this file so it overrides the configuration defined above. 65 | import_config "#{config_env()}.exs" 66 | -------------------------------------------------------------------------------- /class2/processes_DIY.livemd: -------------------------------------------------------------------------------- 1 | # Processes DIY 2 | 3 | ## Echo process 4 | 5 | Write code that will spawn another process, register this process under a given name (eg `:echo`) and make it reply whatever it receivers back to the sender. 6 | 7 | Hint 0 - rememeber to register the echo process first. 8 | 9 | Hint 1 - use recursion for looping. 10 | 11 | Hint 2 - when a message is sent the sender process is unknown to the receiver process, therefore do not forget to include sender's PID in the message. 12 | 13 | ```elixir 14 | defmodule Echo do 15 | def spawn_echo_process do 16 | ## Add spawning code here. 17 | Process.sleep(1000) 18 | end 19 | end 20 | ``` 21 | 22 | ```elixir 23 | spawn(Echo, :spawn_echo_process, []) 24 | 25 | messages = [:first, {:second, :message}, %{"third" => 1}] 26 | 27 | for msg <- messages do 28 | send(:echo, msg) 29 | 30 | receive do 31 | ^msg -> :ok 32 | msg -> {:error, "Unexpected message #{inspect(msg)}"} 33 | after 34 | 100 -> :timeout 35 | end 36 | end 37 | ``` 38 | 39 | ## Counter process 40 | 41 | Implement a process that will work as a counter. It allows for incremenitng 1, incremating and decrementing by any numer or return current value. 42 | 43 | Hint 0 - use recursion for looping. 44 | 45 | Hint 1 - use recursive function's argument to store the state. 46 | 47 | Hint 2 - use asyncronious comunication to update counter and syncronious communication to retrive current counter's value. 48 | 49 | Hint 3 - think about possible race condition that may occure in your system. 50 | 51 | ```elixir 52 | 53 | ``` 54 | 55 | ## Warden 56 | 57 | Implement a process that will start and watch counter process, if the counter crushes it the warden will restart it. With an inintial value. 58 | 59 | Hint 1 - use monitor or link with setting process flag `trap_exit`. 60 | 61 | ```elixir 62 | 63 | ``` 64 | 65 | ## The ring 66 | 67 | One Ring to rule them all, 68 | One Ring to find them, 69 | One Ring to bring them all and in the darkness bind them. 70 | 71 | Implement a ring of n (where n is positive integer > 2) processes, 72 | The 1st process is forwarding a message to the second one, 73 | the second one to the 3ed one etc 74 | 75 | ```mermaid 76 | graph LR; 77 | 1 --> 2; 78 | 2 --> 3; 79 | 3 --> ...; 80 | ... --> n; 81 | n --> 1; 82 | 83 | ``` 84 | 85 | ```elixir 86 | 87 | ``` 88 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | summary("phoenix.channel_joined.duration", 47 | unit: {:native, :millisecond} 48 | ), 49 | summary("phoenix.channel_handled_in.duration", 50 | tags: [:event], 51 | unit: {:native, :millisecond} 52 | ), 53 | 54 | # VM Metrics 55 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 56 | summary("vm.total_run_queue_lengths.total"), 57 | summary("vm.total_run_queue_lengths.cpu"), 58 | summary("vm.total_run_queue_lengths.io") 59 | ] 60 | end 61 | 62 | defp periodic_measurements do 63 | [ 64 | # A module, function and arguments to be invoked periodically. 65 | # This function must call :telemetry.execute/3 and a metric must be added above. 66 | # {ExercisesWeb, :count_users, []} 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello/manager_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.ManagerSupervisor do 2 | @moduledoc """ 3 | A distributed supervisor that dynamically adds and monitors PIDs across the cluster. 4 | """ 5 | use Horde.DynamicSupervisor 6 | 7 | alias PhoenixHello.Manager 8 | 9 | require Logger 10 | 11 | def start_link(args) do 12 | Horde.DynamicSupervisor.start_link(__MODULE__, args, name: __MODULE__) 13 | end 14 | 15 | @spec start_manager(key :: String.t()) :: {:ok, pid()} | {:error, nil} 16 | def start_manager(nil) do 17 | {:error, nil} 18 | end 19 | 20 | def start_manager(key) do 21 | spec = {Manager, key} 22 | 23 | case lookup(key) do 24 | nil -> 25 | case Horde.DynamicSupervisor.start_child(__MODULE__, spec) do 26 | {:ok, _pid} = resp -> 27 | Logger.info("Successfully created Manager", id: key) 28 | resp 29 | 30 | {:error, {:already_started, pid}} -> 31 | Logger.info("Manager already started", id: key) 32 | {:ok, pid} 33 | end 34 | 35 | pid -> 36 | Logger.info("Manager already started", id: key) 37 | {:ok, pid} 38 | end 39 | end 40 | 41 | @doc """ 42 | Stops Manager 43 | """ 44 | @spec stop_manager(child_pid :: pid()) :: :ok 45 | def stop_manager(child_pid) do 46 | Horde.DynamicSupervisor.terminate_child(__MODULE__, child_pid) 47 | end 48 | 49 | def start_random(num) do 50 | Enum.map(1..num, fn _ -> 51 | name = Base.encode16(:crypto.strong_rand_bytes(8)) 52 | start_manager(name) 53 | end) 54 | end 55 | 56 | def start_random() do 57 | names = [ 58 | "Ethan", 59 | "Olivia", 60 | "Mason", 61 | "Sophia", 62 | "Liam", 63 | "Ava", 64 | "Noah", 65 | "Isabella", 66 | "Lucas", 67 | "Mia" 68 | ] 69 | 70 | Enum.map(names, fn name -> 71 | # TODO exercise8 start it under distributed supervisor 72 | PhoenixHello.Manager.start_link(name) 73 | name 74 | end) 75 | end 76 | 77 | def lookup(key) do 78 | case Horde.Registry.lookup(PhoenixHello.DistributedRegistry, key) do 79 | [{pid, _}] -> 80 | pid 81 | 82 | [] -> 83 | nil 84 | end 85 | end 86 | 87 | @impl Horde.DynamicSupervisor 88 | def init(args) do 89 | Horde.DynamicSupervisor.init(args) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | summary("phoenix.channel_joined.duration", 47 | unit: {:native, :millisecond} 48 | ), 49 | summary("phoenix.channel_handled_in.duration", 50 | tags: [:event], 51 | unit: {:native, :millisecond} 52 | ), 53 | 54 | # VM Metrics 55 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 56 | summary("vm.total_run_queue_lengths.total"), 57 | summary("vm.total_run_queue_lengths.cpu"), 58 | summary("vm.total_run_queue_lengths.io") 59 | ] 60 | end 61 | 62 | defp periodic_measurements do 63 | [ 64 | # A module, function and arguments to be invoked periodically. 65 | # This function must call :telemetry.execute/3 and a metric must be added above. 66 | # {PhoenixHelloWeb, :count_users, []} 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /class2/exercises/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Exercises.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :exercises, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {Exercises.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.11"}, 36 | {:phoenix_html, "~> 4.0"}, 37 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 38 | {:phoenix_live_view, "~> 1.0.0"}, 39 | {:floki, ">= 0.30.0", only: :test}, 40 | {:phoenix_live_dashboard, "~> 0.8.3"}, 41 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 42 | {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, 43 | {:heroicons, 44 | github: "tailwindlabs/heroicons", 45 | tag: "v2.1.1", 46 | sparse: "optimized", 47 | app: false, 48 | compile: false, 49 | depth: 1}, 50 | {:swoosh, "~> 1.5"}, 51 | {:finch, "~> 0.13"}, 52 | {:telemetry_metrics, "~> 1.0"}, 53 | {:telemetry_poller, "~> 1.0"}, 54 | {:gettext, "~> 0.20"}, 55 | {:jason, "~> 1.2"}, 56 | {:dns_cluster, "~> 0.2.0"}, 57 | {:bandit, "~> 1.2"} 58 | ] 59 | end 60 | 61 | # Aliases are shortcuts or tasks specific to the current project. 62 | # For example, to install project dependencies and perform other setup tasks, run: 63 | # 64 | # $ mix setup 65 | # 66 | # See the documentation for `Mix` for more info on aliases. 67 | defp aliases do 68 | [ 69 | setup: ["deps.get", "assets.setup", "assets.build"], 70 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], 71 | "assets.build": ["tailwind exercises", "esbuild exercises"], 72 | "assets.deploy": [ 73 | "tailwind exercises --minify", 74 | "esbuild exercises --minify", 75 | "phx.digest" 76 | ] 77 | ] 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /class1/exercises.livemd: -------------------------------------------------------------------------------- 1 | # Exercises 2 | 3 | ## Modules and Functions - Sum 4 | 5 | Write functions `sum(n)` that uses recursion to calculate the sum of integers from 1 to n. 6 | 7 | ```elixir 8 | defmodule Recursive do 9 | def sum(_n) do 10 | # Your code here 11 | end 12 | end 13 | ``` 14 | 15 | ```elixir 16 | 1 = Recursive.sum(1) 17 | 6 = Recursive.sum(3) 18 | 10 = Recursive.sum(4) 19 | ``` 20 | 21 | ## FizzBuzz 22 | 23 | Write a function that returns the numbers from 1 to `n`. But for multiples of: 24 | 25 | * three replaces a number with an atom `:fizz` instead of the number 26 | * five replaces a number with an atom `:buzz` 27 | * both three and five replaces a number with an atom `:fizzbuzz`. 28 | 29 | Tip: see [How to use div/2 and/or rem/2?](https://elixir-lang.org/getting-started/basic-types.html#basic-arithmetic) 30 | 31 | ```elixir 32 | defmodule FizzBuzz do 33 | def fizzbuzz(_n) do 34 | # Your code here 35 | end 36 | end 37 | ``` 38 | 39 | ```elixir 40 | [] = FizzBuzz.fizzbuzz(0) 41 | [1] = FizzBuzz.fizzbuzz(1) 42 | [1, 2] = FizzBuzz.fizzbuzz(2) 43 | [1, 2, :fizz] = FizzBuzz.fizzbuzz(3) 44 | 45 | [ 46 | 1, 47 | 2, 48 | :fizz, 49 | 4, 50 | :buzz, 51 | :fizz, 52 | 7, 53 | 8, 54 | :fizz, 55 | :buzz, 56 | 11, 57 | :fizz, 58 | 13, 59 | 14, 60 | :fizzbuzz, 61 | 16, 62 | 17, 63 | :fizz, 64 | 19, 65 | :buzz, 66 | :fizz, 67 | 22, 68 | 23, 69 | :fizz, 70 | :buzz, 71 | 26, 72 | :fizz, 73 | 28, 74 | 29, 75 | :fizzbuzz, 76 | 31 77 | ] = FizzBuzz.fizzbuzz(31) 78 | ``` 79 | 80 | ## Scrabble Score 81 | 82 | Given a word, compute the Scrabble score of that word. 83 | 84 | ### Letter Values 85 | 86 | | Letter | Value | 87 | | ---------------------------- | ----- | 88 | | A, E, I, O, U, L, N, R, S, T | 1 | 89 | | D, G | 2 | 90 | | B, C, M, P | 3 | 91 | | F, H, V, W, Y | 4 | 92 | | K | 5 | 93 | | J, X | 8 | 94 | | Q, Z | 10 | 95 | 96 | Tip: You may find [String.graphemes/1](https://hexdocs.pm/elixir/String.html#graphemes/1) useful. 97 | 98 | ```elixir 99 | defmodule Scrabble do 100 | def calculate_score(_word) do 101 | # Your code here 102 | end 103 | end 104 | ``` 105 | 106 | ```elixir 107 | 13 = Scrabble.calculate_score("elixir") 108 | 2 = Scrabble.calculate_score("is") 109 | 6 = Scrabble.calculate_score("fun") 110 | ``` 111 | -------------------------------------------------------------------------------- /class4/phoenix_hello/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHello.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phoenix_hello, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {PhoenixHello.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.12"}, 36 | {:phoenix_html, "~> 4.0"}, 37 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 38 | {:phoenix_live_view, "~> 1.0.0"}, 39 | {:floki, ">= 0.30.0", only: :test}, 40 | {:phoenix_live_dashboard, "~> 0.8.3"}, 41 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 42 | {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, 43 | {:heroicons, 44 | github: "tailwindlabs/heroicons", 45 | tag: "v2.1.1", 46 | sparse: "optimized", 47 | app: false, 48 | compile: false, 49 | depth: 1}, 50 | {:swoosh, "~> 1.5"}, 51 | {:finch, "~> 0.13"}, 52 | {:telemetry_metrics, "~> 1.0"}, 53 | {:telemetry_poller, "~> 1.0"}, 54 | {:gettext, "~> 0.20"}, 55 | {:jason, "~> 1.2"}, 56 | {:dns_cluster, "~> 0.2.0"}, 57 | {:bandit, "~> 1.2"}, 58 | {:bumblebee, "~> 0.6.0"}, 59 | {:horde, "~> 0.9.0"}, 60 | {:libcluster, "~> 3.3"} 61 | ] 62 | end 63 | 64 | # Aliases are shortcuts or tasks specific to the current project. 65 | # For example, to install project dependencies and perform other setup tasks, run: 66 | # 67 | # $ mix setup 68 | # 69 | # See the documentation for `Mix` for more info on aliases. 70 | defp aliases do 71 | [ 72 | setup: ["deps.get", "assets.setup", "assets.build"], 73 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], 74 | "assets.build": ["tailwind phoenix_hello", "esbuild phoenix_hello"], 75 | "assets.deploy": [ 76 | "tailwind phoenix_hello --minify", 77 | "esbuild phoenix_hello --minify", 78 | "phx.digest" 79 | ] 80 | ] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /class2/exercises/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we can use it 8 | # to bundle .js and .css sources. 9 | config :exercises, ExercisesWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "CPATyXsWYTiUChQAZaa+TrLbyf4fS6EYfhRmkOsQbICDkYMrl0ZvwhgYdd4hDPKm", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:exercises, ~w(--sourcemap=inline --watch)]}, 19 | tailwind: {Tailwind, :install_and_run, [:exercises, ~w(--watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Run `mix help phx.gen.cert` for more information. 31 | # 32 | # The `http:` config above can be replaced with: 33 | # 34 | # https: [ 35 | # port: 4001, 36 | # cipher_suite: :strong, 37 | # keyfile: "priv/cert/selfsigned_key.pem", 38 | # certfile: "priv/cert/selfsigned.pem" 39 | # ], 40 | # 41 | # If desired, both `http:` and `https:` keys can be 42 | # configured to run both http and https servers on 43 | # different ports. 44 | 45 | # Watch static and templates for browser reloading. 46 | config :exercises, ExercisesWeb.Endpoint, 47 | live_reload: [ 48 | patterns: [ 49 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 50 | ~r"priv/gettext/.*(po)$", 51 | ~r"lib/exercises_web/(controllers|live|components)/.*(ex|heex)$" 52 | ] 53 | ] 54 | 55 | # Enable dev routes for dashboard and mailbox 56 | config :exercises, dev_routes: true 57 | 58 | # Do not include metadata nor timestamps in development logs 59 | config :logger, :console, format: "[$level] $message\n" 60 | 61 | # Set a higher stacktrace during development. Avoid configuring such 62 | # in production as building large stacktraces may be expensive. 63 | config :phoenix, :stacktrace_depth, 20 64 | 65 | # Initialize plugs at runtime for faster development compilation 66 | config :phoenix, :plug_init_mode, :runtime 67 | 68 | # Include HEEx debug annotations as HTML comments in rendered markup 69 | config :phoenix_live_view, :debug_heex_annotations, true 70 | 71 | # Disable swoosh api client as it is only required for production adapters. 72 | config :swoosh, :api_client, false 73 | -------------------------------------------------------------------------------- /class2/exercises/assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | const fs = require("fs") 6 | const path = require("path") 7 | 8 | module.exports = { 9 | content: [ 10 | "./js/**/*.js", 11 | "../lib/exercises_web.ex", 12 | "../lib/exercises_web/**/*.*ex" 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | brand: "#FD4F00", 18 | } 19 | }, 20 | }, 21 | plugins: [ 22 | require("@tailwindcss/forms"), 23 | // Allows prefixing tailwind classes with LiveView classes to add rules 24 | // only when LiveView classes are applied, for example: 25 | // 26 | //
27 | // 28 | plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), 29 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 30 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 31 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), 32 | 33 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle 34 | // See your `CoreComponents.icon/1` for more information. 35 | // 36 | plugin(function({matchComponents, theme}) { 37 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") 38 | let values = {} 39 | let icons = [ 40 | ["", "/24/outline"], 41 | ["-solid", "/24/solid"], 42 | ["-mini", "/20/solid"], 43 | ["-micro", "/16/solid"] 44 | ] 45 | icons.forEach(([suffix, dir]) => { 46 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { 47 | let name = path.basename(file, ".svg") + suffix 48 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)} 49 | }) 50 | }) 51 | matchComponents({ 52 | "hero": ({name, fullPath}) => { 53 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") 54 | let size = theme("spacing.6") 55 | if (name.endsWith("-mini")) { 56 | size = theme("spacing.5") 57 | } else if (name.endsWith("-micro")) { 58 | size = theme("spacing.4") 59 | } 60 | return { 61 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, 62 | "-webkit-mask": `var(--hero-${name})`, 63 | "mask": `var(--hero-${name})`, 64 | "mask-repeat": "no-repeat", 65 | "background-color": "currentColor", 66 | "vertical-align": "middle", 67 | "display": "inline-block", 68 | "width": size, 69 | "height": size 70 | } 71 | } 72 | }, {values}) 73 | }) 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /class4/phoenix_hello/assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | const fs = require("fs") 6 | const path = require("path") 7 | 8 | module.exports = { 9 | content: [ 10 | "./js/**/*.js", 11 | "../lib/phoenix_hello_web.ex", 12 | "../lib/phoenix_hello_web/**/*.*ex" 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | brand: "#FD4F00", 18 | } 19 | }, 20 | }, 21 | plugins: [ 22 | require("@tailwindcss/forms"), 23 | // Allows prefixing tailwind classes with LiveView classes to add rules 24 | // only when LiveView classes are applied, for example: 25 | // 26 | //
27 | // 28 | plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), 29 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 30 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 31 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), 32 | 33 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle 34 | // See your `CoreComponents.icon/1` for more information. 35 | // 36 | plugin(function({matchComponents, theme}) { 37 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") 38 | let values = {} 39 | let icons = [ 40 | ["", "/24/outline"], 41 | ["-solid", "/24/solid"], 42 | ["-mini", "/20/solid"], 43 | ["-micro", "/16/solid"] 44 | ] 45 | icons.forEach(([suffix, dir]) => { 46 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { 47 | let name = path.basename(file, ".svg") + suffix 48 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)} 49 | }) 50 | }) 51 | matchComponents({ 52 | "hero": ({name, fullPath}) => { 53 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") 54 | let size = theme("spacing.6") 55 | if (name.endsWith("-mini")) { 56 | size = theme("spacing.5") 57 | } else if (name.endsWith("-micro")) { 58 | size = theme("spacing.4") 59 | } 60 | return { 61 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, 62 | "-webkit-mask": `var(--hero-${name})`, 63 | "mask": `var(--hero-${name})`, 64 | "mask-repeat": "no-repeat", 65 | "background-color": "currentColor", 66 | "vertical-align": "middle", 67 | "display": "inline-block", 68 | "width": size, 69 | "height": size 70 | } 71 | } 72 | }, {values}) 73 | }) 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /class2/exercises/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /class4/phoenix_hello/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | port = String.to_integer(System.get_env("PORT") || "4000") 7 | # The watchers configuration can be used to run external 8 | # watchers to your application. For example, we can use it 9 | # to bundle .js and .css sources. 10 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 11 | # Binding to loopback ipv4 address prevents access from other machines. 12 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 13 | http: [ip: {127, 0, 0, 1}, port: port], 14 | check_origin: false, 15 | code_reloader: true, 16 | debug_errors: true, 17 | secret_key_base: "pivkKiwtVPq4gGgKq97y1gHfHtGqk3ZaTkJVQEO5pamm+nQWGOP1XzTKkFD/mouA", 18 | watchers: [ 19 | esbuild: {Esbuild, :install_and_run, [:phoenix_hello, ~w(--sourcemap=inline --watch)]}, 20 | tailwind: {Tailwind, :install_and_run, [:phoenix_hello, ~w(--watch)]} 21 | ] 22 | 23 | # ## SSL Support 24 | # 25 | # In order to use HTTPS in development, a self-signed 26 | # certificate can be generated by running the following 27 | # Mix task: 28 | # 29 | # mix phx.gen.cert 30 | # 31 | # Run `mix help phx.gen.cert` for more information. 32 | # 33 | # The `http:` config above can be replaced with: 34 | # 35 | # https: [ 36 | # port: 4001, 37 | # cipher_suite: :strong, 38 | # keyfile: "priv/cert/selfsigned_key.pem", 39 | # certfile: "priv/cert/selfsigned.pem" 40 | # ], 41 | # 42 | # If desired, both `http:` and `https:` keys can be 43 | # configured to run both http and https servers on 44 | # different ports. 45 | 46 | # Watch static and templates for browser reloading. 47 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 48 | live_reload: [ 49 | patterns: [ 50 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 51 | ~r"priv/gettext/.*(po)$", 52 | ~r"lib/phoenix_hello_web/(controllers|live|components)/.*(ex|heex)$" 53 | ] 54 | ] 55 | 56 | # Enable dev routes for dashboard and mailbox 57 | config :phoenix_hello, dev_routes: true 58 | 59 | # Do not include metadata nor timestamps in development logs 60 | # config :logger, :console, format: "[$level] $message\n" 61 | 62 | config :logger, :console, 63 | format: "$metadata[$level] $message\n", 64 | metadata: [:request_id, :id] 65 | 66 | # Set a higher stacktrace during development. Avoid configuring such 67 | # in production as building large stacktraces may be expensive. 68 | config :phoenix, :stacktrace_depth, 20 69 | 70 | # Initialize plugs at runtime for faster development compilation 71 | config :phoenix, :plug_init_mode, :runtime 72 | 73 | config :phoenix_live_view, 74 | # Include HEEx debug annotations as HTML comments in rendered markup 75 | debug_heex_annotations: true, 76 | # Enable helpful, but potentially expensive runtime checks 77 | enable_expensive_runtime_checks: true 78 | 79 | # Disable swoosh api client as it is only required for production adapters. 80 | config :swoosh, :api_client, false 81 | -------------------------------------------------------------------------------- /class4/phoenix_hello/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /class2/exercises/lib/exercises_web.ex: -------------------------------------------------------------------------------- 1 | defmodule ExercisesWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use ExercisesWeb, :controller 9 | use ExercisesWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: ExercisesWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import ExercisesWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {ExercisesWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | end 59 | end 60 | 61 | def live_component do 62 | quote do 63 | use Phoenix.LiveComponent 64 | 65 | unquote(html_helpers()) 66 | end 67 | end 68 | 69 | def html do 70 | quote do 71 | use Phoenix.Component 72 | 73 | # Import convenience functions from controllers 74 | import Phoenix.Controller, 75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 76 | 77 | # Include general helpers for rendering HTML 78 | unquote(html_helpers()) 79 | end 80 | end 81 | 82 | defp html_helpers do 83 | quote do 84 | # HTML escaping functionality 85 | import Phoenix.HTML 86 | # Core UI components and translation 87 | import ExercisesWeb.CoreComponents 88 | import ExercisesWeb.Gettext 89 | 90 | # Shortcut for generating JS commands 91 | alias Phoenix.LiveView.JS 92 | 93 | # Routes generation with the ~p sigil 94 | unquote(verified_routes()) 95 | end 96 | end 97 | 98 | def verified_routes do 99 | quote do 100 | use Phoenix.VerifiedRoutes, 101 | endpoint: ExercisesWeb.Endpoint, 102 | router: ExercisesWeb.Router, 103 | statics: ExercisesWeb.static_paths() 104 | end 105 | end 106 | 107 | @doc """ 108 | When used, dispatch to the appropriate controller/view/etc. 109 | """ 110 | defmacro __using__(which) when is_atom(which) do 111 | apply(__MODULE__, which, []) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /class4/phoenix_hello/lib/phoenix_hello_web.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHelloWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PhoenixHelloWeb, :controller 9 | use PhoenixHelloWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: PhoenixHelloWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import PhoenixHelloWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {PhoenixHelloWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | end 59 | end 60 | 61 | def live_component do 62 | quote do 63 | use Phoenix.LiveComponent 64 | 65 | unquote(html_helpers()) 66 | end 67 | end 68 | 69 | def html do 70 | quote do 71 | use Phoenix.Component 72 | 73 | # Import convenience functions from controllers 74 | import Phoenix.Controller, 75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 76 | 77 | # Include general helpers for rendering HTML 78 | unquote(html_helpers()) 79 | end 80 | end 81 | 82 | defp html_helpers do 83 | quote do 84 | # HTML escaping functionality 85 | import Phoenix.HTML 86 | # Core UI components and translation 87 | import PhoenixHelloWeb.CoreComponents 88 | import PhoenixHelloWeb.Gettext 89 | 90 | # Shortcut for generating JS commands 91 | alias Phoenix.LiveView.JS 92 | 93 | # Routes generation with the ~p sigil 94 | unquote(verified_routes()) 95 | end 96 | end 97 | 98 | def verified_routes do 99 | quote do 100 | use Phoenix.VerifiedRoutes, 101 | endpoint: PhoenixHelloWeb.Endpoint, 102 | router: PhoenixHelloWeb.Router, 103 | statics: PhoenixHelloWeb.static_paths() 104 | end 105 | end 106 | 107 | @doc """ 108 | When used, dispatch to the appropriate controller/live_view/etc. 109 | """ 110 | defmacro __using__(which) when is_atom(which) do 111 | apply(__MODULE__, which, []) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /class2/exercises/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/exercises start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :exercises, ExercisesWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :exercises, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :exercises, ExercisesWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :exercises, ExercisesWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your config/prod.exs, 78 | # ensuring no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :exercises, ExercisesWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | 85 | # ## Configuring the mailer 86 | # 87 | # In production you need to configure the mailer to use a different adapter. 88 | # Also, you may need to configure the Swoosh API client of your choice if you 89 | # are not using SMTP. Here is an example of the configuration: 90 | # 91 | # config :exercises, Exercises.Mailer, 92 | # adapter: Swoosh.Adapters.Mailgun, 93 | # api_key: System.get_env("MAILGUN_API_KEY"), 94 | # domain: System.get_env("MAILGUN_DOMAIN") 95 | # 96 | # For this example you need include a HTTP client required by Swoosh API client. 97 | # Swoosh supports Hackney and Finch out of the box: 98 | # 99 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 100 | # 101 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 102 | end 103 | -------------------------------------------------------------------------------- /class4/phoenix_hello/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/phoenix_hello start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :phoenix_hello, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :phoenix_hello, PhoenixHelloWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :phoenix_hello, PhoenixHelloWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your config/prod.exs, 78 | # ensuring no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :phoenix_hello, PhoenixHelloWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | 85 | # ## Configuring the mailer 86 | # 87 | # In production you need to configure the mailer to use a different adapter. 88 | # Also, you may need to configure the Swoosh API client of your choice if you 89 | # are not using SMTP. Here is an example of the configuration: 90 | # 91 | # config :phoenix_hello, PhoenixHello.Mailer, 92 | # adapter: Swoosh.Adapters.Mailgun, 93 | # api_key: System.get_env("MAILGUN_API_KEY"), 94 | # domain: System.get_env("MAILGUN_DOMAIN") 95 | # 96 | # For this example you need include a HTTP client required by Swoosh API client. 97 | # Swoosh supports Hackney and Finch out of the box: 98 | # 99 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 100 | # 101 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 102 | end 103 | -------------------------------------------------------------------------------- /class3/otp_additional.livemd: -------------------------------------------------------------------------------- 1 | # GenServer, Supervisors DYI 2 | 3 | ## GenServer callbacks 4 | 5 | * handle_call/3 - synchronous, receives messages via GenServer.call/3 6 | * handle_cast/2 - asynchronous, receives messages via GenServer.cast/2 7 | * handle_info/2 - receives messages from within the system, e.g. from itself 8 | 9 | ```elixir 10 | defmodule Periodically do 11 | use GenServer 12 | 13 | def start_link(_) do 14 | GenServer.start_link(__MODULE__, %{}) 15 | end 16 | 17 | @impl true 18 | def init(state) do 19 | # Schedule work to be performed on start 20 | schedule_periodic_work(3_000) 21 | 22 | {:ok, state} 23 | end 24 | 25 | @impl true 26 | def handle_info(:work, state) do 27 | # Do the desired work here 28 | IO.inspect("In handle info") 29 | # Reschedule once more 30 | schedule_periodic_work(5_000) 31 | 32 | {:noreply, state} 33 | end 34 | 35 | defp schedule_periodic_work(period) do 36 | DateTime.utc_now() |> IO.inspect(label: :IN_SCHEDULE_PERIODIC_WORK) 37 | Process.send_after(self(), :work, period) 38 | end 39 | end 40 | ``` 41 | 42 | ```elixir 43 | GenServer.start_link(Periodically, [], name: Periodically) 44 | ``` 45 | 46 | ```elixir 47 | GenServer.stop(Periodically) 48 | ``` 49 | 50 | ## GenServer message processing 51 | 52 | * each GenServer has it's own mailbox 53 | * messages in mailbox are processed in order 54 | * large number of messages or long time of processing? 55 | 56 | Delegate job to another process!!! 57 | 58 | ```elixir 59 | defmodule Stack do 60 | use GenServer 61 | 62 | # Callbacks 63 | 64 | @impl true 65 | def init(stack) do 66 | {:ok, stack} 67 | end 68 | 69 | @impl true 70 | def handle_call(:pop, _from, [head | tail]) do 71 | {:reply, head, tail} 72 | end 73 | 74 | def handle_call(:reply_in_one_second, from, state) do 75 | spawn(fn -> 76 | Process.sleep(1000) 77 | GenServer.reply(from, :replied_after_one_second) 78 | end) 79 | 80 | {:noreply, state} 81 | end 82 | 83 | @impl true 84 | def handle_cast({:push, element}, state) do 85 | {:noreply, [element | state]} 86 | end 87 | end 88 | ``` 89 | 90 | ```elixir 91 | GenServer.start_link(Stack, [:initial], name: Stack) 92 | ``` 93 | 94 | ```elixir 95 | spawn(fn -> 96 | for i <- 1..25 do 97 | GenServer.cast(Stack, {:push, i}) 98 | GenServer.call(Stack, :pop) |> IO.inspect() 99 | Process.sleep(50) 100 | end 101 | end) 102 | 103 | GenServer.call(Stack, :reply_in_one_second) |> IO.inspect() 104 | GenServer.cast(Stack, {:push, :new_value}) 105 | ``` 106 | 107 | ## Supervisor configuration 108 | 109 | ### Max restarts 110 | 111 | `:max_restarts` - maximum number of child restarts allowed in a time frame, default: 3 112 | 113 | ### Max seconds 114 | 115 | `:max_seconds` - the time frame in which `:max_restarts` applies, default: 5 116 | 117 | ### Name 118 | 119 | `:name` - a name to register the supervisor process 120 | 121 | ### Strategies 122 | 123 | `:strategy` - the supervision strategy, how to manage crashed children processes 124 | 125 | * `:one_for_one` - if a child process terminates, only that process is restarted 126 | * `one_for_all` - if a child process terminates, all other child processes are terminated and then all child processes are restarted 127 | * `:rest_for_one` - if a child process terminates, the terminated child process and the rest of the children started AFTER IT are terminated and restarted 128 | 129 | ## Supervison start, shutdown, restarts 130 | 131 | ### Supervisor start 132 | 133 | We can distinguish following steps on supervisor start: 134 | 135 | * traverse all child specifications and start each child in defined order 136 | * call (typically) `start_link/1` for each child, function MUST return `{:ok, pid}` 137 | * child process uses `init/1` to start its work. 138 | 139 | ### Supervisor stop 140 | 141 | On supervisor shutdown: 142 | 143 | * traverse each children process in REVERSE order 144 | * send `Process.exit(child_pid, :shutdown)` signal to child 145 | * await for 5s for child exit, if it doesn't happend send `:kill` signal 146 | 147 | ### Child restart strategies 148 | 149 | Child is restarted depending `:restart` strategy, possible values: 150 | 151 | * `:permanent` - child is always restarted 152 | * `:transient` - child proces is restarted only of it terminates abnormally 153 | * `:temporary` - child process is never restarted 154 | 155 | ### Child exit reasons 156 | 157 | * `:normal` - standard exit reason, no restart in transient mode, linked processes do not exit 158 | * `:shutdown` or `{:shutdown, term}`, no restart in transent mode, linked processes exit with same reason unles they're trapping exits 159 | * any other term - exit is logged, restarts in transient mode, linked processes exit with same reason 160 | 161 | **IMPORTANT** 162 | If supervisor reaches maximum restart intensity it will exit with `:shutdown` reason. In this case the supervisor will only be restarted if its child specification was defined with the `:restart` option set to `:permanent` 163 | 164 | ## What happens if supervisor crashes? 165 | 166 | Short answer - it propagetes errors up the supervision tree 167 | 168 | Long answer - if one of the supervisor children crashed and can't be restarted supervisor after reaching `:max_restarts` in `:max_seconds` will crash and send sends signal that it terminated abruptly to its own supervisor, and so on and so on until we reach top of the supervision tree and the whole Application is terminated and tries to restart 169 | 170 | ## More resources 171 | 172 | * [GenServer](https://hexdocs.pm/elixir/GenServer.html) 173 | * [Supervisor](https://hexdocs.pm/elixir/1.16.2/Supervisor.html) 174 | * [Application](https://hexdocs.pm/elixir/Application.html) 175 | -------------------------------------------------------------------------------- /class4/README.md: -------------------------------------------------------------------------------- 1 | ### Dependencies 2 | 3 | - docker 4 | - Elixr & Erlang 5 | - [caddy](https://caddyserver.com/docs/install#static-binaries) 6 | brew install caddy 7 | sudo apt install caddy 8 | choco install caddy 9 | 10 | ### Agenda 11 | 12 | - distributed nodes 13 | - horde 14 | - libcluster 15 | - phoenix live view 16 | 17 | ### Distributed nodes 18 | 19 | To start up distrubuted node we have to set a node name using `-sname/-name`. 20 | Nodes have to have the same cookie value. 21 | 22 | #### Exmaple commands: 23 | 24 | ##### To start a new node: 25 | 26 | ``` 27 | iex --sname node1@localhost 28 | ``` 29 | 30 | and in separate terminal 31 | ``` 32 | iex --sname node2@localhost 33 | ``` 34 | 35 | ##### To connect a node: 36 | 37 | run on node2 38 | ``` 39 | iex(1)> Node.connect(:node1@localhost) 40 | true 41 | ``` 42 | 43 | ##### To send a message to different node: 44 | 45 | ``` 46 | # send({name, node}, msg) 47 | iex(1)> send({PhoenixHello.Receiver, :node1}, "hello") 48 | "hello" 49 | ``` 50 | 51 | ##### To register process globally 52 | ``` 53 | iex(1)> pid=spawn(fn -> receive do msg -> IO.inspect(msg, label: "got msg ###########") end end) 54 | iex(2)> :global.register_name(:hello, pid) 55 | :yes 56 | ``` 57 | 58 | 59 | ##### To send a message to different node: 60 | 61 | ``` 62 | iex(1)> pid=:global.whereis_name(:hello) 63 | iex(2)> send(pid, "msg from node2") 64 | "hello" 65 | ``` 66 | 67 | ##### To list connected nodes: 68 | 69 | ``` 70 | iex(1)> Node.list() 71 | [] 72 | ``` 73 | 74 | ##### To print node name: 75 | 76 | ``` 77 | iex(1)> Node.self() 78 | :nonode@nohost 79 | ``` 80 | 81 | ##### To get node cookie: 82 | 83 | ``` 84 | iex(1)> Node.get_cookie() 85 | :FNAUFTWDKWYLXSUSHDBV 86 | ``` 87 | 88 | ##### Exercise 1 89 | 90 | - cd class4/phoenix_hello 91 | - Run two nodes - `:node1` and `:node2` 92 | - PORT=4005 iex --sname node1@localhost -S mix phx.server 93 | - PORT=4006 iex --sname node2@localhost -S mix phx.server 94 | - Connect those nodes 95 | - Start `Receiver.start_link()` on node1 96 | - Start `Receiver.start_link()` on node2 97 | - Implement `send_msg` function in `phoenix_hello/receiver.ex` file 98 | - Run test `PhoenixHello.Receiver.send_msg("hello")` on both nodes 99 | - explain results 100 | 101 | ##### Exercise 2 102 | 103 | - Run two nodes - `:node1` and `:node2` 104 | - Connect those nodes 105 | - Start `Receiver.start_link()` on node1 106 | - Start `Receiver.start_link()` on node2 107 | - Implement `send_msg_to_all_nodes` function in `phoenix_hello/receiver.ex` file 108 | - explain results 109 | 110 | ##### Exercise 3 111 | 112 | - Register Receiver Genserver [globally](https://hexdocs.pm/elixir/1.18.2/GenServer.html#module-name-registration) 113 | - Run two nodes - `:node1` and `:node2` 114 | - Connect those nodes 115 | - fix functions send_msg & send_msg_to_all_nodes 116 | - hint: use :global.whereis_name 117 | - Start `Receiver.start_link()` on node1 118 | - Start `Receiver.start_link()` on node2 119 | - Run `send_msg` `send_msg_to_all_nodes` 120 | - explain results 121 | 122 | 123 | ##### Exercise 4 124 | 125 | - Run three nodes - `:node1`, `:node2` & `:node3` 126 | - iex --sname node1@localhost 127 | - iex --sname node2@localhost 128 | - iex --sname node3@localhost 129 | - Connect node1 to node2 130 | - check Node.list() 131 | - Connect node3 to node1 132 | - check Node.list() 133 | - explain results 134 | 135 | 136 | ##### Exercise 5 137 | 138 | - Run two nodes with cookies 139 | - iex --cookie 111 --sname node1@localhost 140 | - iex --cookie 222 --sname node2@localhost 141 | - Connect node1 to node2 142 | - check Node.list() 143 | - explain results 144 | 145 | ### Horde 146 | 147 | [Horde](https://github.com/derekkraan/horde) is a library providing distributed registry and supervisor. 148 | Horde ensures that processes will keep working when nodes or connection fail. 149 | 150 | ##### Exercise 6 151 | 152 | - kill nodes 153 | - uncomment Horde.Registry & PhoenixHello.ManagerSupervisor in `phoenix_hello/application.ex` 154 | - Run two nodes - `:node1` and `:node2` 155 | - Connect those nodes 156 | - start 1000 random processes `PhoenixHello.ManagerSupervisor.start_random(1000)` 157 | - what happend & why? 158 | - kill one node 159 | - what happend & why? 160 | - start killed node and connect to cluster 161 | - what happend & why? 162 | 163 | ### Libcluster 164 | 165 | Automatically forming clusters of Erlang nodes 166 | 167 | ##### Exercise 7 168 | 169 | - setup libcluster configuration in `phoenix_hello/application.ex` 170 | - run two nodes and check if there are connected automatically `Node.list()` 171 | 172 | ### Loadbalancing & Horde 173 | 174 | ##### Exercise 8 175 | 176 | 177 | - install [caddy](https://caddyserver.com/docs/install#static-binaries) 178 | - run caddy `caddy run` in `phoenix_hello` dir 179 | - start 2 nodes on ports 4005, 4006 180 | - open https://localhost:8080/hello 181 | - caddy problem: localhost:4005/hello / localhost:4006/hello 182 | - spawn managers 183 | - kill node which your user uses 184 | - check if list of managers disappeard 185 | - fix it using distributed supervisor in `PhoenixHello.ManagerSupervisor` module 186 | 187 | ### minimum number of nodes? 188 | 189 | ### LiveView 190 | 191 | ##### Exercise 9 192 | 193 | - Add CounterLive to Router moduel 194 | - Implement CounterLive by adding a button which increases counter by one and diplay a counter value 195 | 196 | 197 | #### [Last task](https://docs.google.com/forms/d/e/1FAIpQLScC7H7rlYuvaNd-xmnVzba7Kw3Sn56e5Ang0fCXspaUKG6EGw/viewform?usp=sharing) 198 | 199 | #### [Challenge](https://github.com/LKlemens/elixir_challenge) 200 | 201 | 202 | #### Email 203 | 204 | klemens.lukaszczyk@erlang-solutions.com 205 | 206 | #### References 207 | 208 | - [PhoenixLiveView](https://pragprog.com/titles/liveview/programming-phoenix-liveview/) book 209 | - [Elixir in Action 3rd Edition](https://www.amazon.com/Elixir-Action-Third-Sa%C5%A1a-Juric-ebook/dp/B0CVHVWP9M?ref_=ast_author_dp) 210 | 211 | 212 | -------------------------------------------------------------------------------- /class1/data_types.livemd: -------------------------------------------------------------------------------- 1 | # Data Types 2 | 3 | ## Numbers 4 | 5 | Elixir have 2 types to work with numbers: integers and floats. \ 6 | Notes: Integers automatically expand memory they take to accomodate values (e.g. from int's to bigInt's) 7 | 8 | 9 | 10 | ```elixir 11 | # Integer examples 12 | 13 | 1 14 | +2 15 | -3 16 | 1_000_000_000 17 | # binary notation 18 | 0b1010 19 | # octal notation 20 | 0o777 21 | # hexadecimal notation 22 | 0x1F 23 | 24 | 12.345 25 | -2.71 26 | 3.14e-10 27 | 123_456.789_1011e23 28 | ``` 29 | 30 | Arithmetic operations work as in most programming languages (+, -, *, /, div/2, rem/2) 31 | 32 | ```elixir 33 | 1 + 2 34 | ``` 35 | 36 | ```elixir 37 | 3 - 4 38 | ``` 39 | 40 | ```elixir 41 | 5 * 6 42 | ``` 43 | 44 | ```elixir 45 | 2 * 1.3 46 | ``` 47 | 48 | ```elixir 49 | 8 / 2 50 | ``` 51 | 52 | ```elixir 53 | div(8, 2) 54 | ``` 55 | 56 | ```elixir 57 | rem(8, 3) 58 | ``` 59 | 60 | ## Atoms 61 | 62 | An atom is a constant whose value is its own name. Atoms must be composed of Unicode characters such as letters, numbers, underscore, and `@`. If the keyword has a character that does not belong to the category above, such as spaces, you can wrap it in quotes. 63 | 64 | ```elixir 65 | :apple 66 | ``` 67 | 68 | ```elixir 69 | :a_pple 70 | ``` 71 | 72 | ```elixir 73 | :apple_2 74 | ``` 75 | 76 | ```elixir 77 | :localhost@me 78 | ``` 79 | 80 | ```elixir 81 | :"atom with spaces" 82 | ``` 83 | 84 | ```elixir 85 | :apple == :orange 86 | # :apple == :apple 87 | # :apple == :Apple 88 | # is_atom(:apple) 89 | ``` 90 | 91 | ## Booleans 92 | 93 | Booleans are in fact just atoms :true and :false. In Elixir boolean logic there is a concept of truthly values. There is no separare value for nil therefore atom :nil is used. You can type nil for simplicity sake. Boolean operators && and || consider nil to be the same as false and anything else is consifered to be true. Operator ! negates the truthly values. 94 | 95 | ```elixir 96 | true 97 | # true == :true 98 | # true === :true 99 | # !true 100 | # ! :true 101 | ``` 102 | 103 | ```elixir 104 | nil 105 | # ! nil 106 | # ! :whatever 107 | # nil || :something 108 | # true && :something 109 | ``` 110 | 111 | ## Chars 112 | 113 | Chars are stored as their unicode values (integers). 114 | 115 | ```elixir 116 | ?a 117 | ``` 118 | 119 | ```elixir 120 | IO.inspect(?a) 121 | is_integer(?a) 122 | ``` 123 | 124 | ```elixir 125 | ?ł 126 | ``` 127 | 128 | ```elixir 129 | ?a == 97 130 | ``` 131 | 132 | ```elixir 133 | ?a - ?b 134 | ``` 135 | 136 | ## String 137 | 138 | String are delimited by double quotes, and are encoded in UTF-8. They also support string interpolation, escape sequences. Internally represented by contiguous sequences of bytes known as binaries. 139 | 140 | ```elixir 141 | a = :world 142 | "hello #{a}" 143 | ``` 144 | 145 | ```elixir 146 | IO.puts("hello\nworld") 147 | ``` 148 | 149 | ```elixir 150 | is_binary("hello") 151 | ``` 152 | 153 | ```elixir 154 | "hello" <> " " <> "world" 155 | ``` 156 | 157 | ## Lists 158 | 159 | Elixir uses square brackets to specify a list of values. Values can be of any type, and lists can be concatenated or subtracted using the `++` and `--` operators. You can create a list by putting all the values in the brackets or use a recursive definition `[head | tail]` where `tail` is also a list. 160 | 161 | ```elixir 162 | [] 163 | # [1] 164 | # [:asdf, 1, "apple", true] 165 | ``` 166 | 167 | ```elixir 168 | [1, 2, 3] ++ [4, 5, 6] 169 | # [1, 1, 2, 5, 3, 4] -- [4, 1] 170 | ``` 171 | 172 | ```elixir 173 | [1, 2, 3] 174 | # [1 | [2, 3]] 175 | # [1, 2, 3] == [1 | [2, 3]] 176 | # [1 | [2 | [3]]] 177 | # [1 | [2 | [3 | []]]] 178 | ``` 179 | 180 | ```elixir 181 | # built-in functions hd/1 tl/1 182 | hd([:a, :b, :c]) 183 | # tl([:a, :b, :c]) 184 | ``` 185 | 186 | ## Tuples 187 | 188 | Group of elements with constant size. Fast access time than lists 189 | 190 | ```elixir 191 | tuple = {:ok, "hello"} 192 | # tuple_size({:ok, "hello"}) 193 | # elem(tuple, 1) 194 | # put_elem(tuple, 1, "world") 195 | # tuple 196 | ``` 197 | 198 | ## Keyword lists 199 | 200 | List consisting of 2-item tuples where the first element is an atom and the second element can be any value. Keyword list have 3 special characterisitcs: 201 | 202 | * keys must be atoms 203 | * keys are ordered 204 | * keys can be given more then once 205 | 206 | ```elixir 207 | {:trim, true} 208 | # [{:trim, true}] == [trim: true] 209 | # [a: 1, b: 2] == [b: 2, a: 1] 210 | # usual list operations are available 211 | # list = [a: 1, b: 2] 212 | # list ++ [c: 3] 213 | ``` 214 | 215 | ## Maps 216 | 217 | Maps are created using `%{}` syntax, classic key-value store. 218 | 219 | ```elixir 220 | %{} 221 | # Map.new() 222 | 223 | # computer = %{ 224 | # :type => :pc, 225 | # "graphics card" => ["GTX", 760], 226 | # {:processor, "intel"} => [{8, :cores}, {:type, :x86}] 227 | # } 228 | 229 | # computer[:type] 230 | # computer[{:processor, "intel"}] 231 | # computer["graphics card"] 232 | # computer[:not_exixsting_key] 233 | # Map.get(computer, :not_existing_key) 234 | # Map.get(computer, :not_existing_key, "default_value") 235 | 236 | # computer.type 237 | # # computer.not_exixsting_key 238 | # # computer."graphics card" 239 | 240 | # laptop = %{computer | :type => :laptop} 241 | # # laptop = %{computer | :sound => {:serround, 5.1}} 242 | # laptop = Map.put(computer, :sound, {:serround, 5.1}) 243 | 244 | # tablet = %{:type => %{device_type: :tablet, os: :android}} 245 | 246 | # tablet.type.os 247 | # tablet[:type].os 248 | # tablet.type[:os] 249 | # tablet[:type][:os] 250 | ``` 251 | 252 | ## Structs 253 | 254 | Structs are extensions built on top of maps that provide compile-time checks and default values. 255 | 256 | ```elixir 257 | defmodule Device do 258 | defstruct device_type: nil, 259 | os: :linux, 260 | processor: nil 261 | end 262 | ``` 263 | 264 | ```elixir 265 | pc = %Device{ 266 | device_type: :pc 267 | } 268 | 269 | # pc.os 270 | 271 | # tablet = %Device{ 272 | # device_type: :tablet, 273 | # processor: {:intel, :x86}, 274 | # os: :android 275 | # } 276 | ``` 277 | -------------------------------------------------------------------------------- /class2/exercises/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /class4/phoenix_hello/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /class1/flow_control.livemd: -------------------------------------------------------------------------------- 1 | # Flow control 2 | 3 | ## Match operator = 4 | 5 | Matching operator = is used in one of 3 ways: 6 | 7 | * to assign value to variable 8 | * to check if the value matches given pattern, pin operator `^` is useful when you want to check pattern without reassigning variable 9 | * to extract data from more complex structures 10 | 11 | see more [here](https://elixir-lang.org/getting-started/pattern-matching.html). 12 | 13 | ```elixir 14 | x = 1 15 | # x = 2 16 | # x 17 | # ^x = 2 18 | ``` 19 | 20 | ```elixir 21 | {:ok, value} = {:ok, :success} 22 | value 23 | ``` 24 | 25 | ```elixir 26 | list = [1, 2, 3] 27 | # [head | tail] = list 28 | # head 29 | # tail 30 | # [^head, 2, b] = list 31 | # b 32 | # [a, b] = list 33 | # [] = list 34 | ``` 35 | 36 | ```elixir 37 | {:error, %{:reason => reason}} = {:error, %{:reason => "something went wong", :code => 123}} 38 | reason 39 | ``` 40 | 41 | ## If 42 | 43 | ```elixir 44 | if true do 45 | IO.puts("This works!") 46 | end 47 | 48 | # if not true do 49 | # IO.puts("This will never be seen") 50 | # end 51 | 52 | # if not false do 53 | # IO.puts("This will be seen") 54 | # end 55 | 56 | # if nil do 57 | # IO.puts("This won't be seen") 58 | # else 59 | # IO.puts("This will") 60 | # end 61 | ``` 62 | 63 | ## Case 64 | 65 | ```elixir 66 | earnings = 10 67 | 68 | special_value = 12 69 | 70 | case earnings do 71 | 10 -> "you earn average salary" 72 | # pin operator 73 | ^special_value -> "you earn #{special_value}" 74 | salary when salary > 12 -> "you earn #{salary} you are too rich" 75 | _ -> "you are underpaid" 76 | end 77 | ``` 78 | 79 | ## Cond 80 | 81 | `case` is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that does not evaluate to `nil` or `false`. If all of the conditions return nil or false, an error (`CondClauseError`) is raised. For this reason, it may be necessary to add a final condition, equal to `true`, which will always match 82 | 83 | ```elixir 84 | cond do 85 | 2 + 2 == 5 -> 86 | "This will not be true" 87 | 88 | 2 * 2 == 3 -> 89 | "Nor this" 90 | 91 | 1 + 1 == 2 -> 92 | "But this will" 93 | end 94 | ``` 95 | 96 | ```elixir 97 | cond do 98 | 2 + 2 == 5 -> 99 | "This is never true" 100 | 101 | 2 * 2 == 3 -> 102 | "Nor this" 103 | 104 | true -> 105 | "This is always true (equivalent to else)" 106 | end 107 | ``` 108 | 109 | ## Anonymous functions 110 | 111 | An anonymous function has no name. They allow us to store and pass executable code around as it was an integer or string. They are delimited by the keywords `fn` and `end`. To invoke anonymous funtion use sytnax `fun.(args...)` 112 | 113 | ```elixir 114 | add = fn a, b -> a + b end 115 | add.(2, 3) 116 | # add_two = fn a -> add.(a, 2) end 117 | # add_two.(5) 118 | ``` 119 | 120 | ```elixir 121 | # shorthand notation 122 | add2 = &(&1 + &2) 123 | add2.(2, 3) 124 | ``` 125 | 126 | ```elixir 127 | # Pattern meatching in annonimous functions: 128 | count = fn 129 | 1 -> :one 130 | 2 -> :two 131 | _ -> :many 132 | end 133 | 134 | [ 135 | count.(1), 136 | count.(2), 137 | count.(3), 138 | count.(4) 139 | ] 140 | ``` 141 | 142 | ## Pipe operator 143 | 144 | The `|>` symbol used in the snippet above is the pipe operator: it takes the output from the expression on its left side and passes it as the first argument to the function call on its right side. It’s similar to the Unix `|` operator. Its purpose is to highlight the data being transformed by a series of functions. 145 | 146 | ```elixir 147 | add.(add.(1, 2), 3) 148 | # 1 149 | # |> add.(2) 150 | # |> add.(3) 151 | ``` 152 | 153 | ## Higher order functions 154 | 155 | Functions that accept orther functions as arguments. Read more [here](https://serokell.io/blog/elixir-for-beginners#%E2%9E%A1%EF%B8%8F-higher-order-functions) 156 | 157 | ```elixir 158 | devices = [ 159 | %{:os => :mac, :processor => :intel, :type => :laptop}, 160 | %{:os => :mac, :processor => :intel, :type => :phone}, 161 | %{:os => :windows, :processor => :intel, :type => :laptop}, 162 | %{:os => :android, :processor => :intel, :type => :phone}, 163 | %{:os => :windows, :processor => :intel, :type => :phone} 164 | ] 165 | 166 | ## I want to know what type of divices are used 167 | # devices 168 | # |> Enum.map(fn %{:type => type} -> type end) 169 | # |> Enum.uniq() 170 | 171 | ## I want to see the windows running divices 172 | # devices 173 | # |> Enum.filter(fn(%{:os => :windows}) -> true; _ -> false end) 174 | ``` 175 | 176 | ## See also 177 | 178 | * [Comprehensions](https://hexdocs.pm/elixir/comprehensions.html) 179 | * [Recursion](https://hexdocs.pm/elixir/recursion.html) 180 | 181 | ## Modules 182 | 183 | When writing Elixir code we use functions. And we put group of functions into modules. 184 | 185 | There are 2 types of functions that we put inside modules: 186 | 187 | * public - can only be accessed from anywhere in the code, defined with `def` 188 | * private - can only be accessed within module, defined with `defp` 189 | 190 | Function always return last value it evaluated 191 | 192 | ```elixir 193 | defmodule Math.Examples.Fib do 194 | def fib(0), do: 1 195 | def fib(1), do: 1 196 | 197 | def fib(n) do 198 | fib(n - 1) + fib(n - 2) 199 | end 200 | 201 | def seq_fib(max), do: piv_fib(max, 0, 1, 0) 202 | 203 | defp piv_fib(max, counter, current, previous) when counter >= max, do: current 204 | 205 | ## Handling the warining: 206 | ## warning: variable "previous" is unused (if the variable is not meant to be used, prefix it with an underscore) 207 | ## first_classes/basic_constructs.livemd#cell:11: Fibbonaci.Sequence.piv_fib/4 208 | 209 | # defp piv_fib(max, counter, current, _previous) when counter > max, do: current 210 | # defp piv_fib(max, counter, current, _) when counter > max, do: current 211 | 212 | defp piv_fib(max, counter, current, previous) do 213 | piv_fib(max, counter + 1, current + previous, current) 214 | end 215 | end 216 | 217 | defmodule Math.Examples.Add do 218 | def add(a, b) do 219 | a + b 220 | ## When you uncomment `:ok` it will be returned instead of a + b 221 | # :ok 222 | end 223 | end 224 | ``` 225 | 226 | ```elixir 227 | {[ 228 | Math.Examples.Fib.fib(0), 229 | Math.Examples.Fib.fib(1), 230 | Math.Examples.Fib.fib(2), 231 | Math.Examples.Fib.fib(3), 232 | Math.Examples.Fib.fib(4), 233 | Math.Examples.Fib.fib(5) 234 | ], 235 | [ 236 | Math.Examples.Fib.seq_fib(0), 237 | Math.Examples.Fib.seq_fib(1), 238 | Math.Examples.Fib.seq_fib(2), 239 | Math.Examples.Fib.seq_fib(3), 240 | Math.Examples.Fib.seq_fib(4), 241 | Math.Examples.Fib.seq_fib(5) 242 | ]} 243 | ``` 244 | 245 | ```elixir 246 | Math.Examples.Add.add(2, 3) 247 | ``` 248 | -------------------------------------------------------------------------------- /class3/otp.livemd: -------------------------------------------------------------------------------- 1 | # Class 3 - GenServers and Supervisors 2 | 3 | ## GenServer 4 | 5 | A behaviour module for implementing the server of a client-server relation. 6 | 7 | [Read more here](https://hexdocs.pm/elixir/GenServer.html) 8 | 9 | OTP is a set of Erlang libraries, which consists of the Erlang runtime system, a number of ready-to-use components mainly written in Erlang, and a set of design principles for Erlang programs. [Learn more about Erlang and OTP.](https://www.erlang.org/doc/system_architecture_intro/sys_arch_intro.html) 10 | 11 | ```elixir 12 | defmodule Stack do 13 | use GenServer 14 | 15 | # Callbacks 16 | 17 | @impl true 18 | def init(stack) do 19 | {:ok, stack} 20 | end 21 | 22 | @impl true 23 | def handle_call(:pop, _from, [head | tail]) do 24 | {:reply, head, tail} 25 | end 26 | 27 | @impl true 28 | def handle_cast({:push, element}, state) do 29 | {:noreply, [element | state]} 30 | end 31 | end 32 | ``` 33 | 34 | ```elixir 35 | # Start the server 36 | {:ok, pid} = GenServer.start_link(Stack, [:hello]) |> IO.inspect(label: :START_LINK) 37 | 38 | # This is the client 39 | GenServer.call(pid, :pop) |> IO.inspect(label: :POP) 40 | # => :hello 41 | 42 | GenServer.cast(pid, {:push, :world}) |> IO.inspect(label: :PUSH) 43 | # => :ok 44 | 45 | GenServer.call(pid, :pop) |> IO.inspect(label: :POP) 46 | # => :world 47 | ``` 48 | 49 | ```elixir 50 | # GenServer.call(pid, :pop) 51 | # Error 52 | ``` 53 | 54 | ### Exercise 1 55 | 56 | * `class3/myapp/lib/myapp/shop_inventory.ex` 57 | * `class3/myapp/test/exercises/exercise1_test.exs` 58 | * `mix test --only exercise1` 59 | 60 | Fill the implementation of `MyApp.ShopInventory.init/1`, `MyApp.ShopInventory.handle_call/3` and `MyApp.ShopInventory.handle_cast/2` 61 | 62 | * `init` should take a list of `MyApp.Item` structs and pass them to the state of the GenServer. It's up to you what the internal state data structure is. 63 | * `:list_items` should reply with a list of all items in the state 64 | * `:get_item_by_name` should reply with a single item with a given name or `nil` if it is not present 65 | * `:create_item` should add a provided item to the state 66 | * `:delete_item` should delete a provided item from the state 67 | * You shouldn't modify `MyApp.Item` struct as it can break the tests. 68 | 69 | ## GenServer Client/Server APIs 70 | 71 | ```elixir 72 | defmodule Stack2 do 73 | use GenServer 74 | 75 | # Client 76 | 77 | def start_link(initial_stack) when is_list(initial_stack) do 78 | GenServer.start_link(__MODULE__, initial_stack) 79 | end 80 | 81 | def push(pid, element) do 82 | GenServer.cast(pid, {:push, element}) 83 | end 84 | 85 | def pop(pid) do 86 | GenServer.call(pid, :pop) 87 | end 88 | 89 | # Server (callbacks) 90 | 91 | @impl true 92 | def init(stack) do 93 | {:ok, stack} 94 | end 95 | 96 | @impl true 97 | def handle_call(:pop, _from, [head | tail]) do 98 | {:reply, head, tail} 99 | end 100 | 101 | @impl true 102 | def handle_cast({:push, element}, state) do 103 | {:noreply, [element | state]} 104 | end 105 | end 106 | ``` 107 | 108 | ```elixir 109 | {:ok, pid} = Stack2.start_link([]) |> IO.inspect(label: :START_LINK) 110 | 111 | Stack2.push(pid, :hello) |> IO.inspect(label: :PUSH) 112 | Stack2.pop(pid) |> IO.inspect(label: :POP) 113 | ``` 114 | 115 | ### Exercise 2 116 | 117 | * `class3/myapp/lib/myapp/shop_inventory.ex` 118 | * `class3/myapp/test/exercises/exercise2_test.exs` 119 | * `mix test --only exercise2` 120 | 121 | Fill the implementation of `MyApp.ShopInventory.start_link/1`, `MyApp.ShopInventory.create_item/2`, `MyApp.ShopInventory.list_items/1`, `MyApp.ShopInventory.delete_item/2` and `MyApp.ShopInventory.get_item_by_name/2` 122 | 123 | * Use `GenServer.call/cast` to leverage your implementation from Exercise 1 124 | * `start_link` should allow to initialize our GenServer with a list of items. 125 | 126 | ## GenServer Name registration 127 | 128 | ```elixir 129 | defmodule Stack3 do 130 | use GenServer 131 | 132 | # Client 133 | 134 | def start_link(initial_stack) when is_list(initial_stack) do 135 | GenServer.start_link(__MODULE__, initial_stack, name: __MODULE__) 136 | end 137 | 138 | def push(element) do 139 | GenServer.cast(__MODULE__, {:push, element}) 140 | end 141 | 142 | def pop() do 143 | GenServer.call(__MODULE__, :pop) 144 | end 145 | 146 | # Callbacks 147 | 148 | @impl true 149 | def init(stack) do 150 | {:ok, stack} 151 | end 152 | 153 | @impl true 154 | def handle_call(:pop, _from, [head | tail]) do 155 | {:reply, head, tail} 156 | end 157 | 158 | @impl true 159 | def handle_cast({:push, element}, state) do 160 | {:noreply, [element | state]} 161 | end 162 | end 163 | ``` 164 | 165 | ```elixir 166 | Stack3.start_link([:hello]) 167 | 168 | Stack3.pop() |> IO.inspect(label: :POP) 169 | Stack3.push(:world) |> IO.inspect(label: :PUSH) 170 | Stack3.pop() |> IO.inspect(label: :POP) 171 | ``` 172 | 173 | ### Exercise 3 174 | 175 | * `class3/myapp/lib/myapp/shop_inventory.ex` 176 | * `class3/myapp/test/exercises/exercise3_test.exs` 177 | * `mix test --only exercise3` 178 | 179 | Fill the implementation of `MyApp.ShopInventory.start_link/1`, `MyApp.ShopInventory.create_item/1`, `MyApp.ShopInventory.list_items/0`, `MyApp.ShopInventory.delete_item/1` and `MyApp.ShopInventory.get_item_by_name/1` 180 | 181 | * Register your GenServer under the same name as the module, i.e. `MyApp.ShopInventory`. 182 | 183 | ## Supervisor 184 | 185 | * a process which supervises other processes - parent watches over children 186 | * used to build supervision tree 187 | 188 | 189 | 190 | How to start supervisor: 191 | 192 | * define list of children 193 | * call Supervisor.start_link() 194 | 195 | ```elixir 196 | # We need to stop the process we spawned above in order to proceed 197 | GenServer.stop(Stack3) 198 | ``` 199 | 200 | ```elixir 201 | children = [ 202 | # The Stack3 is a child started via Stack3.start_link([:hello]) 203 | %{ 204 | id: Stack3, 205 | start: {Stack3, :start_link, [[:hello]]} 206 | } 207 | ] 208 | 209 | # Now we start the supervisor with the children and a strategy 210 | {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one) 211 | 212 | # After started, we can query the supervisor for information 213 | Supervisor.count_children(pid) 214 | Supervisor.which_children(pid) 215 | # => %{active: 1, specs: 1, supervisors: 0, workers: 1} 216 | ``` 217 | 218 | ```elixir 219 | Stack3.pop() |> IO.inspect(label: :POP) 220 | Stack3.push(:world) |> IO.inspect(label: :PUSH) 221 | Stack3.pop() |> IO.inspect(label: :POP) 222 | ``` 223 | 224 | ```elixir 225 | Stack3.pop() 226 | ``` 227 | 228 | ```elixir 229 | Supervisor.stop(pid) 230 | ``` 231 | 232 | ```elixir 233 | defmodule StackSupervisor do 234 | use Supervisor 235 | 236 | def start_link(init_arg) do 237 | Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) 238 | end 239 | 240 | @impl true 241 | def init(_init_arg) do 242 | children = [ 243 | # GenServer allows us to shorten it 244 | {Stack3, [:hello]} 245 | ] 246 | 247 | Supervisor.init(children, strategy: :one_for_one) 248 | end 249 | end 250 | ``` 251 | 252 | ```elixir 253 | StackSupervisor.start_link(:ok) 254 | Stack3.pop() |> IO.inspect(label: :POP) 255 | Stack3.push(:world) |> IO.inspect(label: :PUSH) 256 | Stack3.pop() |> IO.inspect(label: :POP) 257 | ``` 258 | 259 | ### Exercise 4 260 | 261 | * `class3/myapp/lib/myapp/supervisor.ex` 262 | * `class3/myapp/test/exercises/exercise4_test.exs` 263 | * `mix test --only exercise4` 264 | 265 | Fill the `children` list so the supervisor starts and monitors `MyApp.ShopInventory` GenServer from the previous exercises. 266 | 267 | ## Application 268 | 269 | * A way of packacking software in Erlang/OTP. 270 | * Similar to the concept of libraries but with additional runtime behaviour. 271 | 272 | ```elixir 273 | defmodule SampleApp do 274 | use Application 275 | 276 | def start(_type, _args) do 277 | children = [ 278 | StackSupervisor 279 | ] 280 | 281 | Supervisor.start_link(children, strategy: :one_for_one) 282 | end 283 | end 284 | ``` 285 | 286 | ### Exercise 5 287 | 288 | * `class3/myapp/lib/myapp/application.ex` 289 | * `class3/myapp/test/exercises/exercise5_test.exs` 290 | * `mix test --only exercise5` 291 | 292 | Add your supervisor to the root supervisor in `MyApp.Application` module. 293 | 294 | Note: some of the previous tests will start to fail. 295 | -------------------------------------------------------------------------------- /class2/exercises/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bandit": {:hex, :bandit, "1.6.10", "ec218849b753b84ed52fb7f19f42bec4ab8b1ff3f74806c6525e4b53eafcad60", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5c5e1d155cbdf98610197d2814934ef431e70b5fd4f487b19c341d9b49cc19a2"}, 3 | "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, 4 | "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, 5 | "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, 6 | "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, 7 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 8 | "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, 9 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, 10 | "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, 11 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, 12 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 14 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 15 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 16 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 17 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 18 | "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, 19 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, 20 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"}, 21 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, 22 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.9", "4dc5e535832733df68df22f9de168b11c0c74bca65b27b088a10ac36dfb75d04", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dccb04ec8544340e01608e108f32724458d0ac4b07e551406b3b920c40ba2e5"}, 23 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 24 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 25 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, 26 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 27 | "swoosh": {:hex, :swoosh, "1.18.3", "ca12197550bd7456654179055b1446168cc0f55067f784a3707e0e4462e269f5", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a533daccea84e887a061a919295212b37f4f2c7916436037eb8be7f1265bacba"}, 28 | "tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"}, 29 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 30 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, 31 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, 32 | "thousand_island": {:hex, :thousand_island, "1.3.12", "590ff651a6d2a59ed7eabea398021749bdc664e2da33e0355e6c64e7e1a2ef93", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55d0b1c868b513a7225892b8a8af0234d7c8981a51b0740369f3125f7c99a549"}, 33 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 34 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 35 | } 36 | -------------------------------------------------------------------------------- /class2/processes.livemd: -------------------------------------------------------------------------------- 1 | # Processes in Elixir 2 | 3 | ## Update 4 | 5 | #### git pull 6 | 7 | #### git stash ; git pull - if just pull doesn't work 8 | 9 | #### Pretty please disable AI addons from your editor 10 | 11 | ## Basics recall 12 | 13 | #### Printing on console 14 | 15 | ```elixir 16 | msg = "hello" 17 | ## inspect is kind of `printf` or `console.log` in other languages 18 | IO.inspect(msg) 19 | IO.inspect(msg, label: "this is label added before msg") 20 | ``` 21 | 22 | #### Creating lambda function 23 | 24 | ```elixir 25 | lambda = fn -> 12 end 26 | lambda.() 27 | ``` 28 | 29 | ## Processes 30 | 31 | Erlang and Elixir processes are: 32 | 33 | * lightweight (grow and shrink dynamically) 34 | * with small memory footprint, 35 | * fast to create and terminate, 36 | * the scheduling overhead is low. 37 | 38 | The theory behind a process is well explained in this blog post: https://www.erlang-solutions.com/blog/understanding-processes-for-elixir-developers/ 39 | 40 | ### Staring new process 41 | 42 | Use function `self/0` to determine the PID (process ID) of current process. 43 | 44 | ```elixir 45 | self() 46 | ``` 47 | 48 | Staring a new process can be done using `spawn/1` function. After spawning, new process is completly unrelated to the process was spawned by. There is no parent-child relation in oposition what you can see eg in Linux OS. 49 | 50 | ```elixir 51 | IO.inspect(self(), label: "I am parent process") 52 | execute_fun = fn -> IO.inspect(self(), label: "I am child process") end 53 | spawn(execute_fun) 54 | ``` 55 | 56 | A process terminates as soon as its starting function finishes execution. 57 | 58 | ```elixir 59 | pid = spawn(fn -> 60 | IO.inspect("starting a process") 61 | Process.sleep(1000) 62 | IO.inspect("last line in a process function") 63 | end) 64 | # process is alive 65 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 66 | # waits after function completes its execution 67 | Process.sleep(1200) 68 | # process is terminated 69 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 70 | 71 | ``` 72 | 73 | A process can be killed during calculations by seding a :shutdown signal via Process.exit/2 74 | 75 | ```elixir 76 | pid = spawn(fn -> 77 | IO.inspect("starting a process") 78 | Process.sleep(1000) 79 | # that line won't execute 80 | IO.inspect("last line in a process function") 81 | end) 82 | # process is alive 83 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 84 | # waits after function completes its execution 85 | Process.exit(pid, :shutdown) 86 | # process is terminated 87 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 88 | 89 | ``` 90 | 91 | Get basic info about process using Erlang function `process_info/0`. 92 | 93 | ```elixir 94 | pid = spawn(fn -> Process.sleep(1000) end) 95 | pid |> Process.info() 96 | ``` 97 | 98 | ## Comunication 99 | 100 | We can send messages to a process with `send/2` and receive them with `receive/1`: 101 | `receive` is blocking operation that waits for a message, while `send` returns immediately 102 | 103 | ```elixir 104 | 105 | pid = spawn(fn -> 106 | # receiving message 107 | receive do 108 | msg -> IO.inspect(msg, label: "I have received") 109 | end 110 | end) 111 | 112 | # sending a message to unnamed process 113 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 114 | send(pid, "Hello") 115 | Process.sleep(100) 116 | Process.alive?(pid) |> IO.inspect(label: "process alive?") 117 | 118 | ``` 119 | 120 | #### Quick recall of pattern matching 121 | 122 | ```elixir 123 | msg = {:greetings, "hello"} 124 | # msg = {:question, "how are u?"} 125 | case msg do 126 | {:question, msg} -> msg 127 | {:greetings, msg} -> msg 128 | end 129 | ``` 130 | 131 | ### Comunication cont. 132 | 133 | When selectivly waiting the message may never arrive, therefore timeout part is added to the receive block: 134 | 135 | ```elixir 136 | pid = spawn(fn -> 137 | receive do 138 | {:greetings, content} -> IO.inspect(content, label: "I have received") 139 | after 140 | # timeout is in miliseconds 141 | 3000 -> IO.inspect(:timeout, label: "I have received") 142 | end 143 | end) 144 | 145 | send(pid, {:question, "How are you?"}) 146 | 147 | ``` 148 | 149 | Messages can be pattern-matched, and if a message doesn’t match any pattern, it stays in the process’s message queue. 150 | 151 | ```elixir 152 | pid = spawn(fn -> 153 | receive do 154 | {:greetings, content} -> IO.inspect(content, label: "I have received") 155 | after 156 | 500 -> :ok 157 | end 158 | 159 | ## This will never happen without after clause 160 | IO.inspect("previous line wait's for {:greetings, ...} msg - so the log statement won't be executed.") 161 | end) 162 | 163 | pid |> Process.info(:message_queue_len) |> IO.inspect() 164 | send(pid, {:question, "How are you?"}) 165 | pid |> Process.info(:message_queue_len) |> IO.inspect() 166 | Process.sleep(200) 167 | Process.alive?(pid) |> IO.inspect(label: "is alive?") 168 | Process.sleep(500) 169 | Process.alive?(pid) |> IO.inspect(label: "is alive?") 170 | 171 | ``` 172 | 173 | ### Naming processes 174 | 175 | Processe can be named via `Process.register/2` so that we do not have to remember their PIDs and we can reffer to them from anywhere in BEAM (Erlang VM). 176 | 177 | ```elixir 178 | execute_fun = fn -> 179 | receive do 180 | msg -> IO.inspect(msg, label: "Krzys received") 181 | end 182 | end 183 | 184 | pid = spawn(execute_fun) 185 | Process.register(pid, :krzys) 186 | 187 | # wait a bit for process registration 188 | Process.sleep(300) 189 | 190 | send(:krzys, "Hello") 191 | ``` 192 | 193 | ### Getting a pid of named process 194 | 195 | ```elixir 196 | spawn(fn -> 197 | IO.inspect(self(), label: "self pid") 198 | Process.register(self(), :hello) 199 | Process.sleep(300) 200 | end) 201 | Process.sleep(100) 202 | Process.whereis(:hello) |> IO.inspect(label: ":hello pid got by whereis") 203 | 204 | ``` 205 | 206 | ### Communication cont. 207 | 208 | We can use selective receives to ensure that messages are consumed in particular order: 209 | 210 | ```elixir 211 | pid = spawn(fn -> 212 | receive do 213 | {:greetings, content} -> 214 | IO.inspect(content, label: "I have received") 215 | end 216 | 217 | receive do 218 | {:question, question} -> 219 | IO.inspect(question, label: "I have received") 220 | end 221 | end) 222 | send(pid, {:question, "How are you?"}) 223 | Process.sleep(3000) 224 | send(pid, {:greetings, "Hello"}) 225 | 226 | ``` 227 | 228 | We can match against multiple patterns in a selective receive, but keep in mind that each receive still waits for only one message at a time. 229 | 230 | ```elixir 231 | pid = spawn(fn -> 232 | receive do 233 | {:greetings, content} -> 234 | IO.inspect(content, label: "I have received") 235 | {:question, question} -> 236 | IO.inspect(question, label: "I have received") 237 | end 238 | end) 239 | send(pid, {:question, "How are you?"}) 240 | Process.sleep(3000) 241 | send(pid, {:greetings, "Hello"}) 242 | 243 | ``` 244 | 245 | ### Exercise 1 - 5 246 | 247 | Please go to `ElixirSchool2025/class2/exercises/lib` 248 | and try to solve exercises 1 - 5 249 | 250 | Tu run tests please run `mix test --only test1` command in `ElixirSchool2025/class2/exercises` directory 251 | 252 | ## Monitors 253 | 254 | Monitor is a way of observing how some other process is doing. When process A monitors process B, and process B crushes, process A gets notified by receiving a message: `{:DOWN, ref, :process, pidB, reason}`. Monitors are unidrectional which means that if A monitors B, B does not know it is monitored. 255 | 256 | Process can die in either normal or unnormal way. Normal is when it runs out of code do execute, and unnormal is when is crashses, for example by trying to match `2` to value `1`. 257 | 258 | ```elixir 259 | spawn(fn -> 260 | Process.register(self(), :B) 261 | receive do 262 | :crash -> 1 = 2 263 | :die_normally -> :ok 264 | end 265 | end) 266 | 267 | 268 | # crash scenario 269 | pidA = spawn(fn -> 270 | Process.monitor(:B) 271 | send(:B, :crash) 272 | 273 | receive do 274 | msg -> IO.inspect(msg, label: "A received") 275 | after 276 | 0 -> :ok 277 | end 278 | end) 279 | ``` 280 | 281 | ```elixir 282 | spawn(fn -> 283 | Process.register(self(), :B) 284 | receive do 285 | :crash -> 1 = 2 286 | :die_normally -> :ok 287 | end 288 | end) 289 | 290 | # normal termination scenario 291 | pidA = spawn(fn -> 292 | Process.monitor(:B) 293 | send(:B, :die_normally) 294 | 295 | receive do 296 | msg -> IO.inspect(msg, label: "A received") 297 | after 298 | 0 -> :ok 299 | end 300 | end) 301 | ``` 302 | 303 | ```elixir 304 | ## Cleanup 305 | for proc <- [:A, :B] do 306 | proc 307 | |> Process.whereis() 308 | |> (fn 309 | nil -> :ok 310 | pid -> Process.exit(pid, :kill) 311 | end).() 312 | end 313 | ``` 314 | 315 | ## Links 316 | 317 | When two processes can be linked to each other. If one of the participants of a link terminates, it will send an exit signal to the other participant. The exit signal will contain the exit reason of the terminated participant. Links are bidirectional. 318 | 319 | ```elixir 320 | IO.inspect(self(), label: "I am") 321 | 322 | a = 323 | spawn(fn -> 324 | Process.register(self(), :A) 325 | IO.inspect({:A, self()}, label: "I am") 326 | 327 | receive do 328 | :crash -> 1 = 2 329 | :die_normally -> :ok 330 | end 331 | end) 332 | 333 | Process.sleep(100) 334 | 335 | b = 336 | spawn(fn -> 337 | Process.register(self(), :B) 338 | IO.inspect({:B, self()}, label: "I am") 339 | 340 | Process.link(a) 341 | 342 | receive do 343 | _ -> :ok 344 | end 345 | end) 346 | 347 | # Process.sleep(100) 348 | # c = spawn(fn -> 349 | # Process.register(self(), C) 350 | # IO.inspect({C, self()}, label: "I am") 351 | # 352 | # Process.link(b) 353 | # receive do 354 | # _ -> :ok 355 | # end 356 | # end) 357 | 358 | Process.sleep(100) 359 | 360 | send(:A, :crash) 361 | # send(:A, :die_normally) 362 | 363 | Process.sleep(100) 364 | 365 | IO.inspect(Process.alive?(a), label: "Final A alive?") 366 | IO.inspect(Process.alive?(b), label: "Final B alive?") 367 | # IO.inspect(Process.alive?(c), label: "Final C alive?") 368 | ``` 369 | 370 | There is an option set a special process flag to trap exits. When `trap_exit` process flag is set to true, instread of receiving **exit signal** a process receives a message - `{:EXIT, pid, reason}` 371 | 372 | ```elixir 373 | IO.inspect(self(), label: "I am") 374 | 375 | a = 376 | spawn(fn -> 377 | Process.register(self(), :A) 378 | IO.inspect({:A, self()}, label: "I am") 379 | 380 | receive do 381 | :crash -> 1 = 2 382 | :die_normally -> :ok 383 | end 384 | end) 385 | 386 | Process.sleep(100) 387 | 388 | b = 389 | spawn(fn -> 390 | Process.register(self(), :B) 391 | Process.flag(:trap_exit, true) 392 | IO.inspect({:B, self()}, label: "I am") 393 | 394 | :A 395 | |> Process.whereis() 396 | |> Process.link() 397 | 398 | receive do 399 | msg -> IO.inspect(msg, label: "The trap exit messge") 400 | end 401 | 402 | receive do 403 | _ -> :ok 404 | end 405 | end) 406 | 407 | Process.sleep(100) 408 | 409 | send(:A, :crash) 410 | 411 | Process.sleep(100) 412 | 413 | IO.inspect(Process.alive?(a), label: "Final A alive?") 414 | IO.inspect(Process.alive?(b), label: "Final B alive?") 415 | ``` 416 | 417 | ```elixir 418 | ## Cleanup 419 | for proc <- [:A, :B, :C] do 420 | proc 421 | |> Process.whereis() 422 | |> (fn 423 | nil -> :ok 424 | pid -> Process.exit(pid, :kill) 425 | end).() 426 | end 427 | ``` 428 | 429 | ### Exercise 6 - 9 430 | 431 | Please go to `ElixirSchool2025/class2/exercises/lib` 432 | and try to solve exercises 6 - 9 433 | 434 | ## Last but not least 435 | 436 | Please visit https://github.com/LKlemens/ElixirSchool2025 437 | 438 | 439 | 440 | ### My email 441 | 442 | klemens.lukaszczyk@erlang-solutions.com 443 | --------------------------------------------------------------------------------