├── Chapter02 ├── README.md ├── apps │ ├── elixir_drip │ │ ├── README.md │ │ ├── test │ │ │ ├── test_helper.exs │ │ │ └── support │ │ │ │ └── data_case.ex │ │ ├── lib │ │ │ ├── elixir_drip │ │ │ │ ├── utils.ex │ │ │ │ ├── storage │ │ │ │ │ ├── provider.ex │ │ │ │ │ ├── media.ex │ │ │ │ │ ├── providers │ │ │ │ │ │ └── google_cloud_storage │ │ │ │ │ │ │ └── local.ex │ │ │ │ │ └── storage.ex │ │ │ │ ├── repo.ex │ │ │ │ ├── behaviours │ │ │ │ │ └── storage_provider.ex │ │ │ │ ├── application.ex │ │ │ │ └── ecto │ │ │ │ │ └── ksuid.ex │ │ │ └── elixir_drip.ex │ │ ├── config │ │ │ ├── config.exs │ │ │ ├── prod.exs │ │ │ ├── dev.exs │ │ │ └── test.exs │ │ ├── priv │ │ │ └── repo │ │ │ │ └── seeds.exs │ │ ├── .gitignore │ │ └── mix.exs │ └── elixir_drip_web │ │ ├── assets │ │ ├── css │ │ │ └── app.css │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ │ └── phoenix.png │ │ │ └── robots.txt │ │ ├── package.json │ │ ├── js │ │ │ ├── app.js │ │ │ └── socket.js │ │ └── brunch-config.js │ │ ├── test │ │ ├── test_helper.exs │ │ ├── elixir_drip_web │ │ │ ├── views │ │ │ │ ├── layout_view_test.exs │ │ │ │ ├── page_view_test.exs │ │ │ │ └── error_view_test.exs │ │ │ └── controllers │ │ │ │ └── page_controller_test.exs │ │ └── support │ │ │ ├── channel_case.ex │ │ │ └── conn_case.ex │ │ ├── lib │ │ ├── elixir_drip_web │ │ │ ├── views │ │ │ │ ├── page_view.ex │ │ │ │ ├── layout_view.ex │ │ │ │ ├── error_view.ex │ │ │ │ └── error_helpers.ex │ │ │ ├── controllers │ │ │ │ └── page_controller.ex │ │ │ ├── router.ex │ │ │ ├── gettext.ex │ │ │ ├── application.ex │ │ │ ├── templates │ │ │ │ ├── page │ │ │ │ │ └── index.html.eex │ │ │ │ └── layout │ │ │ │ │ └── app.html.eex │ │ │ ├── channels │ │ │ │ └── user_socket.ex │ │ │ └── endpoint.ex │ │ └── elixir_drip_web.ex │ │ ├── config │ │ ├── test.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ └── prod.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── mix.exs │ │ └── priv │ │ └── gettext │ │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ │ └── errors.pot ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ └── test.exs ├── .gitignore ├── .formatter.exs ├── env │ ├── test.env │ └── dev.env ├── mix.exs └── rel │ └── config.exs ├── Chapter01 ├── protocols │ ├── folder.ex │ ├── size.ex │ ├── folder_with_enforce_keys.ex │ ├── size_implementations_basic_types.ex │ └── size_implementations_with_file_stat_and_folder.ex ├── typespecs_and_behaviours │ ├── presenter.ex │ ├── cli_presenter.ex │ ├── presenter_with_optional_callbacks.ex │ ├── cli_presenter_with_impl_annotation.ex │ └── string_helper_palindrome_with_typespec.ex ├── functions_and_modules │ ├── nesting_modules_inline.ex │ ├── nesting_modules.ex │ ├── string_helper_emphasize_with_default_args.ex │ ├── string_helper_emphasize.ex │ ├── string_helper_emphasize_with_function_header.ex │ ├── iex_examples.txt │ ├── string_helper_palindrome_with_guard_clause.ex │ ├── string_helper_palindrome_with_defguard.ex │ ├── string_helper_palindrome_with_pipe_operator.ex │ └── string_helper_with_module_attribute.ex ├── working_with_collections │ ├── double.ex │ ├── multiply.ex │ ├── multiply_with_tail_recursion.ex │ └── iex_examples.txt ├── tooling_and_ecosystem │ └── iex_examples.txt ├── pattern_matching │ └── iex_examples.txt ├── control_flow │ └── iex_examples.txt └── the_type_system │ └── iex_examples.txt ├── Chapter03 ├── recovering_from_errors_with_supervisors_and_supervision_trees │ ├── simple_elixir_drip │ │ ├── test │ │ │ ├── test_helper.exs │ │ │ └── simple_elixir_drip_test.exs │ │ ├── .formatter.exs │ │ ├── lib │ │ │ ├── simple_elixir_drip.ex │ │ │ └── simple_elixir_drip │ │ │ │ ├── upload_pipeline_supervisor.ex │ │ │ │ ├── application.ex │ │ │ │ ├── encryption.ex │ │ │ │ ├── cache_supervisor.ex │ │ │ │ └── cache_worker.ex │ │ ├── README.md │ │ ├── .gitignore │ │ ├── mix.exs │ │ └── config │ │ │ └── config.exs │ ├── images │ │ ├── observer_supervision_tree.png │ │ └── simple_elixir_drip_supervision_tree.png │ ├── encryption.ex │ ├── upload_pipeline_supervisor_with_streamlined_child_specs.ex │ ├── upload_pipeline_supervisor.ex │ ├── encryption_with_streamlined_child_specs.ex │ ├── iex_examples.txt │ ├── cache_supervisor.ex │ └── cache_worker_under_supervisor.ex ├── detecting_errors_by_linking_and_monitoring_processes │ ├── iex_examples.txt │ └── cache_worker_with_spawn_link.ex └── working_with_processes │ ├── cache_worker.ex │ └── cache_worker_with_interface_functions.ex ├── Chapter06 ├── ch6_metaprogramming.zip ├── apps │ └── elixir_drip │ │ └── lib │ │ └── elixir_drip │ │ ├── storage │ │ ├── supervisors │ │ │ └── streamlined_upload_pipeline.ex │ │ └── pipeline │ │ │ ├── notifier.ex │ │ │ ├── starter.ex │ │ │ └── remote_storage.ex │ │ └── dsl │ │ ├── pipeliner_producer.ex │ │ └── pipeliner_consumer.ex └── examples │ ├── measured_module.exs │ └── measured_module_expanded.exs ├── Chapter11 ├── images │ ├── B08557_11_01.png │ ├── B08557_11_02.png │ ├── B08557_11_03.png │ ├── B08557_11_04.png │ ├── B08557_11_05.png │ ├── B08557_11_06.png │ ├── B08557_11_07.png │ ├── B08557_11_08.png │ ├── B08557_11_09.png │ ├── B08557_11_10.png │ ├── B08557_11_11.png │ ├── B08557_11_12.png │ ├── B08557_11_13.png │ ├── B08557_11_14.png │ ├── B08557_11_15.png │ ├── B08557_11_16.png │ ├── B08557_11_17.png │ ├── B08557_11_18.png │ ├── B08557_11_19.png │ ├── B08557_11_20.png │ └── B08557_11_21.png └── code │ ├── monitoring │ ├── Dockerfile.prometheus │ ├── tiller-sa-role-binding.yml │ ├── elixir-drip-service-monitor-prod.yml │ └── prometheus.dev.yml │ ├── apps │ ├── elixir_drip_web │ │ ├── lib │ │ │ └── elixir_drip_web │ │ │ │ ├── prometheus │ │ │ │ ├── metrics_exporter.ex │ │ │ │ └── endpoint_instrumenter.ex │ │ │ │ ├── application.ex │ │ │ │ └── endpoint.ex │ │ ├── config │ │ │ └── config.exs │ │ └── mix.exs │ └── elixir_drip │ │ ├── lib │ │ └── elixir_drip │ │ │ ├── application.ex │ │ │ ├── prometheus │ │ │ └── instrumenter.ex │ │ │ └── storage │ │ │ └── workers │ │ │ └── cache_worker.ex │ │ └── mix.exs │ ├── docker-compose.dev.yml │ └── rel │ └── config.exs ├── Chapter05 ├── ch5_demand_driven_processing.zip └── apps │ └── elixir_drip │ └── lib │ └── elixir_drip │ └── storage │ ├── pipeline │ ├── common.ex │ ├── notifier.ex │ ├── starter.ex │ ├── encryption.ex │ └── remote_storage.ex │ └── supervisors │ ├── download_pipeline_supervisor.ex │ └── upload_pipeline_supervisor.ex ├── Chapter08 ├── apps │ ├── elixir_drip_web │ │ ├── lib │ │ │ └── elixir_drip_web │ │ │ │ ├── views │ │ │ │ ├── session_view.ex │ │ │ │ ├── file_view.ex │ │ │ │ └── error_view.ex │ │ │ │ ├── api │ │ │ │ ├── views │ │ │ │ │ ├── session_view.ex │ │ │ │ │ └── file_view.ex │ │ │ │ └── controllers │ │ │ │ │ ├── file_controller.ex │ │ │ │ │ └── session_controller.ex │ │ │ │ ├── templates │ │ │ │ ├── file │ │ │ │ │ ├── file.html.eex │ │ │ │ │ ├── show.html.eex │ │ │ │ │ └── new.html.eex │ │ │ │ ├── session │ │ │ │ │ └── new.html.eex │ │ │ │ └── layout │ │ │ │ │ └── app.html.eex │ │ │ │ ├── controllers │ │ │ │ ├── fallback_controller.ex │ │ │ │ └── session_controller.ex │ │ │ │ ├── plugs │ │ │ │ ├── fetch_user.ex │ │ │ │ └── auth.ex │ │ │ │ ├── notifications │ │ │ │ └── notifications.ex │ │ │ │ ├── router.ex │ │ │ │ ├── channels │ │ │ │ ├── user_socket.ex │ │ │ │ └── user_channel.ex │ │ │ │ └── endpoint.ex │ │ ├── assets │ │ │ └── js │ │ │ │ ├── app.js │ │ │ │ ├── notification.js │ │ │ │ └── socket.js │ │ └── config │ │ │ └── config.exs │ └── elixir_drip │ │ └── lib │ │ └── elixir_drip │ │ └── storage │ │ └── pipeline │ │ └── notifier.ex ├── my_module_plug.ex └── my_plug_pipeline.ex ├── Chapter07 ├── ch07_persisting_data_with_ecto.code.zip ├── ch07_persisting_data_with_ecto.images.zip └── apps │ └── elixir_drip │ ├── priv │ └── repo │ │ └── migrations │ │ ├── 20180320101722_unique_users_constraint.exs │ │ ├── 20180408161233_unique_shares_constraint.exs │ │ ├── 20180408150211_create_media_owners.exs │ │ ├── 20180409214938_change_media_owners_on_delete_media.exs │ │ └── 20180112095312_create_media.exs │ ├── lib │ └── elixir_drip │ │ ├── repo.ex │ │ ├── storage │ │ ├── macros.ex │ │ ├── owner.ex │ │ └── media_owners.ex │ │ ├── accounts │ │ ├── accounts.ex │ │ └── user.ex │ │ ├── ecto │ │ └── ksuid.ex │ │ └── application.ex │ ├── config │ ├── dev.exs │ └── config.exs │ └── mix.exs ├── Chapter09 └── apps │ ├── elixir_drip │ ├── test │ │ ├── test_helper.exs │ │ ├── chronometer_test.exs │ │ └── elixir_drip │ │ │ └── storage │ │ │ ├── pipeline │ │ │ ├── remote_storage_test.exs │ │ │ └── media_property_test.exs │ │ │ ├── storage_repo_test.exs │ │ │ └── media_test.exs │ ├── config │ │ ├── test.exs │ │ └── config.exs │ ├── lib │ │ └── elixir_drip │ │ │ └── storage │ │ │ └── pipeline │ │ │ └── remote_storage.ex │ └── mix.exs │ └── elixir_drip_web │ └── test │ └── elixir_drip_web │ ├── channels │ ├── user_socket_test.exs │ └── user_channel_test.exs │ └── controllers │ └── file_controller_test.exs ├── Chapter04 └── apps │ └── elixir_drip │ └── lib │ └── elixir_drip │ ├── storage │ ├── workers │ │ ├── agent_cache_worker.ex │ │ └── cache_worker.ex │ └── supervisors │ │ └── cache_supervisor.ex │ └── application.ex └── LICENSE /Chapter02/README.md: -------------------------------------------------------------------------------- 1 | # ElixirDrip Umbrella 2 | 3 | 4 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/README.md: -------------------------------------------------------------------------------- 1 | # ElixirDrip 2 | 3 | **TODO: Add description** 4 | -------------------------------------------------------------------------------- /Chapter02/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "#{Mix.env}.exs" 4 | -------------------------------------------------------------------------------- /Chapter02/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | *.beam 7 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ -------------------------------------------------------------------------------- /Chapter01/protocols/folder.ex: -------------------------------------------------------------------------------- 1 | defmodule Folder do 2 | defstruct name: "new folder", files_info: [], path: nil 3 | end 4 | -------------------------------------------------------------------------------- /Chapter01/typespecs_and_behaviours/presenter.ex: -------------------------------------------------------------------------------- 1 | defmodule Presenter do 2 | @callback present(String.t) :: atom 3 | end 4 | -------------------------------------------------------------------------------- /Chapter02/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "mix.exs", 4 | "apps/*/{config,lib,test}/**/*.{ex,exs}" 5 | ] 6 | ] 7 | -------------------------------------------------------------------------------- /Chapter01/protocols/size.ex: -------------------------------------------------------------------------------- 1 | defprotocol Size do 2 | @doc "Calculates the size of a data structure" 3 | def size(data) 4 | end 5 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, :manual) 4 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, :manual) 4 | -------------------------------------------------------------------------------- /Chapter06/ch6_metaprogramming.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter06/ch6_metaprogramming.zip -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_01.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_02.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_03.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_04.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_05.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_06.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_07.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_08.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_09.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_10.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_11.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_12.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_13.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_14.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_15.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_16.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_17.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_18.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_19.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_20.png -------------------------------------------------------------------------------- /Chapter11/images/B08557_11_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter11/images/B08557_11_21.png -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/nesting_modules_inline.ex: -------------------------------------------------------------------------------- 1 | defmodule Helpers.StringHelper do 2 | # StringHelper code goes here 3 | end 4 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.PageView do 2 | use ElixirDripWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /Chapter11/code/monitoring/Dockerfile.prometheus: -------------------------------------------------------------------------------- 1 | FROM prom/prometheus 2 | 3 | COPY monitoring/prometheus.dev.yml /etc/prometheus/prometheus.yml 4 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.LayoutView do 2 | use ElixirDripWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /Chapter05/ch5_demand_driven_processing.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter05/ch5_demand_driven_processing.zip -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.SessionView do 2 | use ElixirDripWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/nesting_modules.ex: -------------------------------------------------------------------------------- 1 | defmodule Helpers do 2 | defmodule StringHelper do 3 | # StringHelper code goes here 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Chapter01/protocols/folder_with_enforce_keys.ex: -------------------------------------------------------------------------------- 1 | defmodule Folder do 2 | @enforce_keys :path 3 | defstruct name: "new folder", files_info: [], path: nil 4 | end 5 | -------------------------------------------------------------------------------- /Chapter07/ch07_persisting_data_with_ecto.code.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter07/ch07_persisting_data_with_ecto.code.zip -------------------------------------------------------------------------------- /Chapter07/ch07_persisting_data_with_ecto.images.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter07/ch07_persisting_data_with_ecto.images.zip -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/elixir_drip_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.LayoutViewTest do 2 | use ElixirDripWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/elixir_drip_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.PageViewTest do 2 | use ElixirDripWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/lib/elixir_drip_web/prometheus/metrics_exporter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.MetricsExporter do 2 | use Prometheus.PlugExporter 3 | end 4 | -------------------------------------------------------------------------------- /Chapter01/typespecs_and_behaviours/cli_presenter.ex: -------------------------------------------------------------------------------- 1 | defmodule CLIPresenter do 2 | @behaviour Presenter 3 | 4 | def present(text) do 5 | IO.puts(text) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter02/apps/elixir_drip_web/assets/static/favicon.ico -------------------------------------------------------------------------------- /Chapter01/typespecs_and_behaviours/presenter_with_optional_callbacks.ex: -------------------------------------------------------------------------------- 1 | defmodule Presenter do 2 | @callback present(String.t) :: atom 3 | @optional_callbacks present: 1 4 | end 5 | -------------------------------------------------------------------------------- /Chapter01/working_with_collections/double.ex: -------------------------------------------------------------------------------- 1 | defmodule Recursion do 2 | def double([]), do: [] 3 | def double([head | tail]) do 4 | [head * 2 | double(tail)] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Chapter01/working_with_collections/multiply.ex: -------------------------------------------------------------------------------- 1 | defmodule Recursion do 2 | def multiply([]), do: 1 3 | def multiply([head | tail]) do 4 | head * multiply(tail) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/lib/elixir_drip_web/prometheus/endpoint_instrumenter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.EndpointInstrumenter do 2 | use Prometheus.PhoenixInstrumenter 3 | end 4 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter02/apps/elixir_drip_web/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /Chapter01/typespecs_and_behaviours/cli_presenter_with_impl_annotation.ex: -------------------------------------------------------------------------------- 1 | defmodule CLIPresenter do 2 | @behaviour Presenter 3 | 4 | @impl true 5 | def present(text) do 6 | IO.puts(text) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/views/file_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.FileView do 2 | use ElixirDripWeb, :view 3 | 4 | def parent_directory(path) do 5 | Path.dirname(path) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter01/typespecs_and_behaviours/string_helper_palindrome_with_typespec.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | @spec palindrome?(String.t) :: boolean 3 | 4 | def palindrome?(term) do 5 | String.reverse(term) == term 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter08/my_module_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule MyModulePlug do 2 | import Plug.Conn 3 | 4 | def init(options), do: options 5 | 6 | def call(conn, _options) do 7 | conn 8 | |> assign(:username, "Gabriel") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Utils do 2 | def generate_timestamp do 3 | DateTime.utc_now() 4 | |> DateTime.to_iso8601(:basic) 5 | |> String.split(".") 6 | |> hd 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Mox.defmock(ElixirDrip.Storage.Providers.GoogleCloudStorageMock, for: ElixirDrip.Behaviours.StorageProvider) 4 | 5 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, :manual) 6 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.PageController do 2 | use ElixirDripWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter01/working_with_collections/multiply_with_tail_recursion.ex: -------------------------------------------------------------------------------- 1 | defmodule Recursion do 2 | def multiply(list, accum \\ 1) 3 | def multiply([], accum), do: accum 4 | def multiply([head | tail], accum) do 5 | multiply(tail, head * accum) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :elixir_drip, ecto_repos: [ElixirDrip.Repo] 4 | 5 | config :elixir_drip, 6 | storage_provider: ElixirDrip.Storage.Providers.GoogleCloudStorage.Local 7 | 8 | import_config "#{Mix.env()}.exs" 9 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/priv/repo/migrations/20180320101722_unique_users_constraint.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo.Migrations.UniqueUsersConstraint do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create unique_index(:users, [:username]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://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 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.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 :elixir_drip_web, ElixirDripWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | -------------------------------------------------------------------------------- /Chapter01/tooling_and_ecosystem/iex_examples.txt: -------------------------------------------------------------------------------- 1 | h Enum 2 | h Enum.map/2 3 | break! StringHelper.palindrome?/1 4 | StringHelper.palindrome?("abba") 5 | 7 * 3 6 | a = v 7 | a * 2 8 | v(-3) 9 | :math.log(10) 10 | port = Port.open({:spawn, "whoami"}, [:binary]) 11 | flush() 12 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/images/observer_supervision_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/images/observer_supervision_tree.png -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/images/simple_elixir_drip_supervision_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Elixir/HEAD/Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/images/simple_elixir_drip_supervision_tree.png -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/storage/provider.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Provider do 2 | @moduledoc false 3 | 4 | @target Application.get_env(:elixir_drip, :storage_provider) 5 | 6 | defdelegate upload(path, content), to: @target 7 | defdelegate download(path), to: @target 8 | end 9 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/test/simple_elixir_drip_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDripTest do 2 | use ExUnit.Case 3 | doctest SimpleElixirDrip 4 | 5 | test "greets the world" do 6 | assert SimpleElixirDrip.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/priv/repo/migrations/20180408161233_unique_shares_constraint.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo.Migrations.UniqueSharesConstraint do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create unique_index(:media_owners, [:media_id, :user_id], name: :single_share_index) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/elixir_drip_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.PageControllerTest do 2 | use ElixirDripWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "prod.secret.exs" 4 | 5 | config :elixir_drip, ElixirDrip.Repo, 6 | adapter: Ecto.Adapters.Postgres, 7 | username: "${DB_USER}", 8 | password: "${DB_PASS}", 9 | database: "${DB_NAME}", 10 | hostname: "${DB_HOST}", 11 | pool_size: 15 12 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip do 2 | @moduledoc """ 3 | ElixirDrip 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 | -------------------------------------------------------------------------------- /Chapter08/my_plug_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule MyPlugPipeline do 2 | use Plug.Builder 3 | 4 | plug MyModulePlug 5 | plug :my_function_plug 6 | 7 | def my_function_plug(conn, _options) do 8 | conn 9 | |> put_resp_content_type("text/plain") 10 | |> send_resp(200, "Hello #{conn.assigns.username}!") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/pipeline/common.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Common do 2 | @moduledoc false 3 | 4 | def stage_name(stage, direction) do 5 | direction = direction 6 | |> Atom.to_string() 7 | |> String.capitalize() 8 | 9 | Module.concat(stage, direction) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo do 2 | use Ecto.Repo, otp_app: :elixir_drip 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo do 2 | use Ecto.Repo, otp_app: :elixir_drip 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter02/env/test.env: -------------------------------------------------------------------------------- 1 | REPLACE_OS_VARS=true 2 | ENV=test 3 | DEPLOYMENT_ENV=test 4 | MIX_ENV=test 5 | APP_NAME=elixir_drip 6 | SECRET_KEY_BASE=top_secret 7 | HOST=localhost 8 | PORT=4001 9 | DB_NAME=elixir_drip_test 10 | DB_USER=postgres 11 | DB_PASS=123456 12 | DB_HOST=postgres 13 | DB_PORT=5432 14 | GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account/key_file.json 15 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/api/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Api.SessionView do 2 | use ElixirDripWeb, :view 3 | 4 | def render("login.json", %{user: user}) do 5 | %{response: "Logged in as #{user.username}"} 6 | end 7 | 8 | def render("logout.json", _assigns) do 9 | %{response: "Logged out"} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter02/env/dev.env: -------------------------------------------------------------------------------- 1 | REPLACE_OS_VARS=true 2 | ENV=dev 3 | DEPLOYMENT_ENV=development 4 | MIX_ENV=dev 5 | APP_NAME=elixir_drip 6 | SECRET_KEY_BASE=top_secret 7 | HOST=localhost 8 | PORT=4000 9 | DB_NAME=elixir_drip_dev 10 | DB_USER=postgres 11 | DB_PASS=123456 12 | DB_HOST=postgres 13 | DB_PORT=5432 14 | GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account/key_file.json 15 | -------------------------------------------------------------------------------- /Chapter11/code/monitoring/tiller-sa-role-binding.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: tiller-role-binding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: cluster-admin 9 | subjects: 10 | - kind: ServiceAccount 11 | name: tiller-sa 12 | namespace: kube-system 13 | -------------------------------------------------------------------------------- /Chapter01/protocols/size_implementations_basic_types.ex: -------------------------------------------------------------------------------- 1 | defimpl Size, for: BitString do 2 | def size(string), do: byte_size(string) 3 | end 4 | 5 | defimpl Size, for: Map do 6 | def size(map), do: map_size(map) 7 | end 8 | 9 | defimpl Size, for: Tuple do 10 | def size(tuple), do: tuple_size(tuple) 11 | end 12 | 13 | defimpl Size, for: Any do 14 | def size(_), do: 0 15 | end 16 | -------------------------------------------------------------------------------- /Chapter02/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # By default, the umbrella project as well as each child 4 | # application will require this configuration file, ensuring 5 | # they all use the same configuration. While one could 6 | # configure all applications here, we prefer to delegate 7 | # back to each application for organization purposes. 8 | import_config "../apps/*/config/config.exs" 9 | -------------------------------------------------------------------------------- /Chapter02/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # By default, the umbrella project as well as each child 4 | # application will require this configuration file, ensuring 5 | # they all use the same configuration. While one could 6 | # configure all applications here, we prefer to delegate 7 | # back to each application for organization purposes. 8 | import_config "../apps/*/config/config.exs" 9 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :elixir_drip, ElixirDrip.Repo, 5 | adapter: Ecto.Adapters.Postgres, 6 | username: System.get_env("DB_USER"), 7 | password: System.get_env("DB_PASS"), 8 | database: System.get_env("DB_NAME"), 9 | hostname: System.get_env("DB_HOST"), 10 | port: System.get_env("DB_PORT"), 11 | pool_size: 10 12 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :elixir_drip, ElixirDrip.Repo, 5 | adapter: Ecto.Adapters.Postgres, 6 | username: System.get_env("DB_USER"), 7 | password: System.get_env("DB_PASS"), 8 | database: System.get_env("DB_NAME"), 9 | hostname: System.get_env("DB_HOST"), 10 | port: System.get_env("DB_PORT"), 11 | pool_size: 10 12 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip do 2 | @moduledoc """ 3 | Documentation for SimpleElixirDrip. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> SimpleElixirDrip.hello 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/templates/file/file.html.eex: -------------------------------------------------------------------------------- 1 | 2 | "> 3 | <%= link(@file.name, to: file_path(@conn, :show, @file.id)) %> 4 | <%= @file.size %> bytes 5 | 6 | <%= button("Download File", to: file_path(@conn, :show, @file.id) <> "/download", class: "btn btn-primary") %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_emphasize_with_default_args.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | def palindrome?(term) do 3 | String.reverse(term) == term 4 | end 5 | 6 | def emphasize(phrase, number_of_marks \\ 3) do 7 | upcased_phrase = String.upcase(phrase) 8 | exclamation_marks = String.duplicate("!", number_of_marks) 9 | "#{upcased_phrase}#{exclamation_marks}" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :elixir_drip, ElixirDrip.Repo, 5 | adapter: Ecto.Adapters.Postgres, 6 | username: System.get_env("DB_USER"), 7 | password: System.get_env("DB_PASS"), 8 | database: System.get_env("DB_NAME"), 9 | hostname: System.get_env("DB_HOST"), 10 | port: System.get_env("DB_PORT"), 11 | pool: Ecto.Adapters.SQL.Sandbox 12 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/storage/macros.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Macros do 2 | defmacro remaining_path(pwd_size, full_path) do 3 | quote do 4 | fragment("right(?, ?)", unquote(full_path), unquote(pwd_size)) 5 | end 6 | end 7 | 8 | defmacro is_folder(remaining_path) do 9 | quote do 10 | fragment("length(?) > 0", unquote(remaining_path)) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/priv/repo/migrations/20180408150211_create_media_owners.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo.Migrations.CreateMediaOwners do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:media_owners) do 6 | add :media_id, references(:storage_media, type: :string) 7 | add :user_id, references(:users, type: :string) 8 | 9 | timestamps(type: :utc_datetime) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/storage/media.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Media do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, ElixirDrip.Ecto.Ksuid, autogenerate: true} 5 | schema "storage_media" do 6 | field :filename, :string 7 | field :full_path, :string 8 | field :metadata, :map 9 | field :encryption_key, :string 10 | field :storage_key, :string 11 | 12 | timestamps() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # ElixirDrip.Repo.insert!(%ElixirDrip.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/storage/owner.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Owner do 2 | use Ecto.Schema 3 | 4 | alias ElixirDrip.Storage.{ 5 | Media, 6 | MediaOwners 7 | } 8 | 9 | @primary_key {:id, ElixirDrip.Ecto.Ksuid, autogenerate: true} 10 | schema "users" do 11 | field :email, :string 12 | many_to_many :files, Media, join_through: MediaOwners, join_keys: [user_id: :id, media_id: :id] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/storage/media_owners.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.MediaOwners do 2 | use Ecto.Schema 3 | 4 | alias ElixirDrip.Storage.{ 5 | Media, 6 | Owner 7 | } 8 | 9 | schema "media_owners" do 10 | belongs_to :media, Media, foreign_key: :media_id, type: ElixirDrip.Ecto.Ksuid 11 | belongs_to :owner, Owner, foreign_key: :user_id, type: ElixirDrip.Ecto.Ksuid 12 | timestamps() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/priv/repo/migrations/20180409214938_change_media_owners_on_delete_media.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo.Migrations.ChangeMediaOwnersOnDeleteMedia do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop constraint(:media_owners, :media_owners_media_id_fkey) 6 | 7 | alter table(:media_owners) do 8 | modify :media_id, references(:storage_media, type: :string, on_delete: :delete_all) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Chapter11/code/monitoring/elixir-drip-service-monitor-prod.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | app: prometheus 6 | name: elixir-drip-monitor 7 | namespace: monitoring 8 | spec: 9 | jobLabel: elixir-drip 10 | endpoints: 11 | - interval: 30s 12 | port: web 13 | namespaceSelector: 14 | matchNames: 15 | - production 16 | selector: 17 | matchLabels: 18 | app: elixir-drip 19 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_emphasize.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | def palindrome?(term) do 3 | String.reverse(term) == term 4 | end 5 | 6 | def emphasize(phrase) do 7 | emphasize(phrase, 3) 8 | end 9 | 10 | def emphasize(phrase, number_of_marks) do 11 | upcased_phrase = String.upcase(phrase) 12 | exclamation_marks = String.duplicate("!", number_of_marks) 13 | "#{upcased_phrase}#{exclamation_marks}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :elixir_drip, ecto_repos: [ElixirDrip.Repo] 4 | 5 | config :elixir_drip, 6 | storage_provider: ElixirDrip.Storage.Providers.GoogleCloudStorage.Local 7 | 8 | config :arc, 9 | storage: Arc.Storage.GCS, 10 | bucket: "elixir-drip-andre-development", 11 | storage_dir: "v1" 12 | 13 | config :goth, 14 | config_module: ElixirDrip.Config.GcsCredentials 15 | 16 | import_config "#{Mix.env()}.exs" 17 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Files matching config/*.secret.exs pattern contain sensitive 11 | # data and you should not commit them into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets files as long as you replace their contents by environment 15 | # variables. 16 | /config/*.secret.exs 17 | -------------------------------------------------------------------------------- /Chapter02/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :junit_formatter, 4 | report_file: "results.xml", 5 | print_report_file: true 6 | 7 | # By default, the umbrella project as well as each child 8 | # application will require this configuration file, ensuring 9 | # they all use the same configuration. While one could 10 | # configure all applications here, we prefer to delegate 11 | # back to each application for organization purposes. 12 | import_config "../apps/*/config/config.exs" 13 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/templates/session/new.html.eex: -------------------------------------------------------------------------------- 1 |

