lib/chronicle.ex

# Copyright (c) Cratis. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.

defmodule Chronicle do
  @moduledoc """
  Idiomatic Elixir client for the Chronicle event-sourcing platform.

  Chronicle is an event-sourcing kernel that stores domain events and projects
  them into read models. This library provides an idiomatic Elixir interface
  built on top of the Chronicle gRPC API.

  ## Quick Start

  Add the dependency to your `mix.exs`:

      {:chronicle, "~> 0.1", organization: "cratis"}

  Start `Chronicle.Client` in your application supervision tree:

      defmodule MyApp.Application do
        use Application

        def start(_type, _args) do
          children = [
            {Chronicle.Client,
              connection_string: "chronicle://localhost:35000?disableTls=true",
              event_store: "my-app",
              event_types: [MyApp.Events.AccountOpened, MyApp.Events.FundsDeposited],
              reactors: [MyApp.Reactors.NotificationReactor],
              reducers: [MyApp.Reducers.AccountReducer]}
          ]

          Supervisor.start_link(children, strategy: :one_for_one)
        end
      end

  ## Defining Event Types

      defmodule MyApp.Events.AccountOpened do
        use Chronicle.EventType, id: "account-opened-v1"
        defstruct [:account_id, :owner_name, :initial_balance]
      end

  ## Appending Events

      Chronicle.append("account-42", %MyApp.Events.AccountOpened{
        account_id: "account-42",
        owner_name: "Alice",
        initial_balance: 1000
      })

  ## Reading Read Models

      {:ok, account} = Chronicle.read_model(MyApp.ReadModels.Account, "account-42")

  ## Defining Reactors

      defmodule MyApp.Reactors.NotificationReactor do
        use Chronicle.Reactor

        @handles MyApp.Events.AccountOpened

        @impl true
        def handle(%MyApp.Events.AccountOpened{} = event, _context) do
          MyApp.Mailer.welcome(event.owner_name)
          :ok
        end
      end

  ## Defining Reducers

      defmodule MyApp.Reducers.AccountReducer do
        use Chronicle.Reducer, model: MyApp.ReadModels.Account

        @handles MyApp.Events.AccountOpened

        @impl true
        def reduce(%MyApp.Events.AccountOpened{} = event, _model, _context) do
          %MyApp.ReadModels.Account{
            account_id: event.account_id,
            owner_name: event.owner_name,
            balance: event.initial_balance
          }
        end
      end

  ## Modules

    * `Chronicle.Client` — the main supervisor; start it in your supervision tree
    * `Chronicle.EventType` — macro for defining event types
    * `Chronicle.Reactor` — behaviour for event reactors
    * `Chronicle.Reducer` — behaviour for read model reducers
    * `Chronicle.ReadModel` — macro for read model structs with embedded projection DSL
    * `Chronicle.EventLog` — append and query events
    * `Chronicle.ReadModels` — query read model instances
    * `Chronicle.Connections.ConnectionString` — parse and format connection strings
    * `Chronicle.Connections.Connection` — resilient gRPC channel management
  """

  @doc """
  Appends a single event to the event log for the given event source.

  Delegates to `Chronicle.EventLog.append/3`.

  ## Options

    * `:client` — the client name (default: `Chronicle.Client`)
    * `:namespace` — overrides the client's default namespace
    * `:tags` — list of tag strings
    * `:subject` — the identity subject string
  """
  @spec append(String.t(), struct(), keyword()) :: :ok | {:error, term()}
  defdelegate append(event_source_id, event, opts \\ []), to: Chronicle.EventLog

  @doc """
  Appends multiple events to the event log for the given event source.

  Delegates to `Chronicle.EventLog.append_many/3`.
  """
  @spec append_many(String.t(), [struct()], keyword()) :: :ok | {:error, term()}
  defdelegate append_many(event_source_id, events, opts \\ []), to: Chronicle.EventLog

  @doc """
  Fetches a read model instance by its key (typically an event source ID).

  Delegates to `Chronicle.ReadModels.get/3`.

  Returns `{:ok, model_struct}` on success, or `{:ok, nil}` if not found.
  """
  @spec read_model(module(), String.t(), keyword()) :: {:ok, struct() | nil} | {:error, term()}
  defdelegate read_model(model_module, key, opts \\ []), to: Chronicle.ReadModels, as: :get

  @doc """
  Returns all instances of the given read model.

  Delegates to `Chronicle.ReadModels.all/2`.
  """
  @spec all(module(), keyword()) :: {:ok, [struct()]} | {:error, term()}
  defdelegate all(model_module, opts \\ []), to: Chronicle.ReadModels
end