├── 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 | | Full path: |
6 | <%= @file.full_path %> |
7 |
8 |
9 | | Uploaded at: |
10 | <%= @file.uploaded_at %> |
11 |
12 |
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 |
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 |
24 |
25 |
<%= get_flash(@conn, :info) %>
26 |
<%= get_flash(@conn, :error) %>
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 |
28 |
29 | <%= if assigns[:current_user] do %>
30 |
Online users:
31 |
32 | <% end %>
33 |
34 |
<%= get_flash(@conn, :info) %>
35 |
36 |
<%= get_flash(@conn, :error) %>
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 |
--------------------------------------------------------------------------------