Login

2 | <%= form_for @conn, session_path(@conn, :create), fn f -> %> 3 |
4 | <%= text_input f, :username, placeholder: "Username", class: "form-control" %> 5 |
6 |
7 | <%= password_input f, :password, placeholder: "Password", class: "form-control" %> 8 |
9 | <%= submit("Login", class: "btn btn-primary") %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.ErrorView do 2 | use ElixirDripWeb, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render("500.html", assigns) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "phoenix": "file:../../../deps/phoenix", 10 | "phoenix_html": "file:../../../deps/phoenix_html" 11 | }, 12 | "devDependencies": { 13 | "babel-brunch": "6.1.1", 14 | "brunch": "2.10.9", 15 | "clean-css-brunch": "2.10.0", 16 | "uglify-js-brunch": "2.10.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/storage/supervisors/streamlined_upload_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.Upload.StreamlinedPipeline do 2 | alias ElixirDrip.Pipeliner 3 | alias ElixirDrip.Storage.Pipeline.{ 4 | Starter, 5 | Encryption, 6 | RemoteStorage, 7 | Notifier 8 | } 9 | 10 | use Pipeliner, 11 | name: :upload_pipeline, min_demand: 1, max_demand: 10, count: 1 12 | 13 | start Starter, args: [:upload] 14 | step RemoteStorage 15 | step Encryption 16 | finish Notifier 17 | end 18 | -------------------------------------------------------------------------------- /Chapter11/code/monitoring/prometheus.dev.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 3 | 4 | # A scrape configuration containing exactly one endpoint to scrape: 5 | # Here it's Prometheus itself. 6 | scrape_configs: 7 | # The job name is added as a label `job=` to any timeseries scraped from this config. 8 | - job_name: 'prometheus' 9 | static_configs: 10 | - targets: ['localhost:9090'] 11 | 12 | - job_name: 'elixir-drip' 13 | static_configs: 14 | - targets: ['app:4000'] 15 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_emphasize_with_function_header.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | def palindrome?(term) do 3 | String.reverse(term) == term 4 | end 5 | 6 | def emphasize(phrase, number_of_marks \\ 3) 7 | def emphasize(_phrase, 0) do 8 | "This isn't the module you're looking for" 9 | end 10 | def emphasize(phrase, number_of_marks) do 11 | upcased_phrase = String.upcase(phrase) 12 | exclamation_marks = String.duplicate("!", number_of_marks) 13 | "#{upcased_phrase}#{exclamation_marks}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/encryption.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Encryption do 2 | require Logger 3 | 4 | def start_link() do 5 | pid = spawn_link(fn -> 6 | Logger.debug("#{inspect(self())} Starting the Encryption module...") 7 | loop() 8 | end) 9 | 10 | {:ok, pid} 11 | end 12 | 13 | defp loop() do 14 | receive do 15 | message -> IO.puts "#{inspect(self())} Encryption module received a message: #{message}" 16 | end 17 | 18 | loop() 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/behaviours/storage_provider.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Behaviours.StorageProvider do 2 | @type path :: binary() 3 | @type content :: bitstring() 4 | @type reason :: atom() 5 | @type details :: map() 6 | 7 | @callback upload(path, content) :: 8 | {:ok, :uploaded} 9 | | {:error, reason} 10 | | {:error, reason, details} 11 | @callback download(path) :: 12 | {:ok, content} 13 | | {:error, reason} 14 | | {:error, reason, details} 15 | end 16 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :elixir_drip, storage_provider: ElixirDrip.Storage.Providers.GoogleCloudStorageMock 4 | 5 | # Configure your database 6 | config :elixir_drip, ElixirDrip.Repo, 7 | adapter: Ecto.Adapters.Postgres, 8 | username: System.get_env("DB_USER"), 9 | password: System.get_env("DB_PASS"), 10 | database: System.get_env("DB_NAME"), 11 | hostname: System.get_env("DB_HOST"), 12 | port: System.get_env("DB_PORT"), 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | 15 | config :stream_data, max_runs: 500 16 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/storage/providers/google_cloud_storage/local.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Providers.GoogleCloudStorage.Local do 2 | @behaviour ElixirDrip.Behaviours.StorageProvider 3 | 4 | require Logger 5 | 6 | def upload(path, content) do 7 | Logger.debug("Uploading #{inspect(byte_size(content))} bytes to Google Cloud Storage, path: #{path}") 8 | {:ok, :uploaded} 9 | end 10 | 11 | def download(path) do 12 | Logger.debug("Downloading #{path} from Google Cloud Storage") 13 | {:ok, "downloaded_content"} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/upload_pipeline_supervisor_with_streamlined_child_specs.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.Upload.Pipeline do 2 | use Supervisor 3 | require Logger 4 | alias ElixirDrip.Storage.Pipeline.Encryption 5 | 6 | def start_link() do 7 | Supervisor.start_link(__MODULE__, :ok) 8 | end 9 | 10 | def init(:ok) do 11 | Logger.debug("#{inspect(self())} Starting the Upload Pipeline Supervisor module...") 12 | 13 | Supervisor.init([Encryption], strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Chapter06/examples/measured_module.exs: -------------------------------------------------------------------------------- 1 | defmodule MeasuredModule do 2 | use ElixirDrip.Chronometer, unit: :secs 3 | 4 | defchrono slow_times(x, y) do 5 | Process.sleep(2000) 6 | x * y 7 | end 8 | 9 | defchrono slow_square(x \\ 3) do 10 | Process.sleep(2000) 11 | x * x 12 | end 13 | 14 | # [a: %{x: 1}, b: %{y: 9}] |> defkv() 15 | 16 | # defchrono_v3 slow_times(x, y) do 17 | # Process.sleep(2000) 18 | # x * y 19 | # end 20 | 21 | # defchrono_v3 slow_times(x, y, z) do 22 | # Process.sleep(2000) 23 | # x * y * z 24 | # end 25 | end 26 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/iex_examples.txt: -------------------------------------------------------------------------------- 1 | plus_one = fn (x) -> x + 1 end 2 | plus_one.(10) 3 | division = fn 4 | (_dividend, 0) -> :infinity 5 | (dividend, divisor) -> dividend / divisor 6 | end 7 | division.(10, 2) 8 | division.(10, 0) 9 | x = 3 10 | some_fun = fn -> "variable x is #{x}" end 11 | some_fun.() 12 | x = 5 13 | some_fun.() 14 | plus_one = &(&1 + 1) 15 | plus_one.(10) 16 | defmodule StringHelper do 17 | def palindrome?(term) do 18 | String.reverse(term) == term 19 | end 20 | end 21 | StringHelper.palindrome?("abcd") 22 | StringHelper.palindrome?("abba") 23 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/priv/repo/migrations/20180112095312_create_media.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Repo.Migrations.Storage.Media do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:storage_media, primary_key: false) do 6 | add :id, :string, primary_key: true, size: 27 7 | add :file_name, :string 8 | add :full_path, :string 9 | add :metadata, :map 10 | add :encryption_key, :string 11 | add :storage_key, :string 12 | add :uploaded_at, :utc_datetime 13 | 14 | timestamps(type: :utc_datetime) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/upload_pipeline_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.Upload.Pipeline do 2 | use Supervisor 3 | require Logger 4 | alias ElixirDrip.Storage.Pipeline.Encryption 5 | 6 | def start_link() do 7 | Supervisor.start_link(__MODULE__, :ok) 8 | end 9 | 10 | def init(:ok) do 11 | Logger.debug("#{inspect(self())} Starting the Upload Pipeline Supervisor module...") 12 | 13 | Supervisor.init( 14 | [worker(Encryption, [], restart: :transient)], 15 | strategy: :one_for_one 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip/upload_pipeline_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.Storage.Supervisors.Upload.Pipeline do 2 | use Supervisor 3 | require Logger 4 | alias SimpleElixirDrip.Storage.Pipeline.Encryption 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | def init(:ok) do 11 | Logger.debug("#{inspect(self())} Starting the Upload Pipeline Supervisor module...") 12 | 13 | Supervisor.init([Encryption], strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Chapter06/examples/measured_module_expanded.exs: -------------------------------------------------------------------------------- 1 | defmodule MeasuredModuleExpanded do 2 | alias ElixirDrip.Chronometer 3 | 4 | function_definition = {:slow_square, [], [{:\\, [], [{:x, [], nil}, 3]}]} 5 | body = {:__block__, [], [{{:., [], [{:__aliases__, [], [:Process]}, :sleep]}, [], [2000]}, {:*, [], [{:x, [], nil}, {:x, [], nil}]}]} 6 | function = :slow_square 7 | arity = 1 8 | 9 | def(unquote(function_definition)) do 10 | signature = 11 | Chronometer.pretty_signature(__MODULE__, unquote(function), unquote(arity)) 12 | Chronometer.run_and_measure(signature, fn -> unquote(body) end) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/templates/file/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= render("file.html", conn: @conn, file: @file) %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Full path: <%= @file.full_path %>
Uploaded at: <%= @file.uploaded_at %>
13 | <%= link("Back to folder", to: file_path(@conn, :index, "path": @file.full_path), class: "btn btn-default") %> 14 | 15 |
16 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_palindrome_with_guard_clause.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | def palindrome?(term) when is_bitstring(term) do 3 | String.reverse(term) == term 4 | end 5 | def palindrome?(_term), do: {:error, :unsupported_type} 6 | 7 | def emphasize(phrase, number_of_marks \\ 3) 8 | def emphasize(_phrase, 0) do 9 | "This isn't the module you're looking for" 10 | end 11 | def emphasize(phrase, number_of_marks) do 12 | upcased_phrase = String.upcase(phrase) 13 | exclamation_marks = String.duplicate("!", number_of_marks) 14 | "#{upcased_phrase}#{exclamation_marks}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :elixir_drip, ecto_repos: [ElixirDrip.Repo] 4 | 5 | config :elixir_drip, storage_provider: ElixirDrip.Storage.Providers.GoogleCloudStorageLive 6 | 7 | config :arc, 8 | storage: Arc.Storage.GCS, 9 | bucket: "elixir-drip-andre-development", 10 | storage_dir: "v1" 11 | 12 | config :goth, 13 | config_module: ElixirDrip.Config.GcsCredentials 14 | 15 | config :distillery, 16 | # bcrypt_elixir depends on this, only in build time 17 | # doesn't make sense to include it in the release 18 | no_warn_missing: [:elixir_make] 19 | 20 | import_config "#{Mix.env()}.exs" 21 | -------------------------------------------------------------------------------- /Chapter01/working_with_collections/iex_examples.txt: -------------------------------------------------------------------------------- 1 | Enum.map([2, 4, 6], &(&1 * 2)) 2 | Enum.reduce([1, 2, 3], 1, &(&1 * &2)) 3 | require Integer 4 | Enum.map([1, 2, 3], &Integer.is_even/1) 5 | for x <- [2, 4, 6], do: x * 2 6 | for x <- [1, 2, 3], y <- [4, 5, 6], Integer.is_odd(x), do: x * y 7 | for x <- [1, 2, 3], into: %{}, do: {x, x + 1} 8 | [1, 2, 3, 4, 5] \ 9 | |> Enum.map(&(&1 + 10)) \ 10 | |> Enum.zip(["a", "b", "c", "d", "e"]) 11 | [1, 2, 3, 4, 5] \ 12 | |> Stream.map(&(&1 + 1)) \ 13 | |> Stream.zip(["a", "b", "c", "d", "e"]) 14 | [1, 2, 3, 4, 5] \ 15 | |> Stream.map(&(&1 + 10)) \ 16 | |> Stream.zip(["a", "b", "c", "d", "e"]) \ 17 | |> Enum.take(1) 18 | -------------------------------------------------------------------------------- /Chapter01/protocols/size_implementations_with_file_stat_and_folder.ex: -------------------------------------------------------------------------------- 1 | defimpl Size, for: BitString do 2 | def size(string), do: byte_size(string) 3 | end 4 | 5 | defimpl Size, for: Map do 6 | def size(map), do: map_size(map) 7 | end 8 | 9 | defimpl Size, for: Tuple do 10 | def size(tuple), do: tuple_size(tuple) 11 | end 12 | 13 | defimpl Size, for: Any do 14 | def size(_), do: 0 15 | end 16 | 17 | defimpl Size, for: File.Stat do 18 | def size(file_stat), do: file_stat.size 19 | end 20 | 21 | defimpl Size, for: Folder do 22 | def size(folder) do 23 | folder.files_info 24 | |> Enum.map(&Size.size(&1)) 25 | |> Enum.sum() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.ErrorView do 2 | use ElixirDripWeb, :view 3 | 4 | def render("400.json", %{message: message}) do 5 | %{ 6 | error: "bad request", 7 | details: message 8 | } 9 | end 10 | 11 | def render("401.json", %{message: message}) do 12 | %{error: message} 13 | end 14 | 15 | def render("404.html", _assigns) do 16 | "Page not found" 17 | end 18 | 19 | def render("500.html", _assigns) do 20 | "Internal server error" 21 | end 22 | 23 | def template_not_found(_template, assigns) do 24 | render("500.html", assigns) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/storage/storage.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage do 2 | @moduledoc false 3 | 4 | alias ElixirDrip.Utils 5 | alias ElixirDrip.Storage.Media 6 | alias ElixirDrip.Storage.Provider 7 | 8 | def store(%Media{} = media, content) do 9 | media 10 | |> generate_storage_key() 11 | |> Map.get(:storage_key) 12 | |> Provider.upload(content) 13 | end 14 | 15 | def retrieve(%Media{storage_key: storage_key}) do 16 | Provider.download(storage_key) 17 | end 18 | 19 | defp generate_storage_key(%Media{id: id} = media) do 20 | %{media | storage_key: id <> "_" <> Utils.generate_timestamp()} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/accounts/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Accounts do 2 | @moduledoc """ 3 | The Accounts context. 4 | """ 5 | 6 | alias ElixirDrip.Repo 7 | alias ElixirDrip.Accounts.User 8 | 9 | @doc """ 10 | Creates a user. 11 | """ 12 | def create_user(attrs \\ %{}) do 13 | %User{} 14 | |> User.create_changeset(attrs) 15 | |> Repo.insert() 16 | end 17 | 18 | def get_user_by_username(username) do 19 | User 20 | |> Repo.get_by(username: username) 21 | end 22 | 23 | def verify_user_password(%User{} = user, password) do 24 | Comeonin.Bcrypt.checkpw(password, user.hashed_password) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Application do 2 | @moduledoc """ 3 | The ElixirDrip Application Service. 4 | 5 | The elixir_drip system business domain lives in this application. 6 | 7 | Exposes API to clients such as the `ElixirDripWeb` application 8 | for use in channels, controllers, and elsewhere. 9 | """ 10 | use Application 11 | 12 | def start(_type, _args) do 13 | import Supervisor.Spec, warn: false 14 | 15 | Supervisor.start_link( 16 | [ 17 | supervisor(ElixirDrip.Repo, []) 18 | ], 19 | strategy: :one_for_one, 20 | name: ElixirDrip.Supervisor 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_palindrome_with_defguard.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | defguard is_string(term) when is_bitstring(term) 3 | 4 | def palindrome?(term) when is_string(term) do 5 | String.reverse(term) == term 6 | end 7 | def palindrome?(_term), do: {:error, :unsupported_type} 8 | 9 | def emphasize(phrase, number_of_marks \\ 3) 10 | def emphasize(_phrase, 0) do 11 | "This isn't the module you're looking for" 12 | end 13 | def emphasize(phrase, number_of_marks) do 14 | upcased_phrase = String.upcase(phrase) 15 | exclamation_marks = String.duplicate("!", number_of_marks) 16 | "#{upcased_phrase}#{exclamation_marks}" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/elixir_drip_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.ErrorViewTest do 2 | use ElixirDripWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(ElixirDripWeb.ErrorView, "404.html", []) == "Page not found" 9 | end 10 | 11 | test "render 500.html" do 12 | assert render_to_string(ElixirDripWeb.ErrorView, "500.html", []) == "Internal server error" 13 | end 14 | 15 | test "render any other" do 16 | assert render_to_string(ElixirDripWeb.ErrorView, "505.html", []) == "Internal server error" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Chapter03/detecting_errors_by_linking_and_monitoring_processes/iex_examples.txt: -------------------------------------------------------------------------------- 1 | cache_worker_pid = CacheWorker.init("some binary content") 2 | CacheWorker.get_content(cache_worker_pid) 3 | cache_worker_pid = CacheWorker.init("some other binary content") 4 | CacheWorker.get_content(cache_worker_pid) 5 | Process.exit(cache_worker_pid, :kill) 6 | Process.flag(:trap_exit, true) 7 | cache_worker_pid = CacheWorker.init("some binary content") 8 | CacheWorker.get_content(cache_worker_pid) 9 | Process.exit(cache_worker_pid, :kill) 10 | flush 11 | cache_worker_pid = CacheWorker.init("some binary content") 12 | CacheWorker.get_content(cache_worker_pid) 13 | monitor_reference = Process.monitor(cache_worker_pid) 14 | flush 15 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Router do 2 | use ElixirDripWeb, :router 3 | 4 | pipeline :browser do 5 | plug(:accepts, ["html"]) 6 | plug(:fetch_session) 7 | plug(:fetch_flash) 8 | plug(:protect_from_forgery) 9 | plug(:put_secure_browser_headers) 10 | end 11 | 12 | pipeline :api do 13 | plug(:accepts, ["json"]) 14 | end 15 | 16 | scope "/", ElixirDripWeb do 17 | # Use the default browser stack 18 | pipe_through(:browser) 19 | 20 | get("/", PageController, :index) 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", ElixirDripWeb do 25 | # pipe_through :api 26 | # end 27 | end 28 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip/application.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | require Logger 6 | 7 | alias SimpleElixirDrip.Storage.Supervisors.CacheSupervisor 8 | alias SimpleElixirDrip.Storage.Supervisors.Upload.Pipeline, as: UploadPipeline 9 | 10 | def start(_type, _args) do 11 | Logger.debug("Starting the SimpleElixirDrip Supervisor...") 12 | Supervisor.start_link( 13 | [ 14 | CacheSupervisor, 15 | UploadPipeline, 16 | ], 17 | strategy: :one_for_one, 18 | name: ElixirDrip.Supervisor 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/api/views/file_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Api.FileView do 2 | use ElixirDripWeb, :view 3 | 4 | def render("index.json", %{files: files, folders: folders}) do 5 | %{ 6 | response: %{ 7 | files: render_many(files, __MODULE__, "file.json"), 8 | folders: render_many(folders, __MODULE__, "folder.json", as: :folder) 9 | } 10 | } 11 | end 12 | 13 | def render("file.json", %{file: file}) do 14 | %{id: file.id, name: file.name, full_path: file.full_path, size: file.size} 15 | end 16 | 17 | def render("folder.json", %{folder: folder}) do 18 | %{name: folder.name, size: folder.size, files: folder.files} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/chronometer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.ChronometerTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.CaptureIO 5 | 6 | defmodule Factorial do 7 | use ElixirDrip.Chronometer, unit: :secs 8 | 9 | defchrono calculate(x) do 10 | _calc_factorial(x) 11 | end 12 | 13 | defp _calc_factorial(0), do: 1 14 | defp _calc_factorial(x) do 15 | x * _calc_factorial(x - 1) 16 | end 17 | end 18 | 19 | test "defchrono measures and prints the function execution time" do 20 | assert capture_io(fn -> 21 | Factorial.calculate(10000) 22 | end) =~ ~r/Took \d+\.\d+ secs to run ElixirDrip.ChronometerTest.Factorial.calculate\/1/ 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/README.md: -------------------------------------------------------------------------------- 1 | # SimpleElixirDrip 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 `simple_elixir_drip` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:simple_elixir_drip, "~> 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 [https://hexdocs.pm/simple_elixir_drip](https://hexdocs.pm/simple_elixir_drip). 21 | 22 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/elixir_drip/storage/pipeline/remote_storage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.RemoteStorageTest do 2 | use ExUnit.Case, async: true 3 | import Mox 4 | 5 | alias ElixirDrip.Storage.Providers.GoogleCloudStorageMock 6 | 7 | @subject ElixirDrip.Storage.Pipeline.RemoteStorage 8 | 9 | setup :verify_on_exit! 10 | 11 | test "on upload it calls the GoogleCloudStorage client with the right arguments" do 12 | expect(GoogleCloudStorageMock, :upload, fn "test_key", "test_content" -> {:ok, :uploaded} end) 13 | 14 | task = %{type: :upload, media: %{id: "test_id", storage_key: "test_key"}, content: "test_content"} 15 | @subject.handle_events([task], nil, nil) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/templates/file/new.html.eex: -------------------------------------------------------------------------------- 1 |

Upload a File

2 | <%= form_for @changeset, file_path(@conn, :create), [multipart: true, as: :file], fn f -> %> 3 |
4 | <%= text_input f, :file_name, placeholder: "File Name", class: "form-control" %> 5 | <%= error_tag f, :file_name %> 6 |
7 |
8 | <%= text_input f, :full_path, value: @path, placeholder: "Full Path", class: "form-control" %> 9 | <%= error_tag f, :full_path %> 10 |
11 |
12 | 13 | <%= file_input f, :file, class: "form-control" %> 14 |
15 | <%= submit("Upload", class: "btn btn-primary") %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /Chapter02/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Umbrella.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | start_permanent: Mix.env() == :prod, 8 | deps: deps(), 9 | aliases: aliases() 10 | ] 11 | end 12 | 13 | # Dependencies listed here are available only for this 14 | # project and cannot be accessed from applications inside 15 | # the apps folder. 16 | # 17 | # Run "mix help deps" for examples and options. 18 | defp deps do 19 | [ 20 | {:distillery, "~> 1.5", runtime: false}, 21 | {:credo, "~> 0.3", only: [:dev, :test], runtime: false} 22 | ] 23 | end 24 | 25 | defp aliases do 26 | [ 27 | lint: ["format", "credo"] 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/.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 | # Ignore package tarball (built via "mix hex.build"). 23 | simple_elixir_drip-*.tar 24 | 25 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_palindrome_with_pipe_operator.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | defguard is_string(term) when is_bitstring(term) 3 | 4 | def palindrome?(term) do 5 | formatted_term = term 6 | |> String.trim() 7 | |> String.downcase() 8 | formatted_term |> String.reverse() == formatted_term 9 | end 10 | def palindrome?(_term), do: {:error, :unsupported_type} 11 | 12 | def emphasize(phrase, number_of_marks \\ 3) 13 | def emphasize(_phrase, 0) do 14 | "This isn't the module you're looking for" 15 | end 16 | def emphasize(phrase, number_of_marks) do 17 | upcased_phrase = String.upcase(phrase) 18 | exclamation_marks = String.duplicate("!", number_of_marks) 19 | "#{upcased_phrase}#{exclamation_marks}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/controllers/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.FallbackController do 2 | use Phoenix.Controller 3 | import ElixirDripWeb.Router.Helpers, only: [file_path: 3] 4 | 5 | def call(conn, {:error, :invalid_path}) do 6 | conn 7 | |> put_flash(:error, "The path provided is invalid.") 8 | |> redirect(to: file_path(conn, :index, "path": "$")) 9 | end 10 | 11 | def call(conn, {:error, :not_found}) do 12 | conn 13 | |> put_flash(:error, "File not found.") 14 | |> redirect(to: file_path(conn, :index, "path": "$")) 15 | end 16 | 17 | def call(conn, _error) do 18 | conn 19 | |> put_flash(:error, "Unexpected error.") 20 | |> redirect(to: file_path(conn, :index, "path": "$")) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/api/controllers/file_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Api.FileController do 2 | use ElixirDripWeb, :controller 3 | 4 | alias ElixirDrip.Storage 5 | 6 | plug ElixirDripWeb.Plugs.Auth, :json 7 | 8 | def index(conn, %{"path" => path}) do 9 | user = conn.assigns.current_user 10 | with {:ok, media} <- Storage.media_by_folder(user.id, path) do 11 | render(conn, files: media.files, folders: media.folders) 12 | else 13 | {:error, :invalid_path} -> 14 | render(conn, ElixirDripWeb.ErrorView, "400.json", message: "The path provided is invalid.") 15 | end 16 | end 17 | 18 | def index(conn, _params), do: render(conn, ElixirDripWeb.ErrorView, "400.json", message: "Missing 'path' query parameter") 19 | end 20 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | 16 | // Import local files 17 | // 18 | // Local files can be imported directly using relative 19 | // paths "./socket" or full ones "web/static/js/socket". 20 | 21 | // import socket from "./socket" 22 | -------------------------------------------------------------------------------- /Chapter01/functions_and_modules/string_helper_with_module_attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule StringHelper do 2 | @default_mark "!" 3 | 4 | defguard is_string(term) when is_bitstring(term) 5 | 6 | def palindrome?(term) do 7 | formatted_term = term 8 | |> String.trim() 9 | |> String.downcase() 10 | formatted_term |> String.reverse() == formatted_term 11 | end 12 | def palindrome?(_term), do: {:error, :unsupported_type} 13 | 14 | def emphasize(phrase, number_of_marks \\ 3) 15 | def emphasize(_phrase, 0) do 16 | "This isn't the module you're looking for" 17 | end 18 | def emphasize(phrase, number_of_marks) do 19 | upcased_phrase = String.upcase(phrase) 20 | exclamation_marks = String.duplicate("!", number_of_marks) 21 | "#{upcased_phrase}#{exclamation_marks}" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/encryption_with_streamlined_child_specs.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Encryption do 2 | require Logger 3 | 4 | def child_spec(_) do 5 | %{ 6 | id: __MODULE__, 7 | start: {__MODULE__, :start_link, []}, 8 | restart: :transient, 9 | shutdown: 10000, 10 | type: :worker 11 | } 12 | end 13 | 14 | def start_link() do 15 | pid = spawn_link(fn -> 16 | Logger.debug("#{inspect(self())} Starting the Encryption module...") 17 | loop() 18 | end) 19 | 20 | {:ok, pid} 21 | end 22 | 23 | defp loop() do 24 | receive do 25 | message -> IO.puts "#{inspect(self())} Encryption module received a message: #{message}" 26 | end 27 | 28 | loop() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "phoenix_html" 15 | import socket from "./socket" 16 | 17 | import Notification from "./notification" 18 | import OnlineUsers from "./online_users" 19 | 20 | Notification.init(socket, window.userId) 21 | OnlineUsers.init(socket) 22 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip/encryption.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.Storage.Pipeline.Encryption do 2 | require Logger 3 | 4 | def child_spec(_) do 5 | %{ 6 | id: __MODULE__, 7 | start: {__MODULE__, :start_link, []}, 8 | restart: :transient, 9 | shutdown: 10000, 10 | type: :worker 11 | } 12 | end 13 | 14 | def start_link() do 15 | pid = spawn_link(fn -> 16 | Logger.debug("#{inspect(self())} Starting the Encryption module...") 17 | loop() 18 | end) 19 | 20 | {:ok, pid} 21 | end 22 | 23 | defp loop() do 24 | receive do 25 | message -> IO.puts "#{inspect(self())} Encryption module received a message: #{message}" 26 | end 27 | 28 | loop() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :simple_elixir_drip, 7 | version: "0.1.0", 8 | elixir: "~> 1.6", 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: {SimpleElixirDrip.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 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/api/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Api.SessionController do 2 | use ElixirDripWeb, :controller 3 | 4 | alias ElixirDrip.Accounts 5 | alias ElixirDripWeb.Plugs.Auth 6 | 7 | def create(conn, %{"username" => username, "password" => password}) do 8 | with {:ok, user} <- Accounts.login_user_with_pw(username, password) do 9 | conn 10 | |> Auth.login(user) 11 | |> render("login.json", user: user) 12 | else 13 | _ -> 14 | conn 15 | |> put_status(:unauthorized) 16 | |> render(ElixirDripWeb.ErrorView, "401.json", message: "Invalid username/password combination.") 17 | |> halt() 18 | end 19 | end 20 | 21 | def delete(conn, _params) do 22 | conn 23 | |> Auth.logout() 24 | |> render("logout.json") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/README.md: -------------------------------------------------------------------------------- 1 | # ElixirDripWeb 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Install Node.js dependencies with `cd assets && npm install` 8 | * Start Phoenix endpoint with `mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: http://phoenixframework.org/docs/overview 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.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 ElixirDripWeb.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, otp_app: :elixir_drip_web 24 | end 25 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip_web/test/elixir_drip_web/channels/user_socket_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.UserSocketTest do 2 | use ElixirDripWeb.ChannelCase, async: true 3 | 4 | @subject ElixirDripWeb.UserSocket 5 | 6 | describe "with a valid token" do 7 | test "it connects to the socket" do 8 | token = Phoenix.Token.sign(@endpoint, "user socket auth", "13") 9 | 10 | assert {:ok, socket} = connect(@subject, %{"token" => token}) 11 | assert socket.assigns.user_id == "13" 12 | end 13 | end 14 | 15 | describe "with an invalid token" do 16 | test "it doesn't connect to the socket" do 17 | assert :error == connect(@subject, %{"token" => "010101"}) 18 | end 19 | end 20 | 21 | describe "without providing a token" do 22 | test "it doesn't connect to the socket" do 23 | assert :error == connect(@subject, %{}) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Chapter01/pattern_matching/iex_examples.txt: -------------------------------------------------------------------------------- 1 | x = 3 2 | x * 3 3 | 3 = x 4 | 2 = x 5 | {number, representation} = {3.1415, "π"} 6 | number 7 | representation 8 | a = 1 9 | a = 7 10 | {a, a} = {3, 3} 11 | {a, a} = {2, 3} 12 | {3.1415, representation} = {3.1415, "π"} 13 | representation 14 | [first, second, third] = ["α", "β", "γ"] 15 | first 16 | second 17 | third 18 | [first, _, third] = ["δ", "ε", "ζ"] 19 | first 20 | third 21 | _ 22 | [first | rest_of_list] = ["α", "β", "γ"] 23 | first 24 | rest_of_list 25 | %{"name" => name, "age" => age} = %{"name" => "Gabriel", "age" => 1} 26 | name 27 | age 28 | name = "Gabriel" 29 | %{name: ^name, age: age} = %{name: "Gabriel", age: 1} 30 | %{name: ^name, age: age} = %{name: "Jose", age: 1} 31 | <> = <<100, 200>> 32 | first_byte 33 | second_byte 34 | <> = "YZ" 35 | first_byte 36 | second_byte 37 | x = y = 100 38 | x 39 | y 40 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/iex_examples.txt: -------------------------------------------------------------------------------- 1 | c("encryption.ex") 2 | c("upload_pipeline_supervisor.ex") 3 | {:ok, supervisor_pid} = ElixirDrip.Storage.Supervisors.Upload.Pipeline.start_link() 4 | send(pid(0, 98, 0), "some message") 5 | Process.exit(pid(0, 98, 0), :kill) 6 | send(pid(0, 102, 0), "some message") 7 | c("cache_worker_under_supervisor.ex") 8 | c("cache_supervisor.ex") 9 | ElixirDrip.Storage.Supervisors.CacheSupervisor.start_link() 10 | ElixirDrip.Storage.Supervisors.CacheSupervisor.put("abc", "def") 11 | ElixirDrip.Storage.Supervisors.CacheSupervisor.get("abc") 12 | ElixirDrip.Storage.Supervisors.CacheSupervisor.get("xyz") 13 | ElixirDrip.Storage.Supervisors.CacheSupervisor.get("abc") 14 | ElixirDrip.Storage.Supervisors.CacheSupervisor.put("abc", "def") 15 | Process.exit(pid(0, 104, 0), :kill) 16 | ElixirDrip.Storage.Supervisors.CacheSupervisor.get("abc") 17 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/elixir_drip/storage/pipeline/media_property_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.MediaPropertyTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | 5 | @subject ElixirDrip.Storage.Media 6 | 7 | property "is_valid_path?/1 with valid paths returns {:ok, :valid}" do 8 | check all path <- string(:ascii), 9 | String.last(path) != "/" do 10 | assert @subject.is_valid_path?("$" <> path) == {:ok, :valid} 11 | end 12 | end 13 | 14 | property "is_valid_path?/1 with invalid paths returns {:error, :invalid_path}" do 15 | check all path <- string(:ascii), 16 | String.first(path) != "$" do 17 | assert @subject.is_valid_path?(path) == {:error, :invalid_path} 18 | end 19 | 20 | check all path <- string(:ascii) do 21 | assert @subject.is_valid_path?(path <> "/") == {:error, :invalid_path} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/plugs/fetch_user.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Plugs.FetchUser do 2 | import Plug.Conn 3 | 4 | alias ElixirDrip.Accounts 5 | 6 | def init(options), do: options 7 | 8 | def call(conn, _options) do 9 | user_id = get_session(conn, :user_id) 10 | find_user(conn, user_id) 11 | end 12 | 13 | defp find_user(conn, nil), do: conn 14 | defp find_user(conn, user_id) do 15 | user = Accounts.find_user(user_id) 16 | assign_current_user(conn, user) 17 | end 18 | 19 | defp assign_current_user(conn, nil), do: conn 20 | defp assign_current_user(conn, user) do 21 | token = generate_token(conn, user.id) 22 | 23 | conn 24 | |> assign(:user_id, user.id) 25 | |> assign(:user_token, token) 26 | |> assign(:current_user, user) 27 | end 28 | 29 | defp generate_token(conn, user_id) do 30 | Phoenix.Token.sign(conn, "user socket auth", user_id) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.SessionController do 2 | use ElixirDripWeb, :controller 3 | 4 | alias ElixirDrip.Accounts 5 | alias ElixirDripWeb.Plugs.Auth 6 | 7 | def new(conn, _params), do: render(conn) 8 | 9 | def create(conn, %{"username" => username, "password" => password}) do 10 | with {:ok, user} <- Accounts.login_user_with_pw(username, password) do 11 | conn 12 | |> Auth.login(user) 13 | |> put_flash(:info, "#{user.username}, you're now logged in. Welcome back!") 14 | |> redirect(to: file_path(conn, :index)) 15 | else 16 | :error -> 17 | conn 18 | |> put_flash(:error, "Invalid username/password combination.") 19 | |> render(:new) 20 | end 21 | end 22 | 23 | def delete(conn, _params) do 24 | conn 25 | |> Auth.logout() 26 | |> redirect(to: page_path(conn, :index)) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /Chapter04/apps/elixir_drip/lib/elixir_drip/storage/workers/agent_cache_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Workers.AgentCacheWorker do 2 | use Agent 3 | require Logger 4 | alias ElixirDrip.Storage.Workers.CacheWorker 5 | 6 | def start_link(media_id, content) do 7 | Agent.start_link(fn -> 8 | %{hits: 0, content: content} 9 | end, name: CacheWorker.name_for(media_id)) 10 | end 11 | 12 | def handle_info(message, state) do 13 | Logger.debug("#{inspect(self())} Just got an OOB message: #{message}") 14 | 15 | {:noreply, state} 16 | end 17 | 18 | def get_media(pid) do 19 | Agent.get_and_update(pid, 20 | fn (%{hits: hits, content: content} = state) -> 21 | Logger.debug("#{inspect(self())}: Received :get_media and served #{byte_size(content)} bytes #{hits+1} times.") 22 | {state.content, %{state | hits: hits+1}} 23 | end) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip/lib/elixir_drip/storage/pipeline/notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Notifier do 2 | @moduledoc false 3 | 4 | require Logger 5 | 6 | @dummy_state [] 7 | 8 | use ElixirDrip.Pipeliner.Consumer, type: :consumer 9 | 10 | @impl ElixirDrip.Pipeliner.Consumer 11 | def prepare_state([]) do 12 | Logger.debug("#{inspect(self())}: Streamlined Pipeline Notifier started.") 13 | 14 | @dummy_state 15 | end 16 | 17 | @impl GenStage 18 | def handle_events(tasks, _from, _state) do 19 | Enum.each(tasks, ¬ify_step(&1)) 20 | 21 | {:noreply, [], @dummy_state} 22 | end 23 | 24 | defp notify_step(%{media: %{id: id}, user_id: user_id, type: :download}) do 25 | ElixirDripWeb.Notifications.notify(:download, id, user_id) 26 | end 27 | 28 | defp notify_step(%{media: %{file_name: file_name}, user_id: user_id, type: :upload}) do 29 | ElixirDripWeb.Notifications.notify(:upload, file_name, user_id) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | import Supervisor.Spec 7 | 8 | # Define workers and child supervisors to be supervised 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(ElixirDripWeb.Endpoint, []) 12 | # Start your own worker by calling: ElixirDripWeb.Worker.start_link(arg1, arg2, arg3) 13 | # worker(ElixirDripWeb.Worker, [arg1, arg2, arg3]), 14 | ] 15 | 16 | # See https://hexdocs.pm/elixir/Supervisor.html 17 | # for other strategies and supported options 18 | opts = [strategy: :one_for_one, name: ElixirDripWeb.Supervisor] 19 | Supervisor.start_link(children, opts) 20 | end 21 | 22 | # Tell Phoenix to update the endpoint configuration 23 | # whenever the application is updated. 24 | def config_change(changed, _new, removed) do 25 | ElixirDripWeb.Endpoint.config_change(changed, removed) 26 | :ok 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :elixir_drip_web, 10 | namespace: ElixirDripWeb, 11 | ecto_repos: [ElixirDrip.Repo] 12 | 13 | # Configures the endpoint 14 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: "ez09L8wb/jXopY7jtFRlbEB8zb0vNAK4gfnUdwgA5kp5DUVZsz3QwxdnMs4f9cbq", 17 | render_errors: [view: ElixirDripWeb.ErrorView, accepts: ~w(html json)], 18 | pubsub: [name: ElixirDripWeb.PubSub, adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | # Import environment specific config. This must remain at the bottom 26 | # of this file so it overrides the configuration defined above. 27 | import_config "#{Mix.env()}.exs" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint ElixirDripWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ElixirDrip.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/plugs/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Plugs.Auth do 2 | import Plug.Conn 3 | import Phoenix.Controller, only: [put_flash: 3, redirect: 2, render: 4] 4 | import ElixirDripWeb.Router.Helpers, only: [page_path: 2] 5 | 6 | def init(format), do: format 7 | 8 | def call(conn, format) do 9 | case conn.assigns[:current_user] do 10 | nil -> logged_out(conn, format) 11 | _ -> conn 12 | end 13 | end 14 | 15 | def login(conn, user) do 16 | conn 17 | |> put_session(:user_id, user.id) 18 | |> assign(:current_user, user) 19 | |> configure_session(renew: true) 20 | end 21 | 22 | def logout(conn), do: configure_session(conn, drop: true) 23 | 24 | defp logged_out(conn, :html) do 25 | conn 26 | |> put_flash(:error, "You need to be logged in to view this content.") 27 | |> halt() 28 | |> redirect(to: page_path(conn, :index)) 29 | end 30 | 31 | defp logged_out(conn, :json) do 32 | conn 33 | |> put_status(:unauthorized) 34 | |> halt() 35 | |> render(ElixirDripWeb.ErrorView, "401.json", message: "Unauthenticated user.") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/notifications/notifications.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Notifications do 2 | use GenServer 3 | import ElixirDripWeb.Router.Helpers, only: [file_path: 3] 4 | alias ElixirDripWeb.Endpoint 5 | 6 | def start_link do 7 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | def init(:ok) do 11 | {:ok, []} 12 | end 13 | 14 | def notify(:upload, file_name, user_id) do 15 | GenServer.cast(__MODULE__, {:upload, file_name, user_id}) 16 | end 17 | 18 | def notify(:download, id, user_id) do 19 | GenServer.cast(__MODULE__, {:download, id, user_id}) 20 | end 21 | 22 | def handle_cast({:upload, file_name, user_id}, state) do 23 | Endpoint.broadcast("users:#{user_id}", "upload", %{message: "The file \"#{file_name}\" was successfully uploaded."}) 24 | 25 | {:noreply, state} 26 | end 27 | 28 | def handle_cast({:download, id, user_id}, state) do 29 | link = file_path(Endpoint, :show, id) <> "/download" 30 | Endpoint.broadcast("users:#{user_id}", "download", %{link: link, message: "Your download is ready. Start it by clicking here."}) 31 | {:noreply, state} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :elixir_drip_web, 10 | namespace: ElixirDripWeb, 11 | ecto_repos: [ElixirDrip.Repo] 12 | 13 | # Configures the endpoint 14 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: "ez09L8wb/jXopY7jtFRlbEB8zb0vNAK4gfnUdwgA5kp5DUVZsz3QwxdnMs4f9cbq", 17 | render_errors: [view: ElixirDripWeb.ErrorView, accepts: ~w(html json)], 18 | pubsub: [name: ElixirDripWeb.PubSub, adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | config :elixir_drip_web, :generators, context_app: false 26 | 27 | # Import environment specific config. This must remain at the bottom 28 | # of this file so it overrides the configuration defined above. 29 | import_config "#{Mix.env()}.exs" 30 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

