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

Typespecs

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

PatternUseReturns
new/NStruct constructorBare struct (infallible) or {:ok, struct()} | {:error, _}
from_X/NParse/convert from X format{:ok, _} | {:error, _}
to_X/NConvert to X format{:ok, _} | {:error, _} (fallible) or bare (infallible)
bang!/NRaises on error, returns bareBare 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.

DomainFunctionnil →valid →invalid →
Financial amountAmount.new/1{:ok, nil}{:ok, integer}{:error, :invalid_amount}
Decimal/priceMath.to_decimal/1{:ok, nil}{:ok, Decimal.t}{:error, :invalid_decimal}
Plain integerMath.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).

TypeUseExample
Amount.t()Bare integer — struct fields, internal calculationstotal: 0, Amount.new("500")
Coin.t()Asset + amount pair — user-facing, cross-protocolCoin.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:

AtomWhen
:invalid_amountAmount.new/1 fails
:invalid_integerMath.to_integer/1 fails
:invalid_decimalMath.to_decimal/1 fails
:invalid_idID format doesn't match expected pattern
:invalid_denomDenom not recognized by Assets.from_denom/1
:invalid_coin_formatCoin.parse/1 cannot tokenize the input
:invalid_eventEvents.parse/1 given a non-event shape
:invalid_attrsSub-event new/1 got a map missing required keys
:not_foundResource lookup returns nothing
:not_supportedOperation valid in shape but disallowed (e.g. Assets.to_secured/1 on a THOR-chain asset)
:unknown_protocolDeployments saw an on-chain contract with no protocol mapping
:no_pricePrices.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 with new/1 constructors

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 nil must include | nil in @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 @spec on every public function — dialyzer infers, but explicit specs catch contract mismatches early