Aliases
Always use explicit, fully qualified aliases. Never use the {} grouping syntax. Alphabetical order within each group.
# good
alias Rujira.Fin.Events.Submit
alias Rujira.Fin.Events.Trade
# bad — grouped, unordered
alias Rujira.Fin.Events.{Trade, Submit}Return Values
Fallible functions (I/O, parsing, construction that can fail) must return {:ok, term()} | {:error, term()}.
Infallible pure functions (getters, math, formatting, predicates) return bare values.
Never return nil as a failure — use {:error, :not_found} or similar.
# fallible — I/O or parsing
def from_denom(denom), do: {:ok, asset}
def from_denom(_), do: {:error, :invalid_denom}
# infallible pure — getter
def decimals(%Asset{chain: "ETH"}), do: 18
def label(%Asset{ticker: ticker}), do: ticker
# predicate
def eq_denom?(asset, denom), do: trueTypespecs
Every public def must have @spec. Use defined types (Asset.t(), Amount.t()) not raw structs.
# good
@spec from_denom(String.t()) :: {:ok, Asset.t()} | {:error, term()}
# bad — missing spec, raw struct
def from_denom(d), do: {:ok, %Asset{...}}Naming
| Pattern | Use | Returns |
|---|---|---|
new/N | Struct constructor | Bare struct (infallible) or {:ok, struct()} | {:error, _} |
from_X/N | Parse/convert from X format | {:ok, _} | {:error, _} |
to_X/N | Convert to X format | {:ok, _} | {:error, _} (fallible) or bare (infallible) |
bang!/N | Raises on error, returns bare | Bare value |
Map Access
Use Map.get/2 instead of bracket syntax for string-keyed maps.
# good
Map.get(attrs, "key")
# bad
attrs["key"]Pattern Matching
Always prefer pattern matching in function heads over case, cond, or if inside the body.
Numeric Parsing
One function per type. nil in → {:ok, nil} out. Use with chains. Never use raw Decimal.parse or Integer.parse with {val, ""} pattern.
| Domain | Function | nil → | valid → | invalid → |
|---|---|---|---|---|
| Financial amount | Amount.new/1 | {:ok, nil} | {:ok, integer} | {:error, :invalid_amount} |
| Decimal/price | Math.to_decimal/1 | {:ok, nil} | {:ok, Decimal.t} | {:error, :invalid_decimal} |
| Plain integer | Math.to_integer/1 | {:ok, nil} | {:ok, integer} | {:error, :invalid_integer} |
Amounts vs Coins
All amounts are integers normalized to 8 decimal places (1.0 = 100_000_000).
| Type | Use | Example |
|---|---|---|
Amount.t() | Bare integer — struct fields, internal calculations | total: 0, Amount.new("500") |
Coin.t() | Asset + amount pair — user-facing, cross-protocol | Coin.new("rune", 1000) |
Use Amount.new/1 for construction. Use Coin when the asset context must travel with the value.
Struct Defaults
Every defstruct must declare explicit defaults — never use the bare [:field] syntax.
- Strings/references:
nil - Lists:
[] - Integers:
0 - Decimals:
Decimal.new(0) - Loadable associations:
:not_loaded - Enums: the most common value (e.g.
side: :base)
# good
defstruct id: nil,
items: [],
total: 0,
price: Decimal.new(0),
book: :not_loaded
# bad — all nil, no type hints
defstruct [:id, :items, :total, :price, :book]Visibility
Every public function on a resource module must be delegated from the facade (defdelegate in Rujira.Protocol), or be a deployment protocol callback (init_msg, migrate_msg, init_label), or be a new constructor. Everything else must be defp.
Error Atoms
Use consistent error atoms across the codebase:
| Atom | When |
|---|---|
:invalid_amount | Amount.new/1 fails |
:invalid_integer | Math.to_integer/1 fails |
:invalid_decimal | Math.to_decimal/1 fails |
:invalid_id | ID format doesn't match expected pattern |
:invalid_denom | Denom not recognized by Assets.from_denom/1 |
:invalid_coin_format | Coin.parse/1 cannot tokenize the input |
:invalid_event | Events.parse/1 given a non-event shape |
:invalid_attrs | Sub-event new/1 got a map missing required keys |
:not_found | Resource lookup returns nothing |
:not_supported | Operation valid in shape but disallowed (e.g. Assets.to_secured/1 on a THOR-chain asset) |
:unknown_protocol | Deployments saw an on-chain contract with no protocol mapping |
:no_price | Prices.get/1 could not resolve an oracle or FIN mid-price |
Logger
Always use Rujira.Logger — never raw Logger. Pass __MODULE__ as the first argument.
Logger.error(__MODULE__, "load #{pair.address} #{inspect(err)}")
Logger.info(__MODULE__, "refreshed #{count} pairs")Section Comments
Organize resource modules with these section headers:
# --- Struct ---
# --- Construction ---
# --- Queries ---
# --- Calculations --- # if applicable
# --- Deployment protocol --- # if applicable
# --- Private ---Structure
- 1 module per file, 1 responsibility per module
- Structs with multiple sub-concerns get their own folder
- Event structs live in
events/subfolder withnew/1constructors
Verification
All of these must pass before merge:
mix format --check-formatted
mix compile --warnings-as-errors
mix test
mix credo --strict
mix dialyzer
Dialyzer
Typespecs must be accurate — dialyzer warnings are treated as errors. Common pitfalls:
Struct fields that default to
nilmust include| nilin@type(e.g.id: String.t() | nil)- Return types must match all code paths (e.g. if a function can return
info: nil, the type must allow it) - Use
@specon every public function — dialyzer infers, but explicit specs catch contract mismatches early