A productive web framework that
does not compromise speed and maintainability.

4 |
5 | 6 |
7 |
8 |

Resources

9 | 20 |
21 | 22 |
23 |

Help

24 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/storage/pipeline/notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Notifier do 2 | @moduledoc false 3 | 4 | require Logger 5 | 6 | @dummy_state [] 7 | 8 | use ElixirDrip.Pipeliner.Consumer, type: :consumer 9 | 10 | @impl ElixirDrip.Pipeliner.Consumer 11 | def prepare_state([]) do 12 | Logger.debug("#{inspect(self())}: Streamlined Pipeline Notifier started.") 13 | 14 | @dummy_state 15 | end 16 | 17 | @impl GenStage 18 | def handle_events(tasks, _from, _state) do 19 | Enum.each(tasks, ¬ify_step(&1)) 20 | 21 | {:noreply, [], @dummy_state} 22 | end 23 | 24 | defp notify_step(%{media: media, content: content, type: :upload}) do 25 | # TODO: Invoke the notifier instead! 26 | Logger.debug("#{inspect(self())}: NOTIFICATION! Uploaded media #{media.id} to #{media.storage_key} with size: #{byte_size(content)} bytes.") 27 | end 28 | 29 | defp notify_step(%{media: %{id: id}, content: content, type: :download}) do 30 | # TODO: Invoke the notifier instead! 31 | Logger.debug("#{inspect(self())}: NOTIFICATION! Downloaded media #{id}, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/elixir_drip/storage/storage_repo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.StorageRepoTest do 2 | use ExUnit.Case, async: true 3 | 4 | @moduletag :repository 5 | 6 | @subject ElixirDrip.Storage 7 | 8 | setup do 9 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ElixirDrip.Repo) 10 | end 11 | 12 | describe "when the user already uploaded some media" do 13 | setup [:create_user, :create_media] 14 | 15 | test "finds the media by ID", %{user: user, media: media} do 16 | {:ok, :download_enqueued, retrieved_media} = @subject.retrieve(user.id, media.id) 17 | 18 | expected_media_id = media.id 19 | assert %{full_path: "$", id: ^expected_media_id, name: "test.txt"} = retrieved_media 20 | end 21 | end 22 | 23 | defp create_user(_context) do 24 | {:ok, user} = 25 | ElixirDrip.Accounts.create_user(%{ 26 | username: "test", 27 | password: "12345678", 28 | email: "test@test.com" 29 | }) 30 | 31 | {:ok, user: user} 32 | end 33 | 34 | defp create_media(%{user: user}) do 35 | {:ok, :upload_enqueued, media} = @subject.store(user.id, "test.txt", "$", "some cool content") 36 | {:ok, media: media} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/dsl/pipeliner_producer.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Pipeliner.Producer do 2 | import ElixirDrip.Pipeliner 3 | 4 | @callback prepare_state(list(any)) :: any 5 | 6 | defmacro __using__(opts) do 7 | args = get_or_default(opts, :args, []) 8 | 9 | optional_args = create_args(__MODULE__, args) 10 | required_args = create_args(__MODULE__, [:name]) 11 | 12 | function_args = optional_args ++ required_args 13 | 14 | quote do 15 | use GenStage 16 | import unquote(__MODULE__) 17 | @behaviour unquote(__MODULE__) 18 | @behaviour GenStage 19 | 20 | def start_link(unquote_splicing(function_args)) do 21 | GenStage.start_link(__MODULE__, unquote(optional_args), name: name) 22 | end 23 | 24 | @impl GenStage 25 | def init([unquote_splicing(optional_args)]) do 26 | args = prepare_state(unquote(optional_args)) 27 | 28 | {:producer, args} 29 | end 30 | 31 | def prepare_state(args), do: args 32 | 33 | defoverridable unquote(__MODULE__) 34 | end 35 | end 36 | 37 | defp create_args(_, []), do: [] 38 | defp create_args(module, arg_names), 39 | do: Enum.map(arg_names, &Macro.var(&1, module)) 40 | end 41 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.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 datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import ElixirDripWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint ElixirDripWeb.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ElixirDrip.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, {:shared, self()}) 34 | end 35 | 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Chapter11/code/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | services: 3 | prometheus: 4 | build: 5 | context: . 6 | dockerfile: monitoring/Dockerfile.prometheus 7 | depends_on: 8 | app: 9 | condition: service_started 10 | ports: 11 | - 9090:9090 12 | app: 13 | image: elixir_drip:dev 14 | build: 15 | context: . 16 | dockerfile: Dockerfile.dev 17 | env_file: ./env/dev.env 18 | environment: 19 | SECRET_KEY_BASE: 20 | GOOGLE_STORAGE_CREDENTIALS: 21 | ports: 22 | - 4000:4000 23 | - 4369 24 | - 9001-9004 25 | command: sh /opt/app/elixir_drip/script/start.sh 26 | depends_on: 27 | postgres: 28 | condition: service_healthy 29 | networks: 30 | - default 31 | volumes: 32 | - .:/opt/app/elixir_drip 33 | postgres: 34 | environment: 35 | POSTGRES_USER: postgres 36 | POSTGRES_PASSWORD: "123456" 37 | PSQL_TRUST_LOCALNET: 'true' 38 | ENCODING: UTF8 39 | image: postgres:9.6 40 | healthcheck: 41 | test: ["CMD", "pg_isready", "-d", "postgres", "-U", "postgres"] 42 | interval: 10s 43 | timeout: 3s 44 | retries: 10 45 | ports: 46 | - 5000:5432 47 | networks: 48 | - default 49 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/accounts/user.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Accounts.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | alias __MODULE__ 6 | 7 | @email_regex ~r/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i 8 | 9 | @primary_key {:id, ElixirDrip.Ecto.Ksuid, autogenerate: true} 10 | schema "users" do 11 | field :username, :string 12 | field :hashed_password, :string 13 | field :password, :string, virtual: true 14 | field :email, :string 15 | 16 | timestamps() 17 | end 18 | 19 | @doc false 20 | def create_changeset(%User{} = user, attrs) do 21 | user 22 | |> cast(attrs, [:username, :password, :email]) 23 | |> validate_required([:username, :password, :email]) 24 | |> validate_length(:username, min: 1, max: 30) 25 | |> validate_length(:password, min: 8, max: 100) 26 | |> validate_format(:email, @email_regex) 27 | |> unique_constraint(:username) 28 | |> put_hashed_password() 29 | end 30 | 31 | defp put_hashed_password(%Ecto.Changeset{valid?: false} = changeset), do: changeset 32 | defp put_hashed_password(%Ecto.Changeset{valid?: true, changes: %{password: pw}} = changeset) do 33 | changeset 34 | |> put_change(:hashed_password, Comeonin.Bcrypt.hashpwsalt(pw)) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /Chapter04/apps/elixir_drip/lib/elixir_drip/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Application do 2 | @moduledoc """ 3 | The ElixirDrip Application Service. 4 | 5 | The elixir_drip system business domain lives in this application. 6 | 7 | Exposes API to clients such as the `ElixirDripWeb` application 8 | for use in channels, controllers, and elsewhere. 9 | """ 10 | use Application 11 | 12 | alias ElixirDrip.Storage.{ 13 | Supervisors.CacheSupervisor, 14 | Workers.QueueWorker 15 | } 16 | alias ElixirDrip.Storage.Supervisors.Download.Pipeline, as: DownloadPipeline 17 | alias ElixirDrip.Storage.Supervisors.Upload.Pipeline, as: UploadPipeline 18 | 19 | def start(_type, _args) do 20 | import Supervisor.Spec, warn: false 21 | 22 | Supervisor.start_link( 23 | [ 24 | supervisor(ElixirDrip.Repo, []), 25 | supervisor(CacheSupervisor, [], name: CacheSupervisor), 26 | worker(QueueWorker, [:download], id: :download_queue, restart: :permanent), 27 | worker(QueueWorker, [:upload], id: :upload_queue, restart: :permanent), 28 | supervisor(DownloadPipeline, []), 29 | supervisor(UploadPipeline, []), 30 | ], 31 | strategy: :one_for_one, 32 | name: ElixirDrip.Supervisor 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/pipeline/notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Notifier do 2 | @moduledoc false 3 | 4 | use GenStage 5 | require Logger 6 | 7 | @dummy_state [] 8 | 9 | def start_link(name, subscription_options), do: 10 | GenStage.start_link(__MODULE__, subscription_options, name: name) 11 | 12 | def init(subscription_options) do 13 | Logger.debug("#{inspect(self())}: Pipeline Notifier started. Options: #{inspect(subscription_options)}") 14 | 15 | {:consumer, @dummy_state, subscription_options} 16 | end 17 | 18 | def handle_events(tasks, _from, _state) do 19 | Enum.each(tasks, ¬ify_step(&1)) 20 | 21 | {:noreply, [], @dummy_state} 22 | end 23 | 24 | defp notify_step(%{media: media, content: content, type: :upload}) do 25 | # TODO: Invoke the notifier instead! 26 | Logger.debug("#{inspect(self())}: NOTIFICATION! Uploaded media #{media.id} to #{media.storage_key} with size: #{byte_size(content)} bytes.") 27 | end 28 | 29 | defp notify_step(%{media: %{id: id}, content: content, type: :download}) do 30 | # TODO: Invoke the notifier instead! 31 | Logger.debug("#{inspect(self())}: NOTIFICATION! Downloaded media #{id}, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello ElixirDripWeb! 11 | "> 12 | 13 | 14 | 15 |
16 |
17 | 22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | <%= render @view_module, @view_template, assigns %> 30 |
31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/lib/elixir_drip_web/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | import Supervisor.Spec 7 | 8 | # Define workers and child supervisors to be supervised 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(ElixirDripWeb.Endpoint, []), 12 | supervisor(ElixirDripWeb.Presence, []), 13 | worker(ElixirDripWeb.Notifications, []) 14 | # Start your own worker by calling: ElixirDripWeb.Worker.start_link(arg1, arg2, arg3) 15 | # worker(ElixirDripWeb.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | ElixirDripWeb.EndpointInstrumenter.setup() 19 | ElixirDripWeb.PlugInstrumenter.setup() 20 | ElixirDripWeb.MetricsExporter.setup() 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: ElixirDripWeb.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 | def config_change(changed, _new, removed) do 31 | ElixirDripWeb.Endpoint.config_change(changed, removed) 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Router do 2 | use ElixirDripWeb, :router 3 | 4 | alias ElixirDripWeb.Plugs.FetchUser 5 | 6 | pipeline :browser do 7 | plug(:accepts, ["html"]) 8 | plug(:fetch_session) 9 | plug(:fetch_flash) 10 | plug(:protect_from_forgery) 11 | plug(:put_secure_browser_headers) 12 | plug(FetchUser) 13 | end 14 | 15 | pipeline :api do 16 | plug(:accepts, ["json"]) 17 | plug(:fetch_session) 18 | plug(FetchUser) 19 | end 20 | 21 | scope "/", ElixirDripWeb do 22 | pipe_through(:browser) 23 | 24 | resources("/files", FileController, only: [:index, :new, :create, :show]) 25 | get("/files/:id/download", FileController, :download) 26 | post("/files/:id/download", FileController, :enqueue_download) 27 | 28 | resources("/users", UserController, only: [:new, :create]) 29 | 30 | resources("/sessions", SessionController, only: [:new, :create]) 31 | delete("/sessions", SessionController, :delete) 32 | 33 | get("/", PageController, :index) 34 | end 35 | 36 | scope "/api", ElixirDripWeb.Api, as: :api do 37 | pipe_through(:api) 38 | 39 | get("/files", FileController, :index) 40 | 41 | post("/sessions", SessionController, :create) 42 | delete("/sessions", SessionController, :delete) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :simple_elixir_drip, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:simple_elixir_drip, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", ElixirDripWeb.RoomChannel 6 | 7 | ## Transports 8 | transport(:websocket, Phoenix.Transports.WebSocket) 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # ElixirDripWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/lib/elixir_drip/ecto/ksuid.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Ecto.Ksuid do 2 | @moduledoc """ 3 | A module implementing ksuid as an ecto type. 4 | 5 | To enable ksuid as the primary key type for ecto schemas add this annotation 6 | on top of the schema definition: 7 | 8 | @primary_key {:id, Motus.Ecto.Ksuid, autogenerate: true} 9 | schema "my_schema" do 10 | (...) 11 | end 12 | 13 | To use it as the type of any other schema field do the following: 14 | 15 | schema "my_schema" do 16 | (...) 17 | field :my_ksuid_field, Motus.Ecto.Ksuid 18 | (...) 19 | end 20 | 21 | More info about ksuid may be found [here](https://segment.com/blog/a-brief-history-of-the-uuid/). 22 | The elixir library used can be found [here](https://github.com/girishramnani/elixir-ksuid). 23 | """ 24 | 25 | @behaviour Ecto.Type 26 | 27 | def type, do: :string 28 | 29 | def cast(ksuid) when is_binary(ksuid), do: {:ok, ksuid} 30 | def cast(_), do: :error 31 | 32 | def cast!(value) do 33 | case cast(value) do 34 | {:ok, ksuid} -> ksuid 35 | :error -> raise Ecto.CastError, type: __MODULE__, value: value 36 | end 37 | end 38 | 39 | def load(ksuid), do: {:ok, ksuid} 40 | 41 | def dump(binary) when is_binary(binary), do: {:ok, binary} 42 | def dump(_), do: :error 43 | 44 | def autogenerate, do: Ksuid.generate() 45 | end 46 | -------------------------------------------------------------------------------- /Chapter02/rel/config.exs: -------------------------------------------------------------------------------- 1 | Path.join(["rel", "plugins", "*.exs"]) 2 | |> Path.wildcard() 3 | |> Enum.map(&Code.eval_file(&1)) 4 | 5 | use Mix.Releases.Config, 6 | # This sets the default release built by `mix release` 7 | default_release: :default, 8 | # This sets the default environment used by `mix release` 9 | default_environment: Mix.env() 10 | 11 | environment :dev do 12 | # If you are running Phoenix, you should make sure that 13 | # server: true is set and the code reloader is disabled, 14 | # even in dev mode. 15 | # It is recommended that you build with MIX_ENV=prod and pass 16 | # the --env flag to Distillery explicitly if you want to use 17 | # dev mode. 18 | set dev_mode: true 19 | set include_erts: false 20 | set cookie: :"L9pTCob/l<)0&WTOFkCjg>OOVLw&HZivG;4=((5THJltA0V[a>.|dQ?Q*}D,q3.,;*`x)D`<~&4(:n.8~oGV.YDikP=8" 27 | end 28 | 29 | # You may define one or more releases in this file. 30 | # If you have not set a default release, or selected one 31 | # when running `mix release`, the first release in the file 32 | # will be used by default 33 | 34 | release :umbrella_minefield do 35 | set version: "0.1.0" 36 | set applications: [ 37 | :runtime_tools, 38 | elixir_drip: :permanent, 39 | elixir_drip_web: :permanent 40 | ] 41 | end 42 | 43 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/ecto/ksuid.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Ecto.Ksuid do 2 | @moduledoc """ 3 | A module implementing ksuid as an ecto type. 4 | 5 | To enable ksuid as the primary key type for ecto schemas add this annotation 6 | on top of the schema definition: 7 | 8 | @primary_key {:id, ElixirDrip.Ecto.Ksuid, autogenerate: true} 9 | schema "my_schema" do 10 | (...) 11 | end 12 | 13 | To use it as the type of any other schema field do the following: 14 | 15 | schema "my_schema" do 16 | (...) 17 | field :my_ksuid_field, ElixirDrip.Ecto.Ksuid 18 | (...) 19 | end 20 | 21 | More info about ksuid may be found [here](https://segment.com/blog/a-brief-history-of-the-uuid/). 22 | The elixir library used can be found [here](https://github.com/girishramnani/elixir-ksuid). 23 | """ 24 | 25 | @behaviour Ecto.Type 26 | 27 | def type, do: :string 28 | 29 | def cast(ksuid) 30 | when is_binary(ksuid) and byte_size(ksuid) == 27, do: {:ok, ksuid} 31 | def cast(_), do: :error 32 | 33 | def cast!(value) do 34 | case cast(value) do 35 | {:ok, ksuid} -> ksuid 36 | :error -> raise Ecto.CastError, type: __MODULE__, value: value 37 | end 38 | end 39 | 40 | def load(ksuid), do: {:ok, ksuid} 41 | 42 | def dump(binary) when is_binary(binary), do: {:ok, binary} 43 | def dump(_), do: :error 44 | 45 | def autogenerate, do: Ksuid.generate() 46 | end 47 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/lib/elixir_drip/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Application do 2 | @moduledoc """ 3 | The ElixirDrip Application Service. 4 | 5 | The elixir_drip system business domain lives in this application. 6 | 7 | Exposes API to clients such as the `ElixirDripWeb` application 8 | for use in channels, controllers, and elsewhere. 9 | """ 10 | use Application 11 | 12 | alias ElixirDrip.Storage.{ 13 | Supervisors.CacheSupervisor, 14 | Workers.QueueWorker 15 | } 16 | # alias ElixirDrip.Storage.Supervisors.Download.Pipeline, as: DownloadPipeline 17 | # alias ElixirDrip.Storage.Supervisors.Upload.Pipeline, as: UploadPipeline 18 | alias ElixirDrip.Storage.Supervisors.Download.StreamlinedPipeline, as: DownloadPipeline 19 | alias ElixirDrip.Storage.Supervisors.Upload.StreamlinedPipeline, as: UploadPipeline 20 | 21 | def start(_type, _args) do 22 | import Supervisor.Spec, warn: false 23 | 24 | Supervisor.start_link( 25 | [ 26 | supervisor(ElixirDrip.Repo, []), 27 | supervisor(CacheSupervisor, [], name: CacheSupervisor), 28 | worker(QueueWorker, [:download], id: :download_queue, restart: :permanent), 29 | worker(QueueWorker, [:upload], id: :upload_queue, restart: :permanent), 30 | supervisor(DownloadPipeline, []), 31 | supervisor(UploadPipeline, []), 32 | ], 33 | strategy: :one_for_one, 34 | name: ElixirDrip.Supervisor 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), class: "help-block") 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(ElixirDripWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(ElixirDripWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /Chapter04/apps/elixir_drip/lib/elixir_drip/storage/supervisors/cache_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.CacheSupervisor do 2 | @behaviour ElixirDrip.Behaviours.CacheSupervisor 3 | 4 | use DynamicSupervisor 5 | alias ElixirDrip.Storage.Workers.CacheWorker 6 | 7 | def start_link() do 8 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 9 | end 10 | 11 | def init(_arg) do 12 | DynamicSupervisor.init(strategy: :one_for_one) 13 | end 14 | 15 | defp cache_worker_spec(id, content) do 16 | Supervisor.child_spec( 17 | CacheWorker, 18 | start: {CacheWorker, :start_link, [id, content]}, 19 | restart: :temporary 20 | ) 21 | end 22 | 23 | def put(id, content) when is_binary(id) and is_bitstring(content), 24 | do: DynamicSupervisor.start_child(__MODULE__, cache_worker_spec(id, content)) 25 | 26 | def refresh(id) when is_binary(id) do 27 | case find_cache(id) do 28 | nil -> nil 29 | pid -> CacheWorker.refresh(pid) 30 | end 31 | end 32 | 33 | def put_or_refresh(id, content) when is_binary(id) and is_bitstring(content) do 34 | case refresh(id) do 35 | nil -> put(id, content) 36 | result -> result 37 | end 38 | end 39 | 40 | def get(id) when is_binary(id) do 41 | case find_cache(id) do 42 | nil -> nil 43 | pid -> CacheWorker.get_media(pid) 44 | end 45 | end 46 | 47 | def find_cache(id) when is_binary(id) do 48 | GenServer.whereis(CacheWorker.name_for(id)) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip/lib/elixir_drip/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Application do 2 | @moduledoc """ 3 | The ElixirDrip Application Service. 4 | 5 | The elixir_drip system business domain lives in this application. 6 | 7 | Exposes API to clients such as the `ElixirDripWeb` application 8 | for use in channels, controllers, and elsewhere. 9 | """ 10 | use Application 11 | 12 | alias ElixirDrip.Storage.{ 13 | Supervisors.CacheSupervisor, 14 | Workers.QueueWorker 15 | } 16 | # alias ElixirDrip.Storage.Supervisors.Download.Pipeline, as: DownloadPipeline 17 | # alias ElixirDrip.Storage.Supervisors.Upload.Pipeline, as: UploadPipeline 18 | alias ElixirDrip.Storage.Supervisors.Download.StreamlinedPipeline, as: DownloadPipeline 19 | alias ElixirDrip.Storage.Supervisors.Upload.StreamlinedPipeline, as: UploadPipeline 20 | 21 | def start(_type, _args) do 22 | import Supervisor.Spec, warn: false 23 | 24 | ElixirDrip.RepoInstrumenter.setup() 25 | ElixirDrip.Instrumenter.setup() 26 | 27 | Supervisor.start_link( 28 | [ 29 | supervisor(ElixirDrip.Repo, []), 30 | supervisor(CacheSupervisor, [], name: CacheSupervisor), 31 | worker(QueueWorker, [:download], id: :download_queue, restart: :permanent), 32 | worker(QueueWorker, [:upload], id: :upload_queue, restart: :permanent), 33 | supervisor(DownloadPipeline, []), 34 | supervisor(UploadPipeline, []), 35 | ], 36 | strategy: :one_for_one, 37 | name: ElixirDrip.Supervisor 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip_web/test/elixir_drip_web/controllers/file_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.FileControllerTest do 2 | use ElixirDripWeb.ConnCase, async: true 3 | 4 | describe "when the user is authenticated" do 5 | setup [:create_user, :login_user, :create_media] 6 | 7 | test "GET /files lists the files for the current user", %{conn: conn, media: media} do 8 | response = get(conn, "/files?path=$") 9 | 10 | assert html_response(response, 200) =~ "test.txt" 11 | assert html_response(response, 200) =~ "/files/#{media.id}/download" 12 | end 13 | end 14 | 15 | describe "when the user is NOT authenticated" do 16 | setup [:create_user, :create_media] 17 | 18 | test "GET /files redirects the user to '/'", %{conn: conn} do 19 | response = get(conn, "/files?path=$") 20 | 21 | assert html_response(response, 302) =~ "You are being redirected" 22 | end 23 | end 24 | 25 | defp create_user(_context) do 26 | {:ok, user} = 27 | ElixirDrip.Accounts.create_user(%{ 28 | username: "test", 29 | password: "12345678", 30 | email: "test@test.com" 31 | }) 32 | 33 | {:ok, user: user} 34 | end 35 | 36 | defp login_user(%{conn: conn, user: user}) do 37 | conn = Plug.Test.init_test_session(conn, user_id: user.id) 38 | {:ok, conn: conn} 39 | end 40 | 41 | defp create_media(%{user: user}) do 42 | {:ok, :upload_enqueued, media} = 43 | ElixirDrip.Storage.store(user.id, "test.txt", "$", "some cool content") 44 | 45 | {:ok, media: media} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/cache_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.CacheSupervisor do 2 | use DynamicSupervisor 3 | require Logger 4 | alias ElixirDrip.Storage.Workers.CacheWorker 5 | 6 | def start_link() do 7 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 8 | end 9 | 10 | def init(_) do 11 | Logger.debug("#{inspect(self())} Starting the CacheSupervisor module...") 12 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 100) 13 | end 14 | 15 | def put(id, content) when is_binary(id) and is_bitstring(content) do 16 | case find_cache(id) do 17 | nil -> start_worker(id, content) 18 | pid -> {:ok, pid} 19 | end 20 | end 21 | 22 | def get(id) when is_binary(id) do 23 | case find_cache(id) do 24 | nil -> {:error, :not_found} 25 | pid -> CacheWorker.get_media(pid) 26 | end 27 | end 28 | 29 | def refresh(id) when is_binary(id) do 30 | case find_cache(id) do 31 | nil -> {:error, :not_found} 32 | pid -> CacheWorker.refresh(pid) 33 | end 34 | end 35 | 36 | defp start_worker(id, content) when is_binary(id) and is_bitstring(content), 37 | do: DynamicSupervisor.start_child(__MODULE__, cache_worker_spec(id, content)) 38 | 39 | defp find_cache(id) when is_binary(id) do 40 | GenServer.whereis(CacheWorker.name_for(id)) 41 | end 42 | 43 | defp cache_worker_spec(id, content) do 44 | Supervisor.child_spec( 45 | CacheWorker, 46 | start: {CacheWorker, :start_link, [id, content]}, 47 | restart: :transient 48 | ) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/dsl/pipeliner_consumer.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Pipeliner.Consumer do 2 | import ElixirDrip.Pipeliner 3 | 4 | @callback prepare_state(list(any)) :: any 5 | 6 | defmacro __using__(opts) do 7 | type = get_or_default(opts, :type) 8 | if type not in [:producer_consumer, :consumer] do 9 | raise ArgumentError, ":type needs to be one of :producer_consumer or :consumer" 10 | end 11 | 12 | args = get_or_default(opts, :args, []) 13 | 14 | optional_args = create_args(__MODULE__, args) 15 | required_args = create_args(__MODULE__, [:name, :sub_options]) 16 | 17 | optional_and_subscription_args = optional_args ++ create_args(__MODULE__, [:sub_options]) 18 | 19 | function_args = optional_args ++ required_args 20 | 21 | quote do 22 | use GenStage 23 | import unquote(__MODULE__) 24 | @behaviour unquote(__MODULE__) 25 | @behaviour GenStage 26 | 27 | def start_link(unquote_splicing(function_args)) do 28 | GenStage.start_link( 29 | __MODULE__, unquote(optional_and_subscription_args), name: name) 30 | end 31 | 32 | @impl GenStage 33 | def init([unquote_splicing(optional_and_subscription_args)]) do 34 | state = prepare_state(unquote(optional_args)) 35 | 36 | {unquote(type), state, subscribe_to: sub_options} 37 | end 38 | 39 | def prepare_state(args), do: args 40 | 41 | defoverridable unquote(__MODULE__) 42 | end 43 | end 44 | 45 | defp create_args(_, []), do: [] 46 | defp create_args(module, arg_names), 47 | do: Enum.map(arg_names, &Macro.var(&1, module)) 48 | end 49 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :elixir_drip_web, 10 | namespace: ElixirDripWeb, 11 | ecto_repos: [ElixirDrip.Repo] 12 | 13 | # Configures the endpoint 14 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: System.get_env("SECRET_KEY_BASE"), 17 | render_errors: [view: ElixirDripWeb.ErrorView, accepts: ~w(html json)], 18 | pubsub: [name: ElixirDripWeb.PubSub, adapter: Phoenix.PubSub.PG2], 19 | instrumenters: [ElixirDripWeb.EndpointInstrumenter] 20 | 21 | # Configures Elixir's Logger 22 | config :logger, :console, 23 | format: "$time $metadata[$level] $message\n", 24 | metadata: [:request_id] 25 | 26 | config :prometheus, ElixirDripWeb.PlugInstrumenter, 27 | labels: [:status_class, :method, :host, :scheme, :request_path] 28 | 29 | config :prometheus, ElixirDripWeb.EndpointInstrumenter, 30 | duration_buckets: [ 31 | 500, 1000, 2500, 5000, 10_000, 25_000, 50_000, 32 | 100_000, 250_000, 500_000, 1_000_000, 2_500_000, 5_000_000, 33 | 10_000_000, 12_500_000, 15_000_000, 17_500_000, 20_000_000 34 | ] 35 | 36 | config :wobserver, 37 | mode: :plug, 38 | port: 4000, 39 | discovery: :custom, 40 | discovery_search: "&ElixirDripWeb.Wobserver.Discovery.discover/0", 41 | remote_url_prefix: "/wobserver" 42 | 43 | 44 | import_config "#{Mix.env()}.exs" 45 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias ElixirDrip.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import ElixirDrip.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ElixirDrip.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(ElixirDrip.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | @one_week_seconds 7 * 24 * 60 * 60 5 | 6 | ## Channels 7 | channel "users:*", ElixirDripWeb.UserChannel 8 | 9 | ## Transports 10 | transport(:websocket, Phoenix.Transports.WebSocket) 11 | # transport :longpoll, Phoenix.Transports.LongPoll 12 | 13 | # Socket params are passed from the client and can 14 | # be used to verify and authenticate a user. After 15 | # verification, you can put default assigns into 16 | # the socket that will be set for all channels, ie 17 | # 18 | # {:ok, assign(socket, :user_id, verified_user_id)} 19 | # 20 | # To deny connection, return `:error`. 21 | # 22 | # See `Phoenix.Token` documentation for examples in 23 | # performing token verification on connect. 24 | def connect(%{"token" => token}, socket) do 25 | case Phoenix.Token.verify(socket, "user socket auth", token, max_age: @one_week_seconds) do 26 | {:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)} 27 | {:error, _reason} -> :error 28 | end 29 | end 30 | 31 | # Socket id's are topics that allow you to identify all sockets for a given user: 32 | # 33 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 34 | # 35 | # Would allow you to broadcast a "disconnect" event and terminate 36 | # all active sockets and channels for a given user: 37 | # 38 | # ElixirDripWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 39 | # 40 | # Returning `nil` makes this socket anonymous. 41 | def id(socket), do: "users_socket:#{socket.assigns.user_id}" 42 | end 43 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip/cache_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.Storage.Supervisors.CacheSupervisor do 2 | use DynamicSupervisor 3 | require Logger 4 | alias SimpleElixirDrip.Storage.Workers.CacheWorker 5 | 6 | def start_link(_) do 7 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 8 | end 9 | 10 | def init(_) do 11 | Logger.debug("#{inspect(self())} Starting the CacheSupervisor module...") 12 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 100) 13 | end 14 | 15 | def put(id, content) when is_binary(id) and is_bitstring(content) do 16 | case find_cache(id) do 17 | nil -> start_worker(id, content) 18 | pid -> {:ok, pid} 19 | end 20 | end 21 | 22 | def get(id) when is_binary(id) do 23 | case find_cache(id) do 24 | nil -> {:error, :not_found} 25 | pid -> CacheWorker.get_media(pid) 26 | end 27 | end 28 | 29 | def refresh(id) when is_binary(id) do 30 | case find_cache(id) do 31 | nil -> {:error, :not_found} 32 | pid -> CacheWorker.refresh(pid) 33 | end 34 | end 35 | 36 | defp start_worker(id, content) when is_binary(id) and is_bitstring(content), 37 | do: DynamicSupervisor.start_child(__MODULE__, cache_worker_spec(id, content)) 38 | 39 | defp find_cache(id) when is_binary(id) do 40 | GenServer.whereis(CacheWorker.name_for(id)) 41 | end 42 | 43 | defp cache_worker_spec(id, content) do 44 | Supervisor.child_spec( 45 | CacheWorker, 46 | start: {CacheWorker, :start_link, [id, content]}, 47 | restart: :transient 48 | ) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/storage/pipeline/starter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Starter do 2 | @moduledoc false 3 | 4 | require Logger 5 | alias ElixirDrip.Storage.Workers.QueueWorker 6 | 7 | # @queue_polling 5000 8 | @queue_polling 30 * 1000 9 | 10 | use ElixirDrip.Pipeliner.Producer, args: [:type] 11 | 12 | @impl ElixirDrip.Pipeliner.Producer 13 | def prepare_state([type]) do 14 | Logger.debug("#{inspect(self())}: #{type} Pipeline Starter started.") 15 | 16 | %{queue: QueueWorker.queue_name(type), type: type, pending: 0} 17 | end 18 | 19 | @impl GenStage 20 | def handle_info(:try_again, %{queue: queue, pending: demand} = state) do 21 | send_events_from_queue(queue, demand, state) 22 | end 23 | 24 | @impl GenStage 25 | def handle_demand(demand, %{queue: queue, pending: pending} = state) when demand > 0 do 26 | Logger.debug("#{inspect(self())}: Starter(#{inspect(queue)}) received demand of #{demand}, pending = #{pending}.") 27 | total_demand = demand + pending 28 | 29 | send_events_from_queue(queue, total_demand, state) 30 | end 31 | 32 | defp send_events_from_queue(queue, how_many, %{type: type} = state) do 33 | tasks = queue 34 | |> QueueWorker.dequeue(how_many) 35 | |> Enum.map(&Map.put(&1, :type, type)) 36 | 37 | if length(tasks) > 0 do 38 | Logger.debug("#{inspect(self())}: Starter, will emit #{length(tasks)} tasks, :#{type} events now.") 39 | end 40 | 41 | if length(tasks) < how_many do 42 | Process.send_after(self(), :try_again, @queue_polling) 43 | end 44 | 45 | {:noreply, tasks, %{state | pending: how_many - length(tasks)}} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.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 use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [ 15 | node: [ 16 | "node_modules/brunch/bin/brunch", 17 | "watch", 18 | "--stdin", 19 | cd: Path.expand("../assets", __DIR__) 20 | ] 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 | # command from your terminal: 28 | # 29 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 30 | # 31 | # The `http:` config above can be replaced with: 32 | # 33 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 34 | # 35 | # If desired, both `http:` and `https:` keys can be 36 | # configured to run both http and https servers on 37 | # different ports. 38 | 39 | # Watch static and templates for browser reloading. 40 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 41 | live_reload: [ 42 | patterns: [ 43 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 44 | ~r{priv/gettext/.*(po)$}, 45 | ~r{lib/elixir_drip_web/views/.*(ex)$}, 46 | ~r{lib/elixir_drip_web/templates/.*(eex)$} 47 | ] 48 | ] 49 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/pipeline/starter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Starter do 2 | @moduledoc false 3 | 4 | use GenStage 5 | require Logger 6 | alias ElixirDrip.Storage.Workers.QueueWorker 7 | 8 | # @queue_polling 5000 9 | @queue_polling 30 * 1000 10 | 11 | def start_link(name, type) do 12 | GenStage.start_link(__MODULE__, type, name: name) 13 | end 14 | 15 | def init(type) do 16 | Logger.debug("#{inspect(self())}: #{type} Pipeline Starter started.") 17 | 18 | { 19 | :producer, 20 | %{queue: QueueWorker.queue_name(type), type: type, pending: 0} 21 | } 22 | end 23 | 24 | def handle_info(:try_again, %{queue: queue, pending: demand} = state) do 25 | send_events_from_queue(queue, demand, state) 26 | end 27 | 28 | def handle_demand(demand, %{queue: queue, pending: pending} = state) when demand > 0 do 29 | Logger.debug("#{inspect(self())}: Starter(#{inspect(queue)}) received demand of #{demand}, pending = #{pending}.") 30 | total_demand = demand + pending 31 | 32 | send_events_from_queue(queue, total_demand, state) 33 | end 34 | 35 | defp send_events_from_queue(queue, how_many, %{type: type} = state) do 36 | tasks = queue 37 | |> QueueWorker.dequeue(how_many) 38 | |> Enum.map(&Map.put(&1, :type, type)) 39 | 40 | if length(tasks) > 0 do 41 | Logger.debug("#{inspect(self())}: Starter, will emit #{length(tasks)} tasks, :#{type} events now.") 42 | end 43 | 44 | if length(tasks) < how_many do 45 | Process.send_after(self(), :try_again, @queue_polling) 46 | end 47 | 48 | {:noreply, tasks, %{state | pending: how_many - length(tasks)}} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Chapter11/code/rel/config.exs: -------------------------------------------------------------------------------- 1 | Path.join(["rel", "plugins", "*.exs"]) 2 | |> Path.wildcard() 3 | |> Enum.map(&Code.eval_file(&1)) 4 | 5 | use Mix.Releases.Config, 6 | # This sets the default release built by `mix release` 7 | default_release: :default, 8 | # This sets the default environment used by `mix release` 9 | default_environment: Mix.env() 10 | 11 | environment :dev do 12 | # If you are running Phoenix, you should make sure that 13 | # server: true is set and the code reloader is disabled, 14 | # even in dev mode. 15 | # It is recommended that you build with MIX_ENV=prod and pass 16 | # the --env flag to Distillery explicitly if you want to use 17 | # dev mode. 18 | set dev_mode: true 19 | set include_erts: false 20 | set cookie: :"L9pTCob/l<)0&WTOFkCjg>OOVLw&HZivG;4=((5THJltA0V[a>.|dQ { this.sharePopUp(userChannel) } 10 | } 11 | 12 | userChannel.join() 13 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 14 | .receive("error", reason => { console.log("Unable to join", reason) }) 15 | 16 | userChannel.on("upload", ({message}) => this.renderNotifcation(infoArea, successArea, message)) 17 | userChannel.on("download", ({message, link}) => this.renderDownloadNotifcation(infoArea, successArea, message, link)) 18 | userChannel.on("share", ({message}) => this.renderNotifcation(infoArea, successArea, message)) 19 | }, 20 | 21 | renderNotifcation(infoArea, successArea, message) { 22 | infoArea.innerHTML = "" 23 | successArea.innerHTML = message 24 | }, 25 | 26 | renderDownloadNotifcation(infoArea, successArea, message, link) { 27 | infoArea.innerHTML = "" 28 | successArea.innerHTML = `${message}` 29 | }, 30 | 31 | sharePopUp(userChannel) { 32 | let username = prompt("Share file with:", "username") 33 | if (username != null && username != "") { 34 | let splitUrl = window.location.href.split('/') 35 | let fileId = splitUrl.pop() || splitUrl.pop() 36 | 37 | userChannel.push("share", {username: username, file_id: fileId}) 38 | } 39 | } 40 | } 41 | 42 | export default Notification 43 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip, 7 | version: "0.0.1", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | start_permanent: Mix.env == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {ElixirDrip.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:postgrex, ">= 0.0.0"}, 40 | {:ecto, "~> 2.1"}, 41 | {:ksuid, "~> 0.1.2"}, 42 | ] 43 | end 44 | 45 | # Aliases are shortcuts or tasks specific to the current project. 46 | # For example, to create, migrate and run the seeds file at once: 47 | # 48 | # $ mix ecto.setup 49 | # 50 | # See the documentation for `Mix` for more info on aliases. 51 | defp aliases do 52 | [ 53 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 54 | "ecto.reset": ["ecto.drop", "ecto.setup"], 55 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /Chapter01/control_flow/iex_examples.txt: -------------------------------------------------------------------------------- 1 | x = 5 2 | cond do 3 | x * x == 9 -> "x was 3" 4 | x * x == 16 -> "x was 4" 5 | x * x == 25 -> "x was 5" 6 | true -> "none of the above matched" 7 | end 8 | case Enum.random(1..10) do 9 | 2 -> "The lucky ball was 2" 10 | 7 -> "The lucky ball was 7" 11 | _ -> "The lucky ball was not 2 nor 7" 12 | end 13 | options = [x: [y: [z: "the value we're after!"]]] 14 | case Keyword.fetch(options, :x) do 15 | {:ok, value} -> case Keyword.fetch(value, :y) do 16 | {:ok, inner_value} -> case Keyword.fetch(inner_value, :z) do 17 | {:ok, inner_inner_value} -> inner_inner_value 18 | _ -> "non-existing key" 19 | end 20 | _ -> "non-existing key" 21 | end 22 | _ -> "non-existing key" 23 | end 24 | with {:ok, value} <- Keyword.fetch(options, :x), 25 | {:ok, inner_value} <- Keyword.fetch(value, :y), 26 | {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z), 27 | do: inner_inner_value 28 | with {:ok, value} <- Keyword.fetch(options, :missing_key), 29 | {:ok, inner_value} <- Keyword.fetch(value, :y), 30 | {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z), 31 | do: inner_inner_value 32 | with {:ok, value} <- Keyword.fetch(options, :missing_key), 33 | {:ok, inner_value} <- Keyword.fetch(value, :y), 34 | {:ok, inner_inner_value} <- Keyword.fetch(inner_value, :z) do 35 | inner_inner_value 36 | else 37 | :error -> "non-existing key" 38 | _ -> "some other error" 39 | end 40 | raise "Something very strange occurred" 41 | raise ArithmeticError, message: "Some weird math going on here" 42 | try do 43 | 5 / 0 44 | rescue 45 | e in ArithmeticError -> "Tried to divide by 0." 46 | _ -> "None of the above matched" 47 | end 48 | try do 49 | 5 / 0 50 | rescue 51 | ArgumentError -> "ArgumentError was raised." 52 | end 53 | -------------------------------------------------------------------------------- /Chapter03/working_with_processes/cache_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule CacheWorker do 2 | require Logger 3 | 4 | @expire_time 60_000 5 | 6 | def init(content) do 7 | spawn(fn -> 8 | timer = Process.send_after(self(), :expire, @expire_time) 9 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 10 | loop(%{hits: 0, content: content, timer: timer}) 11 | end) 12 | end 13 | 14 | defp loop(state) do 15 | new_state = receive do 16 | {:get_content, caller} -> get_content(state, caller) 17 | :refresh -> refresh_timer(state) 18 | :expire -> terminate(state) 19 | message -> unexpected_message(state, message) 20 | end 21 | 22 | loop(new_state) 23 | end 24 | 25 | defp get_content(%{content: content, hits: hits} = state, caller) do 26 | Logger.debug("Serving request for get_content. Content is #{content}") 27 | send(caller, {:response, content}) 28 | new_state = refresh_timer(state) 29 | 30 | %{new_state | hits: hits + 1} 31 | end 32 | 33 | defp refresh_timer(%{timer: timer} = state) do 34 | Process.cancel_timer(timer) 35 | new_timer = Process.send_after(self(), :expire, @expire_time) 36 | expires_in = Process.read_timer(new_timer) 37 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 38 | 39 | %{state | timer: new_timer} 40 | end 41 | 42 | defp terminate(%{hits: hits}) do 43 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 44 | Process.exit(self(), :normal) 45 | end 46 | 47 | defp unexpected_message(state, message) do 48 | Logger.warn("#{inspect(self())}: Received unexpected message: #{inspect(message)}") 49 | state 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/supervisors/upload_pipeline_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Supervisors.Upload.Pipeline do 2 | @moduledoc """ 3 | A pipeline supervisor that will spawn and supervise all the GenStage processes that compose the upload pipeline. 4 | 5 | Steps: 6 | Upload enqueued 7 | |> Encryption 8 | |> RemoteStorage 9 | |> Notifier 10 | """ 11 | 12 | use Supervisor 13 | alias ElixirDrip.Storage.Pipeline.{ 14 | Common, 15 | Starter, 16 | Encryption, 17 | RemoteStorage, 18 | Notifier 19 | } 20 | 21 | @direction :upload 22 | @starter_name Common.stage_name(Starter, @direction) 23 | @encryption_name Common.stage_name(Encryption, @direction) 24 | @storage_name Common.stage_name(RemoteStorage, @direction) 25 | @notifier_name Common.stage_name(Notifier, @direction) 26 | 27 | def start_link() do 28 | Supervisor.start_link(__MODULE__, [], name: __MODULE__) 29 | end 30 | 31 | def init(_) do 32 | encryption_subscription = [subscribe_to: [{@starter_name, min_demand: 1, max_demand: 10}]] 33 | remote_storage_subscription = [subscribe_to: [{@encryption_name, min_demand: 1, max_demand: 10}]] 34 | notifier_subscription = [subscribe_to: [{@storage_name, min_demand: 1, max_demand: 10}]] 35 | 36 | Supervisor.init([ 37 | worker(Starter, [@starter_name, @direction], 38 | restart: :permanent), 39 | 40 | worker(Encryption, [@encryption_name, encryption_subscription], 41 | restart: :permanent), 42 | 43 | worker(RemoteStorage, [@storage_name, remote_storage_subscription], 44 | restart: :permanent), 45 | 46 | worker(Notifier, [@notifier_name, notifier_subscription], 47 | restart: :permanent) 48 | ], 49 | strategy: :rest_for_one, 50 | name: __MODULE__ 51 | ) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/test/elixir_drip/storage/media_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.MediaTest do 2 | use ExUnit.Case, async: true 3 | 4 | @subject ElixirDrip.Storage.Media 5 | doctest @subject 6 | 7 | setup do 8 | user_id = "0ujsswThIGTUYm2K8FjOOfXtY1K" 9 | file_name = "test.txt" 10 | full_path = "$/abc" 11 | file_size = 123 12 | 13 | {:ok, user_id: user_id, file_name: file_name, full_path: full_path, file_size: file_size} 14 | end 15 | 16 | test "create_initial_changeset/4 returns a valid changeset", context do 17 | changeset = @subject.create_initial_changeset(context.user_id, context.file_name, context.full_path, context.file_size) 18 | 19 | assert changeset.valid? == true 20 | end 21 | 22 | describe "when the file name is invalid" do 23 | setup :invalid_file_name 24 | 25 | test "the changeset isn't valid", context do 26 | changeset = @subject.create_initial_changeset(context.user_id, context.file_name, context.full_path, context.file_size) 27 | 28 | assert changeset.valid? == false 29 | end 30 | 31 | test "the changeset contains the expected error message", context do 32 | changeset = @subject.create_initial_changeset(context.user_id, context.file_name, context.full_path, context.file_size) 33 | 34 | assert changeset.errors == [file_name: {"Invalid file name", []}] 35 | end 36 | end 37 | 38 | test "is_valid_path?/1 returns {:ok, :valid} for a valid path" do 39 | path = "$/abc" 40 | 41 | assert {:ok, :valid} == @subject.is_valid_path?(path) 42 | end 43 | 44 | test "is_valid_path?/1 returns {:error, :invalid_path} for an invalid path" do 45 | path = "$/abc/" 46 | 47 | assert {:error, :invalid_path} == @subject.is_valid_path?(path) 48 | end 49 | 50 | defp invalid_file_name(_context) do 51 | {:ok, file_name: "a/bc" } 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip_web, 7 | version: "0.0.1", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 15 | start_permanent: Mix.env == :prod, 16 | aliases: aliases(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application. 22 | # 23 | # Type `mix help compile.app` for more information. 24 | def application do 25 | [ 26 | mod: {ElixirDripWeb.Application, []}, 27 | extra_applications: [:logger, :runtime_tools] 28 | ] 29 | end 30 | 31 | # Specifies which paths to compile per environment. 32 | defp elixirc_paths(:test), do: ["lib", "test/support"] 33 | defp elixirc_paths(_), do: ["lib"] 34 | 35 | # Specifies your project dependencies. 36 | # 37 | # Type `mix help deps` for examples and options. 38 | defp deps do 39 | [ 40 | {:phoenix, "~> 1.3.0"}, 41 | {:phoenix_pubsub, "~> 1.0"}, 42 | {:phoenix_ecto, "~> 3.2"}, 43 | {:phoenix_html, "~> 2.10"}, 44 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 45 | {:gettext, "~> 0.11"}, 46 | {:cowboy, "~> 1.0"}, 47 | {:elixir_drip, in_umbrella: true} 48 | ] 49 | end 50 | 51 | # Aliases are shortcuts or tasks specific to the current project. 52 | # For example, we extend the test task to create and migrate the database. 53 | # 54 | # See the documentation for `Mix` for more info on aliases. 55 | defp aliases do 56 | ["test": ["ecto.create --quiet", "ecto.migrate", "test"]] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /Chapter01/the_type_system/iex_examples.txt: -------------------------------------------------------------------------------- 1 | # Integers 2 | 25 + 8 3 | 1_000_000 - 500_000 4 | 0b10001 5 | 0o21 6 | 0x11 7 | 8 | # Floats 9 | 0.1 * 0.5 10 | 0.1e3 11 | 4/2 12 | 13 | # Atoms 14 | :ok 15 | :error 16 | :some_descriptive_name! 17 | :value@start 18 | :"Atom name with arbitrary characters#$%^" 19 | 20 | # Booleans 21 | true == :true 22 | false == :false 23 | nil == :nil 24 | true or false 25 | true and false 26 | not false 27 | "a value" || false 28 | "a value" && false 29 | nil && "a value" 30 | !"a value" 31 | 1 == 1.0 32 | 1 === 1.0 33 | 34 | # Tuples 35 | {:ok, 3.14} 36 | result = {:ok, 3.14} 37 | elem(result, 1) 38 | put_elem(result, 1, 1.61) 39 | result 40 | 41 | # Lists 42 | [1, :an_atom, 0.5] 43 | [0, 1, 1] ++ [2, 3, 5] 44 | [0, 1, 1] -- [1, 2, 3] 45 | 1 in [0, 1, 1, 2, 3, 5] 46 | 99 in [0, 1, 1, 2, 3, 5] 47 | hd([0, 1, 1, 2, 3, 5]) 48 | tl([0, 1, 1, 2, 3, 5]) 49 | 50 | # Maps 51 | %{:name => "Gabriel", :age => 1} 52 | %{name: "Gabriel", age: 1} 53 | map = %{name: "Gabriel", age: 1} 54 | map[:name] 55 | map.name 56 | %{map | age: 2} 57 | Map.put(map, :gender, "Male") 58 | 59 | # Binaries 60 | <<5, 10>> 61 | <<5, 256>> 62 | <<5, 256::16>> 63 | 64 | # Strings and Charlists 65 | "hey, a string" 66 | 'hey, a charlist' 67 | hd('hey, a charlist') 68 | "two plus two is: #{2+2}" 69 | 'four minus one is: #{4-1}' 70 | """ 71 | a string with heredoc notation 72 | """ 73 | ''' 74 | a charlist with heredoc notation 75 | ''' 76 | ~s(a string created by a sigil) 77 | ~c(a charlist created by a sigil) 78 | Regex.run(~r{str}, "a string") 79 | Regex.run(~r{123}, "a string") 80 | String.to_charlist("converting to charlist") 81 | List.to_string('converting to string') 82 | 83 | # Other types 84 | keyword_list = [name: "Gabriel", age: 1] 85 | keyword_list[:name] 86 | 87 | 17..21 88 | 19 in 17..21 89 | 90 | set = MapSet.new 91 | set = MapSet.put(set, 1) 92 | set = MapSet.put(set, 2) 93 | set = MapSet.put(set, 1) 94 | 95 | i("a string") 96 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip/lib/elixir_drip/prometheus/instrumenter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Instrumenter do 2 | use Prometheus 3 | 4 | @histograms %{ 5 | "storage_store" => "Time histogram for the Storage.store/4 function" 6 | } 7 | 8 | @counters %{ 9 | "cache_worker_hit" => "Counter for each download served by the CacheWorker instead of relying on the Cloud provider" 10 | } 11 | 12 | @default_buckets :prometheus_http.microseconds_duration_buckets() 13 | 14 | def setup do 15 | @histograms 16 | |> Enum.map(&__MODULE__.histogram_config(&1, @default_buckets)) 17 | |> Enum.each(&Histogram.new(&1)) 18 | 19 | @counters 20 | |> Enum.map(&__MODULE__.counter_config(&1)) 21 | |> Enum.each(&Counter.new(&1)) 22 | end 23 | 24 | @counters 25 | |> Enum.map(&elem(&1, 0)) 26 | |> Enum.map(&String.to_atom(&1)) 27 | |> Enum.map(fn counter -> 28 | def count(unquote(counter)), do: count(unquote(counter), 1) 29 | def count(unquote(counter), count), do: Counter.count([name: __MODULE__.counter_name(unquote(counter))], count) 30 | end) 31 | 32 | @histograms 33 | |> Enum.map(&elem(&1, 0)) 34 | |> Enum.map(&String.to_atom(&1)) 35 | |> Enum.map(fn histogram -> 36 | def observe(unquote(histogram), value), do: Histogram.observe([name: __MODULE__.histogram_name(unquote(histogram))], value) 37 | 38 | def observe_duration(unquote(histogram), body), do: Histogram.observe_duration([name: __MODULE__.histogram_name(unquote(histogram))], body.()) 39 | end) 40 | 41 | def counter_config({name, help}), do: [name: counter_name(name), help: help] 42 | def histogram_config({name, help}, buckets), do: [name: histogram_name(name), help: help, buckets: buckets] 43 | 44 | def counter_name(name), 45 | do: "elixir_drip_" <> to_string(name) <> "_count" |> String.to_atom() 46 | def histogram_name(name), 47 | do: "elixir_drip_" <> to_string(name) <> "_microseconds" |> String.to_atom() 48 | end 49 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/lib/elixir_drip/storage/pipeline/remote_storage.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.RemoteStorage do 2 | @moduledoc false 3 | 4 | require Logger 5 | alias ElixirDrip.Storage 6 | alias Storage.Supervisors.CacheSupervisor, as: Cache 7 | 8 | @provider Application.get_env(:elixir_drip, :storage_provider) 9 | 10 | @dummy_state [] 11 | 12 | use ElixirDrip.Pipeliner.Consumer, type: :producer_consumer 13 | 14 | @impl ElixirDrip.Pipeliner.Consumer 15 | def prepare_state([]) do 16 | Logger.debug("#{inspect(self())}: Streamlined Pipeline RemoteStorage started.") 17 | 18 | @dummy_state 19 | end 20 | 21 | @impl GenStage 22 | def handle_events(tasks, _from, _state) do 23 | processed = Enum.map(tasks, &remote_storage_step(&1)) 24 | {:noreply, processed, @dummy_state} 25 | end 26 | 27 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key} = media, content: content, type: :upload} = task) do 28 | Logger.debug("#{inspect(self())}: Uploading media #{id} to #{storage_key}, size: #{byte_size(content)} bytes.") 29 | 30 | {:ok, :uploaded} = @provider.upload(storage_key, content) 31 | 32 | %{task | media: Storage.set_upload_timestamp(media)} 33 | end 34 | 35 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key}, type: :download} = task) do 36 | result = case Cache.get(id) do 37 | nil -> 38 | {:ok, content} = @provider.download(storage_key) 39 | 40 | Logger.debug("#{inspect(self())}: Just downloaded media #{id}, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 41 | 42 | %{content: content} 43 | 44 | {:ok, content} -> 45 | Logger.debug("#{inspect(self())}: Got media #{id} from cache, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 46 | 47 | 48 | %{content: content, status: :original} 49 | end 50 | 51 | Map.merge(task, result) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use ElixirDripWeb, :controller 9 | use ElixirDripWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, 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 any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: ElixirDripWeb 23 | import Plug.Conn 24 | import ElixirDripWeb.Router.Helpers 25 | import ElixirDripWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/elixir_drip_web/templates", 33 | namespace: ElixirDripWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 37 | 38 | # Use all HTML functionality (forms, tags, etc) 39 | use Phoenix.HTML 40 | 41 | import ElixirDripWeb.Router.Helpers 42 | import ElixirDripWeb.ErrorHelpers 43 | import ElixirDripWeb.Gettext 44 | end 45 | end 46 | 47 | def router do 48 | quote do 49 | use Phoenix.Router 50 | import Plug.Conn 51 | import Phoenix.Controller 52 | end 53 | end 54 | 55 | def channel do 56 | quote do 57 | use Phoenix.Channel 58 | import ElixirDripWeb.Gettext 59 | end 60 | end 61 | 62 | @doc """ 63 | When used, dispatch to the appropriate controller/view/etc. 64 | """ 65 | defmacro __using__(which) when is_atom(which) do 66 | apply(__MODULE__, which, []) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Welcome to ElixirDrip! 11 | "> 12 | 13 | 14 | 15 |
16 |
17 | 26 | 27 |
28 | 29 | <%= if assigns[:current_user] do %> 30 |

