lipu_kasi/phoenix.org

431 lines
14 KiB
Org Mode

#+TITLE: Project Configuration, Mix, etc
In here I de-tangle the Phoenix and Mix infrastructure for [[file:../lipu_kasi.org][§Lipu Kasi]]. In general, when I work in a code module it should get moved in to its own file, but I don't know how to address that for these "infrastructure" elements. For now they'll live here. I also need to design some build systems and build scripts, and set up my =.gitignore=
* Project =.gitignore= file
This project is intended to be developed as a [[file:../literate_programming.org][§Literate Programming]] [[file:../org-mode.org][§org-mode]] application, developed within my [[file:../org-roam.org][§org-roam]] wiki. The project is its own subdirectory under my =org-roam-directory= and it can be on yours as well, or provided standalone.
The repo will contain docs and assets and a build script that will call up an [[file:../Emacs.org][§Emacs]] that will "tangle" the files out and build the application. It will *not* contain the compiled code checked in, the docs must be considered the source of truth. We'll have facilities to "de-tangle" the source code eventually to aide contribution from folks who aren't running Emacs. For now, though, I'm gonna be a bastard 😄
This starts with a .gitignore, =phx.new= provides one that I will expand. When updating this, the order of operation is to delete the file from the index, update this, tangle it, and then commit. it's a bit of a mess, maybe detangling is better.
#+begin_src conf :tangle .gitignore
# elixir and mix outputs
/_build/
/cover/
/deps/
/doc/
/.fetch
# debug and dumps
erl_crash.dump
npm-debug.log
# Archives
,*.ez
lipu_kasi-*.tar
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
/priv/static/
#+end_src
Files which are provided from tangles will need to be managed here:
#+begin_src conf :tangle .gitignore
/mix.exs
# /mix.lock is checked in.
.formatter.exs
/config/*.exs
!/config/prod.secret.exs
# LipuKasi module
/lib/lipu_kasi.ex
/lib/lipu_kasi/*.ex
# LipuKasiWeb module
/lib/lipu_kasi_web.ex
/lib/lipu_kasi_web/*.ex
# Channels, Controllers, Views, Templates
/lib/lipu_kasi_web/channels/
/lib/lipu_kasi_web/controllers/
/lib/lipu_kasi_web/views/
/lib/lipu_kasi_web/templates/
/test/lipu_kasi_web/controllers/
/test/lipu_kasi_web/views/
/test/test_helper.exs
/test/support/
# Assets
/assets/js/app.js
/assets/js/foundation.js
/assets/js/sw.js
#+end_src
This file should be checked in when it's changed, along with =mix.lock=
* Mix Configuration ([[./mix.exs]])
:PROPERTIES:
:ID: 11e43150-7556-45a9-978e-05e942d40a68
:END:
Mix is the main tool interface for Elixir, an Elixir app almost always has a mix.exs in its root directory, and this is ours: it uses [[file:../noweb_syntax.org][§noweb syntax]] to tangle the file from multiple source code blocks so that the documentation can be placed around each important segment.
#+begin_src elixir :tangle mix.exs :noweb tangle
defmodule LipuKasi.MixProject do
use Mix.Project
<<project-definition>>
<<application-definition>>
<<dependencies>>
<<mix-aliases>>
<<elixirc-paths>>
end
#+end_src
Basic Mix project configuration, appliation version, required elixir version, similar things are returned by =project/0=, this calls in to some other functions in the module. My [[file:../fedora_linux.org][§Fedora Linux]] host has Elixir 1.5 installed on it now, I assume I'm going to have to update this sooner or later, though!
#+begin_src elixir :noweb-ref project-definition
def project do
[
app: :lipu_kasi,
version: "0.1.0",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
#+end_src
Configuration for the OTP application. Type =mix help compile.app= for more information.
#+begin_src elixir :noweb-ref application-definition
def application do
[
mod: {LipuKasi.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
#+end_src
The project dependencies are specified here, this is a simple Phoenix web app for now, we'll probably only need a few other things to ship this.
#+begin_src elixir :noweb-ref dependencies
defp deps do
[
{:phoenix, "~> 1.4.10"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.1"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}
]
end
#+end_src
Aliases are shortcuts or tasks specific to the current project. For example, to create, migrate and run the seeds file at once: =mix ecto.setup=
#+begin_src elixir :noweb-ref mix-aliases
defp aliases do
[
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end
#+end_src
In =:test= environment
#+begin_src elixir :noweb-ref elixirc-paths
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
#+end_src
* Elixir Formatter Configurations
There are two =.formatter.exs= files, one in the project root, and one stored with the migrations. I'm not sure if this will be used by the org-babel programming, but I'll provide them for now so that I can =git clean=
#+begin_src elixir :tangle .formatter.exs
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]
#+end_src
This one is in =priv/repo/migrations/=
#+begin_src elixir :tangle priv/repo/migrations/.formatter.exs
[
import_deps: [:ecto_sql],
inputs: ["*.exs"]
]
#+end_src
* Runtime Configurations
This is *not* noweb-tangled since it's not wrapped in a =defmodule=, just tangled line-by-line and concatenated.
This file is responsible for configuring your application and its dependencies with the aid of the Mix.Config module:
#+begin_src elixir :tangle config/config.exs
use Mix.Config
#+end_src
We've just got the one Ecto repo, it might be all we need. I don't expect this application to have a lot of weight to it.
#+begin_src elixir :tangle config/config.exs
config :lipu_kasi,
ecto_repos: [LipuKasi.Repo]
#+end_src
The Phoenix endpoint configuration is pretty straightforward, we use Phoenix's PostgreSQL PubSub adapter these days to have a durable message passing channel between frontend and backend. And this damned secret key will have to be provided for production builds somehow, that'll be fun.
#+begin_src elixir :tangle config/config.exs
config :lipu_kasi, LipuKasiWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "qjGyFdguDBSm8O7R4TI5M084mtgZ2vPE5p7a/2kTMuDCaPRUz9JiFdLK1RNUBggy",
render_errors: [view: LipuKasiWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: LipuKasi.PubSub, adapter: Phoenix.PubSub.PG2]
#+end_src
Logger configuration is simple, I haven't changed this, though I probably would like to have JSON logs some day.
#+begin_src elixir :tangle config/config.exs
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
#+end_src
Use Jason for the Phoenix JSON library, this is also a default. I've used Poison in the past for personal projects, but I don't have anything resembling strong feelings.
#+begin_src elixir :tangle config/config.exs
config :phoenix, :json_library, Jason
#+end_src
Lastly, config.exs loads environment-specific configuration.
#+begin_src elixir :tangle config/config.exs
import_config "#{Mix.env()}.exs"
#+end_src
** Development Configuration
#+begin_src elixir :tangle config/dev.exs
use Mix.Config
#+end_src
The database configuration for local development is pretty simple, a local postgres instance is required, which makes it a bit heavy to do on older hardware, but this is an Elixir decision not mine. There is a sqlite for ecto2, but I only use that in the [[file:../arcology.org][§Arcology]] project right now.
#+begin_src elixir :tangle config/dev.exs
config :lipu_kasi, LipuKasi.Repo,
username: "postgres",
password: "postgres",
database: "lipu_kasi_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
#+end_src
Disable the cache and enable debugging and hot code reloading, as well as setting up the webpack development server for assets.
#+begin_src elixir :tangle config/dev.exs
config :lipu_kasi, LipuKasiWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
"development",
"--watch-stdin",
cd: Path.expand("../assets", __DIR__)
]
]
#+end_src
Configure Phoenix's LiveReload functionality, which watches some file paths and will recompile and reload the browser page if any of the files change:
#+begin_src elixir :tangle config/dev.exs
config :lipu_kasi, LipuKasiWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/lipu_kasi_web/{live,views}/.*(ex)$",
~r"lib/lipu_kasi_web/templates/.*(eex)$"
]
]
#+end_src
In development we don't need timestamps or other metadata in the logs, and provide more stacktrace context when there is an error.
#+begin_src elixir :tangle config/dev.exs
config :logger, :console, format: "[$level] $message\n"
config :phoenix, :stacktrace_depth, 20
#+end_src
"Initialize plugs at runtime for faster development compilation", sure!
#+begin_src elixir :tangle config/dev.exs
config :phoenix, :plug_init_mode, :runtime
#+end_src
** Testing Configuration
Testing is pretty simple and similar to development, just less stuff going on. The server doesn't run, the logger is less loud, warnings and errors only.
#+begin_src elixir :tangle config/test.exs
use Mix.Config
config :lipu_kasi, LipuKasi.Repo,
username: "postgres",
password: "postgres",
database: "lipu_kasi_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
config :lipu_kasi, LipuKasiWeb.Endpoint,
http: [port: 4002],
server: false
config :logger, level: :warn
#+end_src
** Production Configuration
#+begin_src elixir :tangle config/prod.exs
use Mix.Config
#+end_src
Note we also include the path to a cache manifest containing the digested version of static files. This manifest is generated by the `mix phx.digest` task, which you should run after static files are built and before starting your production server.
#+begin_src elixir :tangle config/prod.exs
config :lipu_kasi, LipuKasiWeb.Endpoint,
url: [host: "lipu.garden", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
#+end_src
Don't print debug messages in production.
#+begin_src elixir :tangle config/prod.exs
config :logger, level: :info
#+end_src
Keeping this secrets file in sync is an [[file:../open_threads.org][§open thread]].
#+begin_src elixir :tangle config/prod.exs
import_config "prod.secret.exs"
#+end_src
When deployed, we'll probably do SSL termination in =nginx=, it's not so hard.
* Error Views and other base views
These are default, I'll change them some day.
#+begin_src elixir :tangle lib/lipu_kasi_web/views/error_view.ex
defmodule LipuKasiWeb.ErrorView do
use LipuKasiWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
#+end_src
#+begin_src elixir :tangle lib/lipu_kasi_web/views/error_helpers.ex
defmodule LipuKasiWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error), class: "help-block")
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(LipuKasiWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(LipuKasiWeb.Gettext, "errors", msg, opts)
end
end
end
#+end_src
#+begin_src elixir :tangle test/lipu_kasi_web/views/error_view_test.exs :mkdirp yes
defmodule LipuKasiWeb.ErrorViewTest do
use LipuKasiWeb.ConnCase, async: true
# Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View
test "renders 404.html" do
assert render_to_string(LipuKasiWeb.ErrorView, "404.html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(LipuKasiWeb.ErrorView, "500.html", []) == "Internal Server Error"
end
end
#+end_src
IDK why I need to hold on to this one.
#+begin_src elixir :tangle lib/lipu_kasi_web/views/layout_view.ex :mkdirp yes
defmodule LipuKasiWeb.LayoutView do
use LipuKasiWeb, :view
end
#+end_src