lipu_kasi/phoenix.org

14 KiB

Project Configuration, Mix, etc

In here I de-tangle the Phoenix and Mix infrastructure for §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 §Literate Programming §org-mode application, developed within my §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 §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.

# 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/

Files which are provided from tangles will need to be managed here:

/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

This file should be checked in when it's changed, along with mix.lock

Mix Configuration (/rrix/lipu_kasi/src/branch/main/mix.exs)

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 §noweb syntax to tangle the file from multiple source code blocks so that the documentation can be placed around each important segment.

defmodule LipuKasi.MixProject do
  use Mix.Project

  <<project-definition>>
  <<application-definition>>
  <<dependencies>>
  <<mix-aliases>>
  <<elixirc-paths>>
end

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 §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!

  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

Configuration for the OTP application. Type mix help compile.app for more information.

  def application do
    [
      mod: {LipuKasi.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

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.

  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

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

  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

In :test environment

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

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

[
  import_deps: [:ecto, :phoenix],
  inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
  subdirectories: ["priv/*/migrations"]
]

This one is in priv/repo/migrations/

[
  import_deps: [:ecto_sql],
  inputs: ["*.exs"]
]

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:

use Mix.Config

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.

config :lipu_kasi,
  ecto_repos: [LipuKasi.Repo]

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.

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]

Logger configuration is simple, I haven't changed this, though I probably would like to have JSON logs some day.

config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

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.

config :phoenix, :json_library, Jason

Lastly, config.exs loads environment-specific configuration.

import_config "#{Mix.env()}.exs"

Development Configuration

use Mix.Config

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 §Arcology project right now.

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

Disable the cache and enable debugging and hot code reloading, as well as setting up the webpack development server for assets.

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__)
    ]
  ]

Configure Phoenix's LiveReload functionality, which watches some file paths and will recompile and reload the browser page if any of the files change:

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)$"
    ]
  ]

In development we don't need timestamps or other metadata in the logs, and provide more stacktrace context when there is an error.

config :logger, :console, format: "[$level] $message\n"
config :phoenix, :stacktrace_depth, 20

"Initialize plugs at runtime for faster development compilation", sure!

config :phoenix, :plug_init_mode, :runtime

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.

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

Production Configuration

use Mix.Config

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.

config :lipu_kasi, LipuKasiWeb.Endpoint,
  url: [host: "lipu.garden", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

Don't print debug messages in production.

config :logger, level: :info

Keeping this secrets file in sync is an §open thread.

import_config "prod.secret.exs"

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.

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
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
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

IDK why I need to hold on to this one.

defmodule LipuKasiWeb.LayoutView do
  use LipuKasiWeb, :view
end