Online users:

31 |
32 | <% end %> 33 | 34 | 35 | 36 | 37 | 38 |
39 | <%= render @view_module, @view_template, assigns %> 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/pipeline/encryption.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.Encryption do 2 | @moduledoc false 3 | @dummy_state [] 4 | 5 | use GenStage 6 | require Logger 7 | alias ElixirDrip.Storage.Providers.Encryption.Simple, as: Provider 8 | alias ElixirDrip.Storage.Supervisors.CacheSupervisor, as: Cache 9 | 10 | def start_link(name, subscription_options), 11 | do: GenStage.start_link(__MODULE__, subscription_options, name: name) 12 | 13 | def init(subscription_options) do 14 | Logger.debug("#{inspect(self())}: Pipeline Encryption started. Options: #{inspect(subscription_options)}") 15 | 16 | {:producer_consumer, @dummy_state, subscription_options} 17 | end 18 | 19 | def handle_events(tasks, _from, _state) do 20 | encrypted = Enum.map(tasks, &encryption_step(&1)) 21 | 22 | {:noreply, encrypted, @dummy_state} 23 | end 24 | 25 | defp encryption_step(%{media: %{id: id, encryption_key: encryption_key}, content: content, type: :upload} = task) do 26 | Process.sleep(1000) 27 | 28 | Cache.put(id, content) 29 | 30 | Logger.debug("#{inspect(self())}: Encrypting media #{id}, size: #{byte_size(content)} bytes.") 31 | 32 | %{task | content: Provider.encrypt(content, encryption_key)} 33 | end 34 | 35 | defp encryption_step(%{media: media, content: _content, status: :original, type: :download} = task) do 36 | Logger.debug("#{inspect(self())}: Media #{media.id} already decrypted, skipping decryption...") 37 | 38 | task 39 | end 40 | 41 | defp encryption_step(%{media: %{id: id, encryption_key: encryption_key}, content: content, type: :download} = task) do 42 | Process.sleep(1000) 43 | Logger.debug("#{inspect(self())}: Decrypting media #{id}, size: #{byte_size(content)} bytes.") 44 | 45 | clear_content = Provider.decrypt(content, encryption_key) 46 | Cache.put_or_refresh(id, clear_content) 47 | 48 | %{task | content: clear_content} 49 | end 50 | end 51 | 52 | -------------------------------------------------------------------------------- /Chapter06/apps/elixir_drip/lib/elixir_drip/storage/pipeline/remote_storage.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.RemoteStorage do 2 | @moduledoc false 3 | 4 | require Logger 5 | alias ElixirDrip.Storage 6 | alias Storage.Provider 7 | alias Storage.Supervisors.CacheSupervisor, as: Cache 8 | 9 | @dummy_state [] 10 | 11 | use ElixirDrip.Pipeliner.Consumer, type: :producer_consumer 12 | 13 | @impl ElixirDrip.Pipeliner.Consumer 14 | def prepare_state([]) do 15 | Logger.debug("#{inspect(self())}: Streamlined Pipeline RemoteStorage started.") 16 | 17 | @dummy_state 18 | end 19 | 20 | @impl GenStage 21 | def handle_events(tasks, _from, _state) do 22 | processed = Enum.map(tasks, &remote_storage_step(&1)) 23 | 24 | {:noreply, processed, @dummy_state} 25 | end 26 | 27 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key} = media, content: content, type: :upload} = task) do 28 | Process.sleep(2000) 29 | 30 | Logger.debug("#{inspect(self())}: Uploading media #{id} to #{storage_key}, size: #{byte_size(content)} bytes.") 31 | 32 | {:ok, :uploaded} = Provider.upload(storage_key, content) 33 | 34 | %{task | media: Storage.set_upload_timestamp(media)} 35 | end 36 | 37 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key}, type: :download} = task) do 38 | Process.sleep(2000) 39 | 40 | result = case Cache.get(id) do 41 | nil -> 42 | {:ok, content} = Provider.download(storage_key) 43 | 44 | Logger.debug("#{inspect(self())}: Just downloaded media #{id}, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 45 | 46 | %{content: content} 47 | 48 | {:ok, content} -> 49 | Logger.debug("#{inspect(self())}: Got media #{id} from cache, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 50 | 51 | 52 | %{content: content, status: :original} 53 | end 54 | 55 | Map.merge(task, result) 56 | end 57 | end 58 | 59 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip_web/test/elixir_drip_web/channels/user_channel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.UserChannelTest do 2 | use ElixirDripWeb.ChannelCase 3 | 4 | setup [:create_users, :create_media, :connect_to_socket] 5 | 6 | test "join users:lobby pushes a 'presence_state' event to the joiner", %{socket: socket} do 7 | {:ok, _reply, _socket} = subscribe_and_join(socket, "users:lobby") 8 | 9 | assert_push "presence_state", %{} 10 | end 11 | 12 | test "join users: broadcasts 'share' events to owner and sharee", %{socket: socket, user: user, sharee: sharee, media: media} do 13 | {:ok, _reply, socket} = subscribe_and_join(socket, "users:#{user.id}") 14 | ElixirDripWeb.Endpoint.subscribe(self(), "users:#{sharee.id}") 15 | 16 | push(socket, "share", %{"username" => "other_user", "file_id" => media.id}) 17 | 18 | assert_broadcast "share", %{message: "Successfully shared the file with other_user."} 19 | assert_receive %Phoenix.Socket.Broadcast{ 20 | payload: %{message: "test has just shared a file with you!"} 21 | } 22 | end 23 | 24 | defp create_users(_context) do 25 | {:ok, user} = 26 | ElixirDrip.Accounts.create_user(%{ 27 | username: "test", 28 | password: "12345678", 29 | email: "test@test.com" 30 | }) 31 | 32 | {:ok, sharee} = 33 | ElixirDrip.Accounts.create_user(%{ 34 | username: "other_user", 35 | password: "12345678", 36 | email: "sharee@test.com" 37 | }) 38 | 39 | {:ok, user: user, sharee: sharee} 40 | end 41 | 42 | defp create_media(%{user: user}) do 43 | {:ok, :upload_enqueued, media} = ElixirDrip.Storage.store(user.id, "test.txt", "$", "some cool content") 44 | {:ok, media: media} 45 | end 46 | 47 | defp connect_to_socket(%{user: user}) do 48 | token = Phoenix.Token.sign(@endpoint, "user socket auth", user.id) 49 | {:ok, socket} = connect(ElixirDripWeb.UserSocket, %{"token" => token}) 50 | {:ok, socket: socket} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/cache_worker_under_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Workers.CacheWorker do 2 | use GenServer 3 | require Logger 4 | 5 | @expire_time 60_000 6 | 7 | def start_link(media_id, content) do 8 | GenServer.start_link(__MODULE__, content, name: name_for(media_id)) 9 | end 10 | 11 | def name_for(media_id), do: {:global, {:cache, media_id}} 12 | 13 | def init(content) do 14 | timer = Process.send_after(self(), :expire, @expire_time) 15 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 16 | 17 | {:ok, %{hits: 0, content: content, timer: timer}} 18 | end 19 | 20 | def handle_info(:expire, %{hits: hits} = state) do 21 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 22 | {:stop, :normal, state} 23 | end 24 | 25 | def handle_info(msg, state) do 26 | super(msg, state) 27 | end 28 | 29 | def get_media(pid), do: GenServer.call(pid, :get_media) 30 | 31 | def handle_call(:get_media, _from, %{hits: hits, content: content, timer: timer} = state) do 32 | Logger.debug("#{inspect(self())}: Received :get_media and served #{byte_size(content)} bytes #{hits+1} times.") 33 | 34 | new_timer = refresh_timer(timer) 35 | 36 | {:reply, {:ok, content}, %{state | hits: hits + 1, timer: new_timer}} 37 | end 38 | 39 | def refresh(pid), do: GenServer.cast(pid, :refresh) 40 | 41 | def handle_cast(:refresh, %{timer: timer} = state), 42 | do: {:noreply, %{state | timer: refresh_timer(timer)}} 43 | 44 | defp refresh_timer(timer) do 45 | Process.cancel_timer(timer) 46 | new_timer = Process.send_after(self(), :expire, @expire_time) 47 | expires_in = Process.read_timer(new_timer) 48 | 49 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 50 | 51 | new_timer 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip_web, 7 | version: "0.0.8", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 15 | start_permanent: Mix.env == :prod, 16 | aliases: aliases(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application. 22 | # 23 | # Type `mix help compile.app` for more information. 24 | def application do 25 | [ 26 | mod: {ElixirDripWeb.Application, []}, 27 | extra_applications: [:logger, :runtime_tools] 28 | ] 29 | end 30 | 31 | # Specifies which paths to compile per environment. 32 | defp elixirc_paths(:test), do: ["lib", "test/support"] 33 | defp elixirc_paths(_), do: ["lib"] 34 | 35 | # Specifies your project dependencies. 36 | # 37 | # Type `mix help deps` for examples and options. 38 | defp deps do 39 | [ 40 | {:phoenix, "~> 1.3.2"}, 41 | {:phoenix_pubsub, "~> 1.0"}, 42 | {:phoenix_ecto, "~> 3.3"}, 43 | {:phoenix_html, "~> 2.11"}, 44 | {:phoenix_live_reload, "~> 1.1", only: :dev}, 45 | {:gettext, "~> 0.15"}, 46 | {:cowboy, "~> 1.0"}, 47 | {:timex, "~> 3.3"}, 48 | {:elixir_drip, in_umbrella: true}, 49 | {:prometheus_phoenix, "~> 1.2"}, 50 | {:prometheus_plugs, "~> 1.1"}, 51 | {:wobserver, "~> 0.1.8"}, 52 | ] 53 | end 54 | 55 | # Aliases are shortcuts or tasks specific to the current project. 56 | # For example, we extend the test task to create and migrate the database. 57 | # 58 | # See the documentation for `Mix` for more info on aliases. 59 | defp aliases do 60 | ["test": ["ecto.create --quiet", "ecto.migrate", "test"]] 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /Chapter03/recovering_from_errors_with_supervisors_and_supervision_trees/simple_elixir_drip/lib/simple_elixir_drip/cache_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleElixirDrip.Storage.Workers.CacheWorker do 2 | use GenServer 3 | require Logger 4 | 5 | @expire_time 60_000 6 | 7 | def start_link(media_id, content) do 8 | GenServer.start_link(__MODULE__, content, name: name_for(media_id)) 9 | end 10 | 11 | def name_for(media_id), do: {:global, {:cache, media_id}} 12 | 13 | def init(content) do 14 | timer = Process.send_after(self(), :expire, @expire_time) 15 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 16 | 17 | {:ok, %{hits: 0, content: content, timer: timer}} 18 | end 19 | 20 | def handle_info(:expire, %{hits: hits} = state) do 21 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 22 | {:stop, :normal, state} 23 | end 24 | 25 | def handle_info(msg, state) do 26 | super(msg, state) 27 | end 28 | 29 | def get_media(pid), do: GenServer.call(pid, :get_media) 30 | 31 | def handle_call(:get_media, _from, %{hits: hits, content: content, timer: timer} = state) do 32 | Logger.debug("#{inspect(self())}: Received :get_media and served #{byte_size(content)} bytes #{hits+1} times.") 33 | 34 | new_timer = refresh_timer(timer) 35 | 36 | {:reply, {:ok, content}, %{state | hits: hits + 1, timer: new_timer}} 37 | end 38 | 39 | def refresh(pid), do: GenServer.cast(pid, :refresh) 40 | 41 | def handle_cast(:refresh, %{timer: timer} = state), 42 | do: {:noreply, %{state | timer: refresh_timer(timer)}} 43 | 44 | defp refresh_timer(timer) do 45 | Process.cancel_timer(timer) 46 | new_timer = Process.send_after(self(), :expire, @expire_time) 47 | expires_in = Process.read_timer(new_timer) 48 | 49 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 50 | 51 | new_timer 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Chapter07/apps/elixir_drip/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip, 7 | version: "0.0.1", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | start_permanent: Mix.env == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {ElixirDrip.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib", "../../examples/parallel_genstage"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:postgrex, "~> 0.13"}, 40 | {:ecto, "~> 2.1"}, 41 | {:ksuid, "~> 0.1.2"}, 42 | {:arc, github: "stavro/arc", override: true}, 43 | {:arc_gcs, "~> 0.0.3"}, 44 | {:comeonin, "~> 4.1"}, 45 | {:bcrypt_elixir, "~> 1.0"}, 46 | {:flow, "~> 0.12.0"}, 47 | {:gen_stage, "~> 0.13.0", override: true} 48 | ] 49 | end 50 | 51 | # Aliases are shortcuts or tasks specific to the current project. 52 | # For example, to create, migrate and run the seeds file at once: 53 | # 54 | # $ mix ecto.setup 55 | # 56 | # See the documentation for `Mix` for more info on aliases. 57 | defp aliases do 58 | [ 59 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 60 | "ecto.reset": ["ecto.drop", "ecto.setup"], 61 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip/lib/elixir_drip/storage/workers/cache_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Workers.CacheWorker do 2 | use GenServer 3 | require Logger 4 | 5 | @expire_time 60_000*120 6 | 7 | def start_link(media_id, content) do 8 | GenServer.start_link(__MODULE__, content, name: name_for(media_id)) 9 | end 10 | 11 | def name_for(media_id), do: {:global, {:cache, media_id}} 12 | 13 | def init(content) do 14 | timer = Process.send_after(self(), :expire, @expire_time) 15 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 16 | 17 | {:ok, %{hits: 0, content: content, timer: timer}} 18 | end 19 | 20 | def handle_info(:expire, %{hits: hits} = state) do 21 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 22 | {:stop, :normal, state} 23 | end 24 | 25 | def handle_info(msg, state) do 26 | super(msg, state) 27 | end 28 | 29 | def get_media(pid), do: GenServer.call(pid, :get_media) 30 | 31 | def handle_call(:get_media, _from, %{hits: hits, content: content, timer: timer} = state) do 32 | ElixirDrip.Instrumenter.count(:cache_worker_hit) 33 | 34 | Logger.debug("#{inspect(self())}: Received :get_media and served #{byte_size(content)} bytes #{hits+1} times.") 35 | 36 | new_timer = refresh_timer(timer) 37 | 38 | {:reply, {:ok, content}, %{state | hits: hits + 1, timer: new_timer}} 39 | end 40 | 41 | def refresh(pid), do: GenServer.cast(pid, :refresh) 42 | 43 | def handle_cast(:refresh, %{timer: timer} = state), 44 | do: {:noreply, %{state | timer: refresh_timer(timer)}} 45 | 46 | defp refresh_timer(timer) do 47 | Process.cancel_timer(timer) 48 | new_timer = Process.send_after(self(), :expire, @expire_time) 49 | expires_in = Process.read_timer(new_timer) 50 | 51 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 52 | 53 | new_timer 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/lib/elixir_drip_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :elixir_drip_web 3 | 4 | socket("/socket", ElixirDripWeb.UserSocket) 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug( 11 | Plug.Static, 12 | at: "/", 13 | from: :elixir_drip_web, 14 | gzip: false, 15 | only: ~w(css fonts images js favicon.ico robots.txt) 16 | ) 17 | 18 | # Code reloading can be explicitly enabled under the 19 | # :code_reloader configuration of your endpoint. 20 | if code_reloading? do 21 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 22 | plug(Phoenix.LiveReloader) 23 | plug(Phoenix.CodeReloader) 24 | end 25 | 26 | plug(Plug.RequestId) 27 | plug(Plug.Logger) 28 | 29 | plug( 30 | Plug.Parsers, 31 | parsers: [:urlencoded, :multipart, :json], 32 | pass: ["*/*"], 33 | json_decoder: Poison 34 | ) 35 | 36 | plug(Plug.MethodOverride) 37 | plug(Plug.Head) 38 | 39 | # The session will be stored in the cookie and signed, 40 | # this means its contents can be read but not tampered with. 41 | # Set :encryption_salt if you would also like to encrypt it. 42 | plug( 43 | Plug.Session, 44 | store: :cookie, 45 | key: "_elixir_drip_web_key", 46 | signing_salt: "uj4bNucu" 47 | ) 48 | 49 | plug(ElixirDripWeb.Router) 50 | 51 | @doc """ 52 | Callback invoked for dynamically configuring the endpoint. 53 | 54 | It receives the endpoint configuration and checks if 55 | configuration should be loaded from the system environment. 56 | """ 57 | def init(_key, config) do 58 | if config[:load_from_system_env] do 59 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 60 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 61 | else 62 | {:ok, config} 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :elixir_drip_web 3 | 4 | @doc """ 5 | Callback invoked for dynamically configuring the endpoint. 6 | 7 | It receives the endpoint configuration and checks if 8 | configuration should be loaded from the system environment. 9 | """ 10 | def init(_key, config) do 11 | if config[:load_from_system_env] do 12 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 13 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 14 | else 15 | {:ok, config} 16 | end 17 | end 18 | 19 | socket("/socket", ElixirDripWeb.UserSocket) 20 | 21 | # Serve at "/" the static files from "priv/static" directory. 22 | # 23 | # You should set gzip to true if you are running phoenix.digest 24 | # when deploying your static files in production. 25 | plug( 26 | Plug.Static, 27 | at: "/", 28 | from: :elixir_drip_web, 29 | gzip: false, 30 | only: ~w(css fonts images js favicon.ico robots.txt) 31 | ) 32 | 33 | # Code reloading can be explicitly enabled under the 34 | # :code_reloader configuration of your endpoint. 35 | if code_reloading? do 36 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 37 | plug(Phoenix.LiveReloader) 38 | plug(Phoenix.CodeReloader) 39 | end 40 | 41 | plug(Plug.RequestId) 42 | plug(Plug.Logger) 43 | 44 | plug( 45 | Plug.Parsers, 46 | parsers: [:urlencoded, :multipart, :json], 47 | pass: ["*/*"], 48 | json_decoder: Poison 49 | ) 50 | 51 | plug(Plug.MethodOverride) 52 | plug(Plug.Head) 53 | 54 | # The session will be stored in the cookie and signed, 55 | # this means its contents can be read but not tampered with. 56 | # Set :encryption_salt if you would also like to encrypt it. 57 | plug( 58 | Plug.Session, 59 | store: :cookie, 60 | key: "_elixir_drip_web_key", 61 | signing_salt: "uj4bNucu" 62 | ) 63 | 64 | plug(ElixirDripWeb.Router) 65 | end 66 | -------------------------------------------------------------------------------- /Chapter05/apps/elixir_drip/lib/elixir_drip/storage/pipeline/remote_storage.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Pipeline.RemoteStorage do 2 | @moduledoc false 3 | 4 | use GenStage 5 | require Logger 6 | alias ElixirDrip.Storage 7 | alias Storage.Provider 8 | alias Storage.Supervisors.CacheSupervisor, as: Cache 9 | 10 | @dummy_state [] 11 | 12 | def start_link(name, subscription_options), 13 | do: GenStage.start_link(__MODULE__, subscription_options, name: name) 14 | 15 | def init(subscription_options) do 16 | Logger.debug("#{inspect(self())}: Pipeline RemoteStorage started. Options: #{inspect(subscription_options)}") 17 | 18 | {:producer_consumer, @dummy_state, subscription_options} 19 | end 20 | 21 | def handle_events(tasks, _from, _state) do 22 | processed = Enum.map(tasks, &remote_storage_step(&1)) 23 | 24 | {:noreply, processed, @dummy_state} 25 | end 26 | 27 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key} = media, content: content, type: :upload} = task) do 28 | Process.sleep(2000) 29 | 30 | Logger.debug("#{inspect(self())}: Uploading media #{id} to #{storage_key}, size: #{byte_size(content)} bytes.") 31 | 32 | {:ok, :uploaded} = Provider.upload(storage_key, content) 33 | 34 | %{task | media: Storage.set_upload_timestamp(media)} 35 | end 36 | 37 | defp remote_storage_step(%{media: %{id: id, storage_key: storage_key}, type: :download} = task) do 38 | Process.sleep(2000) 39 | 40 | result = case Cache.get(id) do 41 | nil -> 42 | {:ok, content} = Provider.download(storage_key) 43 | 44 | Logger.debug("#{inspect(self())}: Just downloaded media #{id}, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 45 | 46 | %{content: content} 47 | 48 | {:ok, content} -> 49 | Logger.debug("#{inspect(self())}: Got media #{id} from cache, content: #{inspect(content)}, size: #{byte_size(content)} bytes.") 50 | 51 | 52 | %{content: content, status: :original} 53 | end 54 | 55 | Map.merge(task, result) 56 | end 57 | end 58 | 59 | -------------------------------------------------------------------------------- /Chapter04/apps/elixir_drip/lib/elixir_drip/storage/workers/cache_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Storage.Workers.CacheWorker do 2 | use GenServer 3 | require Logger 4 | 5 | @expire_time 60_000*120 6 | 7 | def start_link(media_id, content) do 8 | GenServer.start_link(__MODULE__, content, name: name_for(media_id)) 9 | end 10 | 11 | def name_for(media_id), do: {:global, {:cache, media_id}} 12 | 13 | def init(content) do 14 | timer = Process.send_after(self(), :expire, @expire_time) 15 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 16 | 17 | {:ok, %{hits: 0, content: content, timer: timer}} 18 | end 19 | 20 | def handle_info(:expire, %{hits: hits} = state) do 21 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 22 | {:stop, :normal, state} 23 | end 24 | 25 | def handle_info(msg, state) do 26 | super(msg, state) 27 | end 28 | 29 | def get_media(pid), do: GenServer.call(pid, :get_media) 30 | 31 | def handle_call(:get_media, _from, %{hits: hits, content: content, timer: timer} = state) do 32 | # Logger.debug("#{inspect(self())}: Received :get_media and served #{byte_size(content)} bytes #{hits+1} times.") 33 | Logger.debug("#{inspect(self())}: Received :get_media and served '#{inspect(content)}' bytes #{hits+1} times.") 34 | 35 | new_timer = refresh_timer(timer) 36 | 37 | {:reply, {:ok, content}, %{state | hits: hits + 1, timer: new_timer}} 38 | end 39 | 40 | def refresh(pid), do: GenServer.cast(pid, :refresh) 41 | 42 | def handle_cast(:refresh, %{timer: timer} = state), 43 | do: {:noreply, %{state | timer: refresh_timer(timer)}} 44 | 45 | defp refresh_timer(timer) do 46 | Process.cancel_timer(timer) 47 | new_timer = Process.send_after(self(), :expire, @expire_time) 48 | expires_in = Process.read_timer(new_timer) 49 | 50 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 51 | 52 | new_timer 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip_web/lib/elixir_drip_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :elixir_drip_web 3 | 4 | @doc """ 5 | Callback invoked for dynamically configuring the endpoint. 6 | 7 | It receives the endpoint configuration and checks if 8 | configuration should be loaded from the system environment. 9 | """ 10 | def init(_key, config) do 11 | if config[:load_from_system_env] do 12 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 13 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 14 | else 15 | {:ok, config} 16 | end 17 | end 18 | 19 | socket("/socket", ElixirDripWeb.UserSocket) 20 | 21 | # Serve at "/" the static files from "priv/static" directory. 22 | # 23 | # You should set gzip to true if you are running phoenix.digest 24 | # when deploying your static files in production. 25 | plug( 26 | Plug.Static, 27 | at: "/", 28 | from: :elixir_drip_web, 29 | gzip: false, 30 | only: ~w(css fonts images js favicon.ico robots.txt) 31 | ) 32 | 33 | # Code reloading can be explicitly enabled under the 34 | # :code_reloader configuration of your endpoint. 35 | if code_reloading? do 36 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 37 | plug(Phoenix.LiveReloader) 38 | plug(Phoenix.CodeReloader) 39 | end 40 | 41 | plug(Plug.RequestId) 42 | 43 | plug( 44 | Plug.Parsers, 45 | parsers: [:urlencoded, :multipart, :json], 46 | pass: ["*/*"], 47 | json_decoder: Poison 48 | ) 49 | 50 | plug(Plug.MethodOverride) 51 | plug(Plug.Head) 52 | 53 | # The session will be stored in the cookie and signed, 54 | # this means its contents can be read but not tampered with. 55 | # Set :encryption_salt if you would also like to encrypt it. 56 | plug( 57 | Plug.Session, 58 | store: :cookie, 59 | key: "_elixir_drip_web_key", 60 | signing_salt: "uj4bNucu" 61 | ) 62 | 63 | plug(ElixirDripWeb.PlugInstrumenter) 64 | plug(ElixirDripWeb.MetricsExporter) 65 | 66 | plug(ElixirDripWeb.Router) 67 | end 68 | -------------------------------------------------------------------------------- /Chapter09/apps/elixir_drip/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip, 7 | version: "0.0.8", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | start_permanent: Mix.env == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {ElixirDrip.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib", "../../examples/parallel_genstage"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:postgrex, "~> 0.13"}, 40 | {:ecto, "~> 2.1"}, 41 | {:ksuid, "~> 0.1.2"}, 42 | {:arc, github: "stavro/arc", override: true}, 43 | {:arc_gcs, "~> 0.0.3"}, 44 | {:comeonin, "~> 4.1"}, 45 | {:bcrypt_elixir, "~> 1.0"}, 46 | {:flow, "~> 0.12.0"}, 47 | {:libcluster, "~> 2.5"}, 48 | {:gen_stage, "~> 0.13.0", override: true}, 49 | {:mox, "~> 0.3", only: :test}, 50 | {:stream_data, "~> 0.4", only: :test} 51 | ] 52 | end 53 | 54 | # Aliases are shortcuts or tasks specific to the current project. 55 | # For example, to create, migrate and run the seeds file at once: 56 | # 57 | # $ mix ecto.setup 58 | # 59 | # See the documentation for `Mix` for more info on aliases. 60 | defp aliases do 61 | [ 62 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 63 | "ecto.reset": ["ecto.drop", "ecto.setup"], 64 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 65 | ] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /Chapter03/working_with_processes/cache_worker_with_interface_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule CacheWorker do 2 | require Logger 3 | 4 | @expire_time 60_000 5 | 6 | def init(content) do 7 | spawn(fn -> 8 | timer = Process.send_after(self(), :expire, @expire_time) 9 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 10 | loop(%{hits: 0, content: content, timer: timer}) 11 | end) 12 | end 13 | 14 | def get_content(server_pid) do 15 | send(server_pid, {:get_content, self()}) 16 | 17 | receive do 18 | {:response, content} -> content 19 | after 20 | 5000 -> {:error, :timeout} 21 | end 22 | end 23 | 24 | def refresh(server_pid) do 25 | send(server_pid, :refresh) 26 | end 27 | 28 | defp loop(state) do 29 | new_state = receive do 30 | {:get_content, caller} -> get_content(state, caller) 31 | :refresh -> refresh_timer(state) 32 | :expire -> terminate(state) 33 | message -> unexpected_message(state, message) 34 | end 35 | 36 | loop(new_state) 37 | end 38 | 39 | defp get_content(%{content: content, hits: hits} = state, caller) do 40 | Logger.debug("Serving request for get_content. Content is #{content}") 41 | send(caller, {:response, content}) 42 | new_state = refresh_timer(state) 43 | 44 | %{new_state | hits: hits + 1} 45 | end 46 | 47 | defp refresh_timer(%{timer: timer} = state) do 48 | Process.cancel_timer(timer) 49 | new_timer = Process.send_after(self(), :expire, @expire_time) 50 | expires_in = Process.read_timer(new_timer) 51 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 52 | 53 | %{state | timer: new_timer} 54 | end 55 | 56 | defp terminate(%{hits: hits}) do 57 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 58 | Process.exit(self(), :normal) 59 | end 60 | 61 | defp unexpected_message(state, message) do 62 | Logger.warn("#{inspect(self())}: Received unexpected message: #{inspect(message)}") 63 | state 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Chapter03/detecting_errors_by_linking_and_monitoring_processes/cache_worker_with_spawn_link.ex: -------------------------------------------------------------------------------- 1 | defmodule CacheWorker do 2 | require Logger 3 | 4 | @expire_time 60_000 5 | 6 | def init(content) do 7 | spawn_link(fn -> 8 | timer = Process.send_after(self(), :expire, @expire_time) 9 | Logger.debug("#{inspect(self())}: CacheWorker started. Will expire in #{Process.read_timer(timer)} milliseconds.") 10 | loop(%{hits: 0, content: content, timer: timer}) 11 | end) 12 | end 13 | 14 | def get_content(server_pid) do 15 | send(server_pid, {:get_content, self()}) 16 | 17 | receive do 18 | {:response, content} -> content 19 | after 20 | 5000 -> {:error, :timeout} 21 | end 22 | end 23 | 24 | def refresh(server_pid) do 25 | send(server_pid, :refresh) 26 | end 27 | 28 | defp loop(state) do 29 | new_state = receive do 30 | {:get_content, caller} -> get_content(state, caller) 31 | :refresh -> refresh_timer(state) 32 | :expire -> terminate(state) 33 | message -> unexpected_message(state, message) 34 | end 35 | 36 | loop(new_state) 37 | end 38 | 39 | defp get_content(%{content: content, hits: hits} = state, caller) do 40 | Logger.debug("Serving request for get_content. Content is #{content}") 41 | send(caller, {:response, content}) 42 | new_state = refresh_timer(state) 43 | 44 | %{new_state | hits: hits + 1} 45 | end 46 | 47 | defp refresh_timer(%{timer: timer} = state) do 48 | Process.cancel_timer(timer) 49 | new_timer = Process.send_after(self(), :expire, @expire_time) 50 | expires_in = Process.read_timer(new_timer) 51 | Logger.debug("#{inspect(self())}: Canceled the previous expiration timer. Will now expire in #{expires_in} milliseconds.") 52 | 53 | %{state | timer: new_timer} 54 | end 55 | 56 | defp terminate(%{hits: hits}) do 57 | Logger.debug("#{inspect(self())}: Terminating process... Served the cached content #{hits} times.") 58 | Process.exit(self(), :normal) 59 | end 60 | 61 | defp unexpected_message(state, message) do 62 | Logger.warn("#{inspect(self())}: Received unexpected message: #{inspect(message)}") 63 | state 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Chapter11/code/apps/elixir_drip/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirDrip.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_drip, 7 | version: "0.0.8", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | start_permanent: Mix.env == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {ElixirDrip.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib", "../../examples/parallel_genstage"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:postgrex, "~> 0.13"}, 40 | {:ecto, "~> 2.1"}, 41 | {:ksuid, "~> 0.1.2"}, 42 | {:arc, github: "stavro/arc", override: true}, 43 | {:arc_gcs, "~> 0.0.3"}, 44 | {:comeonin, "~> 4.1"}, 45 | {:bcrypt_elixir, "~> 1.0"}, 46 | {:flow, "~> 0.12.0"}, 47 | {:libcluster, "~> 2.5"}, 48 | {:gen_stage, "~> 0.13.0", override: true}, 49 | {:prometheus, "~> 4.0", override: true}, 50 | {:prometheus_ex, "~> 3.0", override: true}, 51 | {:prometheus_ecto, "~> 1.0"}, 52 | ] 53 | end 54 | 55 | # Aliases are shortcuts or tasks specific to the current project. 56 | # For example, to create, migrate and run the seeds file at once: 57 | # 58 | # $ mix ecto.setup 59 | # 60 | # See the documentation for `Mix` for more info on aliases. 61 | defp aliases do 62 | [ 63 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 64 | "ecto.reset": ["ecto.drop", "ecto.setup"], 65 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 66 | ] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/lib/elixir_drip_web/channels/user_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirDripWeb.UserChannel do 2 | use ElixirDripWeb, :channel 3 | alias ElixirDripWeb.{Presence, Endpoint} 4 | alias ElixirDrip.{Accounts, Storage} 5 | 6 | def join("users:lobby", _auth_message, socket) do 7 | send(self(), :after_join) 8 | {:ok, socket} 9 | end 10 | 11 | def join("users:" <> _user_id, _auth_message, socket) do 12 | {:ok, socket} 13 | end 14 | 15 | def handle_info(:after_join, socket) do 16 | push(socket, "presence_state", Presence.list(socket)) 17 | 18 | user = Accounts.find_user(socket.assigns.user_id) 19 | 20 | {:ok, _} = 21 | Presence.track(socket, socket.assigns.user_id, %{ 22 | username: user.username 23 | }) 24 | 25 | {:noreply, socket} 26 | end 27 | 28 | def handle_in("share", %{"username" => username, "file_id" => file_id}, socket) do 29 | sharee = Accounts.get_user_by_username(username) 30 | case sharee do 31 | nil -> broadcast_sharee_not_found(socket, username) 32 | _ -> share_file(socket, socket.assigns.user_id, file_id, sharee) 33 | end 34 | 35 | {:noreply, socket} 36 | end 37 | 38 | defp share_file(socket, owner_id, file_id, sharee) do 39 | case Storage.share(owner_id, file_id, sharee.id) do 40 | {:ok, _media_owners} -> 41 | broadcast_to_owner(socket, sharee.username) 42 | broadcast_to_sharee(sharee.id, socket.assigns.user_id) 43 | {:error, _reason} -> 44 | broadcast_error_to_owner(socket, sharee.username) 45 | end 46 | end 47 | 48 | defp broadcast_to_owner(socket, sharee_username) do 49 | broadcast(socket, "share", %{message: "Successfully shared the file with #{sharee_username}."}) 50 | end 51 | 52 | defp broadcast_to_sharee(sharee_user_id, owner_user_id) do 53 | owner = Accounts.find_user(owner_user_id) 54 | Endpoint.broadcast("users:#{sharee_user_id}", "share", %{message: "#{owner.username} has just shared a file with you!"}) 55 | end 56 | 57 | defp broadcast_sharee_not_found(socket, username) do 58 | broadcast(socket, "share", %{message: "Couldn't find a user with #{username} username."}) 59 | end 60 | 61 | defp broadcast_error_to_owner(socket, username) do 62 | broadcast(socket, "share", %{message: "Couldn't share the file with #{username}."}) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # ElixirDripWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :elixir_drip_web, ElixirDripWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [host: "example.com", port: 80], 19 | cache_static_manifest: "priv/static/cache_manifest.json" 20 | 21 | # ## SSL Support 22 | # 23 | # To get SSL working, you will need to add the `https` key 24 | # to the previous section and set your `:url` port to 443: 25 | # 26 | # config :elixir_drip_web, ElixirDripWeb.Endpoint, 27 | # ... 28 | # url: [host: "example.com", port: 443], 29 | # https: [:inet6, 30 | # port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :elixir_drip_web, ElixirDripWeb.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :elixir_drip_web, ElixirDripWeb.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "lib/web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "lib/web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/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 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /Chapter02/apps/elixir_drip_web/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file 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 as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /Chapter08/apps/elixir_drip_web/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", { 9 | params: {token: window.userToken}, 10 | logger: (type, message, data) => { console.log(`${type}: ${message}`, data) } 11 | }) 12 | 13 | // When you connect, you'll often need to authenticate the client. 14 | // For example, imagine you have an authentication plug, `MyAuth`, 15 | // which authenticates the session and assigns a `:current_user`. 16 | // If the current user exists you can assign the user's token in 17 | // the connection for use in the layout. 18 | // 19 | // In your "lib/web/router.ex": 20 | // 21 | // pipeline :browser do 22 | // ... 23 | // plug MyAuth 24 | // plug :put_user_token 25 | // end 26 | // 27 | // defp put_user_token(conn, _) do 28 | // if current_user = conn.assigns[:current_user] do 29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 30 | // assign(conn, :user_token, token) 31 | // else 32 | // conn 33 | // end 34 | // end 35 | // 36 | // Now you need to pass this token to JavaScript. You can do so 37 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 38 | // 39 | // 40 | // 41 | // You will need to verify the user token in the "connect/2" function 42 | // in "lib/web/channels/user_socket.ex": 43 | // 44 | // def connect(%{"token" => token}, socket) do 45 | // # max_age: 1209600 is equivalent to two weeks in seconds 46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 47 | // {:ok, user_id} -> 48 | // {:ok, assign(socket, :user, user_id)} 49 | // {:error, reason} -> 50 | // :error 51 | // end 52 | // end 53 | // 54 | // Finally, pass the token on connect as below. Or remove it 55 | // from connect if you don't care about authentication. 56 | 57 | 58 | // Now that you are connected, you can join channels with a topic: 59 | // let channel = socket.channel("users:456", {}) 60 | // channel.join() 61 | // .receive("ok", resp => { console.log("Joined successfully", resp) }) 62 | // .receive("error", resp => { console.log("Unable to join", resp) }) 63 | 64 | // channel.on("ping", ({count}) => console.log("PING", count)) 65 | // channel.on("download", ({details}) => console.log("DOWNLOAD", details)) 66 | 67 | socket.connect() 68 | 69 | export default socket 70 | --------------------------------------------------------------------------------