event_engine 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +210 -399
  4. data/Rakefile +0 -1
  5. data/lib/event_engine/catalog_entry.rb +84 -0
  6. data/lib/event_engine/configuration.rb +9 -9
  7. data/lib/event_engine/definition_publisher.rb +22 -0
  8. data/lib/event_engine/event.rb +0 -8
  9. data/lib/event_engine/event_builder.rb +0 -8
  10. data/lib/event_engine/event_schema.rb +32 -57
  11. data/lib/event_engine/event_schema_json_loader.rb +19 -0
  12. data/lib/event_engine/handler_registry.rb +4 -4
  13. data/lib/event_engine/invalid_rules_error.rb +8 -0
  14. data/lib/event_engine/processing_rules.rb +39 -0
  15. data/lib/event_engine/processor_registry.rb +19 -0
  16. data/lib/event_engine/processor_resolver.rb +16 -0
  17. data/lib/event_engine/railtie.rb +54 -4
  18. data/lib/event_engine/rules_file.rb +14 -0
  19. data/lib/event_engine/schema_catalog_builder.rb +18 -0
  20. data/lib/event_engine/schema_registry.rb +7 -58
  21. data/lib/event_engine/unregistered_processor_error.rb +8 -0
  22. data/lib/event_engine/unroutable_event_error.rb +9 -0
  23. data/lib/event_engine/unrouted_events_error.rb +9 -0
  24. data/lib/event_engine/version.rb +1 -1
  25. data/lib/event_engine.rb +117 -123
  26. data/lib/tasks/event_engine_catalog.rake +15 -6
  27. data/lib/tasks/event_engine_rules.rake +10 -0
  28. data/the_local/agents/event_engine-develop.md +154 -0
  29. data/the_local/agents/event_engine-info.md +82 -0
  30. data/the_local/agents/event_engine-install.md +99 -0
  31. data/the_local/interface.yml +27 -0
  32. metadata +45 -48
  33. data/app/assets/config/event_engine_manifest.js +0 -1
  34. data/app/assets/stylesheets/event_engine/application.css +0 -15
  35. data/app/controllers/event_engine/application_controller.rb +0 -4
  36. data/app/helpers/event_engine/application_helper.rb +0 -4
  37. data/app/jobs/event_engine/application_job.rb +0 -4
  38. data/app/mailers/event_engine/application_mailer.rb +0 -6
  39. data/app/models/event_engine/application_record.rb +0 -5
  40. data/app/views/layouts/event_engine/application.html.erb +0 -15
  41. data/config/routes.rb +0 -2
  42. data/lib/event_engine/definition_loader.rb +0 -26
  43. data/lib/event_engine/dsl_compiler.rb +0 -50
  44. data/lib/event_engine/engine.rb +0 -56
  45. data/lib/event_engine/event_definition/inputs.rb +0 -43
  46. data/lib/event_engine/event_definition/payloads.rb +0 -47
  47. data/lib/event_engine/event_definition/schemas.rb +0 -158
  48. data/lib/event_engine/event_definition/validation.rb +0 -18
  49. data/lib/event_engine/event_definition.rb +0 -76
  50. data/lib/event_engine/event_schema_dumper.rb +0 -13
  51. data/lib/event_engine/event_schema_loader.rb +0 -37
  52. data/lib/event_engine/event_schema_merger.rb +0 -62
  53. data/lib/event_engine/event_schema_writer.rb +0 -47
  54. data/lib/event_engine/lifecycle_definition.rb +0 -86
  55. data/lib/event_engine/process_type.rb +0 -26
  56. data/lib/event_engine/reference/guide.md +0 -129
  57. data/lib/event_engine/reference.rb +0 -16
  58. data/lib/event_engine/schema_catalog.rb +0 -50
  59. data/lib/event_engine/schema_compatibility.rb +0 -50
  60. data/lib/event_engine/schema_diff.rb +0 -35
  61. data/lib/event_engine/schema_drift_guard.rb +0 -38
  62. data/lib/event_engine/subject_registry.rb +0 -40
  63. data/lib/event_engine/the_local/agents/event_engine-develop.md +0 -142
  64. data/lib/event_engine/the_local/agents/event_engine-info.md +0 -140
  65. data/lib/event_engine/the_local/agents/event_engine-install.md +0 -140
  66. data/lib/event_engine/the_local.rb +0 -55
  67. data/lib/generators/event_engine/install_generator.rb +0 -31
  68. data/lib/generators/event_engine/templates/event_schema.rb +0 -10
  69. data/lib/generators/event_engine/templates/initializer.rb +0 -4
  70. data/lib/tasks/event_engine_schema.rake +0 -82
  71. data/lib/tasks/event_engine_schema_check.rake +0 -20
  72. data/lib/tasks/event_engine_tasks.rake +0 -4
@@ -1,50 +0,0 @@
1
- module EventEngine
2
- class SchemaCatalog
3
- def initialize(schema_registry:, subject_registry:)
4
- @schema_registry = schema_registry
5
- @subject_registry = subject_registry
6
- end
7
-
8
- def to_markdown
9
- (["# Event Catalog"] + event_sections).join("\n\n") + "\n"
10
- end
11
-
12
- private
13
-
14
- def event_sections
15
- @schema_registry.events.map do |event|
16
- section(@schema_registry.latest_for(event))
17
- end
18
- end
19
-
20
- def section(schema)
21
- ([
22
- "## #{schema.event_name} (v#{schema.event_version})",
23
- "- Type: #{schema.event_type}",
24
- subject_line(schema)
25
- ] + payload_lines(schema)).compact.join("\n")
26
- end
27
-
28
- def payload_lines(schema)
29
- return [] if schema.payload_fields.empty?
30
-
31
- ["- Payload:"] + schema.payload_fields.map do |field|
32
- " - #{field[:name]} (#{field[:required] ? "required" : "optional"})"
33
- end
34
- end
35
-
36
- def subject_line(schema)
37
- return nil unless schema.subject
38
-
39
- details = subject_details(schema.subject)
40
- details.empty? ? "- Subject: #{schema.subject}" : "- Subject: #{schema.subject} (#{details})"
41
- end
42
-
43
- def subject_details(name)
44
- registered = @subject_registry[name]
45
- return "" unless registered
46
-
47
- registered.metadata.map { |key, value| "#{key}: #{value}" }.join(", ")
48
- end
49
- end
50
- end
@@ -1,50 +0,0 @@
1
- module EventEngine
2
- class SchemaCompatibility
3
- def self.violations(old_registry:, new_registry:)
4
- new_registry.events.flat_map do |event|
5
- previous = old_registry.latest_for(event)
6
- next [] unless previous
7
-
8
- current = new_registry.latest_for(event)
9
- new(old: previous, new: current).breaking_changes.map do |change|
10
- "#{event}: #{change}"
11
- end
12
- end
13
- end
14
-
15
- def initialize(old:, new:)
16
- @old = old
17
- @new = new
18
- end
19
-
20
- def breaking_changes
21
- removed_required_fields + newly_required_fields
22
- end
23
-
24
- private
25
-
26
- def removed_required_fields
27
- (required_names(@old) - field_names(@new)).map do |name|
28
- "required payload field removed: #{name}"
29
- end
30
- end
31
-
32
- def newly_required_fields
33
- (optional_names(@old) & required_names(@new)).map do |name|
34
- "payload field became required: #{name}"
35
- end
36
- end
37
-
38
- def field_names(schema)
39
- schema.payload_fields.map { |field| field[:name] }
40
- end
41
-
42
- def required_names(schema)
43
- schema.payload_fields.select { |field| field[:required] }.map { |field| field[:name] }
44
- end
45
-
46
- def optional_names(schema)
47
- schema.payload_fields.reject { |field| field[:required] }.map { |field| field[:name] }
48
- end
49
- end
50
- end
@@ -1,35 +0,0 @@
1
- module EventEngine
2
- class SchemaDiff
3
- def initialize(expected:, actual:)
4
- @expected = expected
5
- @actual = actual
6
- end
7
-
8
- def changed?
9
- @expected != @actual
10
- end
11
-
12
- def to_s
13
- expected_lines = @expected.lines
14
- actual_lines = @actual.lines
15
-
16
- Array.new([ expected_lines.size, actual_lines.size ].max) do |index|
17
- line_diff(expected_lines[index], actual_lines[index])
18
- end.compact.join
19
- end
20
-
21
- private
22
-
23
- def line_diff(expected_line, actual_line)
24
- return " #{expected_line}" if expected_line == actual_line
25
-
26
- [ marked("-", expected_line), marked("+", actual_line) ].compact.join
27
- end
28
-
29
- def marked(sign, line)
30
- return nil if line.nil?
31
-
32
- "#{sign}#{line}"
33
- end
34
- end
35
- end
@@ -1,38 +0,0 @@
1
- require "event_engine/schema_diff"
2
-
3
- module EventEngine
4
- class SchemaDriftGuard
5
- class DriftError < StandardError; end
6
-
7
- def self.check!(schema_path:, definitions:)
8
- raise DriftError, "Schema file does not exist: #{schema_path}" unless File.exist?(schema_path)
9
-
10
- committed = File.read(schema_path)
11
- regenerated = dump_to_string(definitions)
12
-
13
- return true if committed == regenerated
14
-
15
- raise DriftError, <<~MSG
16
- EventEngine schema drift detected.
17
-
18
- The DSL definitions do not match #{schema_path}.
19
-
20
- #{SchemaDiff.new(expected: committed, actual: regenerated)}
21
- Run:
22
- bin/rails event_engine:schema:dump
23
-
24
- And commit the updated schema file.
25
- MSG
26
- end
27
-
28
- def self.dump_to_string(definitions)
29
- Tempfile.create("event_schema") do |file|
30
- EventEngine::EventSchemaDumper.dump!(
31
- definitions: definitions,
32
- path: file.path
33
- )
34
- File.read(file.path)
35
- end
36
- end
37
- end
38
- end
@@ -1,40 +0,0 @@
1
- module EventEngine
2
- class SubjectRegistry
3
- class UnknownSubjectError < StandardError; end
4
-
5
- class Subject
6
- attr_reader :name, :metadata
7
-
8
- def initialize(name, **metadata)
9
- @name = name
10
- @metadata = metadata
11
- end
12
- end
13
-
14
- def self.define(&block)
15
- registry = new
16
- registry.instance_eval(&block) if block
17
- registry
18
- end
19
-
20
- def initialize
21
- @subjects = {}
22
- end
23
-
24
- def subject(name, **metadata)
25
- @subjects[name] = Subject.new(name, **metadata)
26
- end
27
-
28
- def [](name)
29
- @subjects[name]
30
- end
31
-
32
- def registered?(name)
33
- @subjects.key?(name)
34
- end
35
-
36
- def names
37
- @subjects.keys
38
- end
39
- end
40
- end
@@ -1,142 +0,0 @@
1
- ---
2
- name: event_engine-develop
3
- description: Use PROACTIVELY for any EventEngine work — defining events, choosing process_type, emitting, and keeping the committed schema in sync. MUST BE USED instead of hand-writing event plumbing.
4
- tools: Read, Write, Edit, Grep
5
- ---
6
-
7
- You build EventEngine events following the reference's conventions: one
8
- EventDefinition class per event in app/event_definitions/, payloads composed from
9
- inputs, process_type set explicitly, emitted through the generated
10
- EventEngine.<event_name> helpers. After any definition change you run
11
- `bin/rails event_engine:schema:dump` and commit db/event_schema.rb, keeping
12
- event_engine:schema_check green. You keep handlers idempotent.
13
-
14
- ## EventEngine
15
-
16
- > **DO NOT** explore the event_engine gem source code. This reference is the
17
- > complete user-facing API, embedded verbatim into every event_engine local so
18
- > their guidance never drifts. Keep it the single source of truth.
19
-
20
- EventEngine is a Rails engine for defining domain events as declarative classes,
21
- compiling them to a committed schema, emitting them through generated helpers, and
22
- dispatching them to registered handlers. Core builds and routes events; it ships no
23
- handlers of its own. Durable delivery, an event store, and ready-made subscriber
24
- classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
25
- `event_engine-subscribers`) — this reference covers core only.
26
-
27
- ### What it offers
28
-
29
- **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
30
-
31
- ```ruby
32
- class CowFed < EventEngine::EventDefinition
33
- event_name :cow_fed # the event's identity (required)
34
- event_type :domain # classification, e.g. :domain (required)
35
- process_type :durable # routing type (optional; set it explicitly)
36
-
37
- input :cow # a required input
38
- optional_input :farmer # an optional input
39
-
40
- required_payload :weight, from: :cow, attr: :weight
41
- optional_payload :farmer_name, from: :farmer, attr: :name
42
- end
43
- ```
44
-
45
- | DSL method | Purpose |
46
- |---|---|
47
- | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
48
- | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
49
- | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
50
- | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
51
- | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
52
- | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
53
-
54
- Duplicate input names raise `ArgumentError`; payload `from:` must reference a
55
- declared input.
56
-
57
- **process_type** — core stamps this symbol onto every emitted event but does not act
58
- on it. Which handlers receive an event is decided by each handler's `levels:`. The
59
- values:
60
-
61
- | value | intent |
62
- |---|---|
63
- | `:inline` | handled in-process, synchronously |
64
- | `:background` | handled in-process, via a background job |
65
- | `:durable` | handled when a durable outbox drains |
66
- | `:broker` | published to an external transport |
67
- | `:telemetry` | metrics / observability handlers |
68
- | `:sourced` | an append-only event store |
69
-
70
- The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
71
- etc. their behavior; core just routes to whatever is registered. If `process_type`
72
- is omitted it is `nil` — set it explicitly so routing intent is clear.
73
-
74
- **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
75
-
76
- ```ruby
77
- EventEngine.cow_fed(
78
- cow: cow, farmer: farmer, # declared inputs, by name
79
- occurred_at: Time.current, # optional, defaults to now
80
- metadata: { request_id: "abc" }, # optional
81
- idempotency_key: "…", # optional, defaults to a UUID
82
- aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
83
- event_version: 1 # optional, defaults to the latest schema version
84
- )
85
- ```
86
-
87
- Missing a required input, or passing an unknown one, raises `ArgumentError`. The
88
- event's `payload` is symbol-keyed.
89
-
90
- **Register handlers** — a handler is any object responding to `call(event)`:
91
-
92
- ```ruby
93
- EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
94
- EventEngine.dispatch(event) # fan an event out (emit helpers call this)
95
- EventEngine.reset_handlers! # clear all handlers
96
- ```
97
-
98
- Handlers run synchronously in registration order; if one raises, the rest don't run.
99
- Keep handlers idempotent.
100
-
101
- **Configure** — `config/initializers/event_engine.rb`, logger only:
102
-
103
- ```ruby
104
- EventEngine.configure { |config| config.logger = Rails.logger }
105
- ```
106
-
107
- **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
108
- is authoritative at boot:
109
-
110
- ```bash
111
- bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
112
- bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
113
- ```
114
-
115
- A new event is version 1; changing an event's identity or payload bumps its version.
116
- Changing only `process_type` does not bump the version.
117
-
118
- ### Install
119
-
120
- 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
121
- 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
122
- `config/initializers/event_engine.rb`.
123
- 3. Define events as classes in `app/event_definitions/`.
124
- 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
125
- 5. Set `config.logger` in the initializer if you want something other than the default.
126
-
127
- Durable delivery, an event store, and prebuilt subscriber classes are separate gems
128
- (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
129
- when you need them and follow their own setup.
130
-
131
- ### EventEngine conventions
132
-
133
- - Define one `EventDefinition` class per event in `app/event_definitions/`; never
134
- hand-build event hashes.
135
- - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
136
- raw payload hashes to the emit helper.
137
- - Always set `process_type` explicitly so routing intent is clear.
138
- - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
139
- declared inputs.
140
- - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
141
- definition change; keep `event_engine:schema_check` green in CI.
142
- - Keep handlers and subscribers idempotent.
@@ -1,140 +0,0 @@
1
- ---
2
- name: event_engine-info
3
- description: Use to learn what EventEngine offers — the event-definition DSL, process_type, emitting, handlers, and the schema workflow.
4
- tools: Read
5
- ---
6
-
7
- You explain how EventEngine works and how to use it, answering only from the
8
- reference: defining events, process_type routing, emitting through the generated
9
- helpers, registering handlers, and the schema dump/check workflow. You make no
10
- changes.
11
-
12
- ## EventEngine
13
-
14
- > **DO NOT** explore the event_engine gem source code. This reference is the
15
- > complete user-facing API, embedded verbatim into every event_engine local so
16
- > their guidance never drifts. Keep it the single source of truth.
17
-
18
- EventEngine is a Rails engine for defining domain events as declarative classes,
19
- compiling them to a committed schema, emitting them through generated helpers, and
20
- dispatching them to registered handlers. Core builds and routes events; it ships no
21
- handlers of its own. Durable delivery, an event store, and ready-made subscriber
22
- classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
23
- `event_engine-subscribers`) — this reference covers core only.
24
-
25
- ### What it offers
26
-
27
- **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
28
-
29
- ```ruby
30
- class CowFed < EventEngine::EventDefinition
31
- event_name :cow_fed # the event's identity (required)
32
- event_type :domain # classification, e.g. :domain (required)
33
- process_type :durable # routing type (optional; set it explicitly)
34
-
35
- input :cow # a required input
36
- optional_input :farmer # an optional input
37
-
38
- required_payload :weight, from: :cow, attr: :weight
39
- optional_payload :farmer_name, from: :farmer, attr: :name
40
- end
41
- ```
42
-
43
- | DSL method | Purpose |
44
- |---|---|
45
- | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
46
- | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
47
- | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
48
- | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
49
- | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
50
- | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
51
-
52
- Duplicate input names raise `ArgumentError`; payload `from:` must reference a
53
- declared input.
54
-
55
- **process_type** — core stamps this symbol onto every emitted event but does not act
56
- on it. Which handlers receive an event is decided by each handler's `levels:`. The
57
- values:
58
-
59
- | value | intent |
60
- |---|---|
61
- | `:inline` | handled in-process, synchronously |
62
- | `:background` | handled in-process, via a background job |
63
- | `:durable` | handled when a durable outbox drains |
64
- | `:broker` | published to an external transport |
65
- | `:telemetry` | metrics / observability handlers |
66
- | `:sourced` | an append-only event store |
67
-
68
- The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
69
- etc. their behavior; core just routes to whatever is registered. If `process_type`
70
- is omitted it is `nil` — set it explicitly so routing intent is clear.
71
-
72
- **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
73
-
74
- ```ruby
75
- EventEngine.cow_fed(
76
- cow: cow, farmer: farmer, # declared inputs, by name
77
- occurred_at: Time.current, # optional, defaults to now
78
- metadata: { request_id: "abc" }, # optional
79
- idempotency_key: "…", # optional, defaults to a UUID
80
- aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
81
- event_version: 1 # optional, defaults to the latest schema version
82
- )
83
- ```
84
-
85
- Missing a required input, or passing an unknown one, raises `ArgumentError`. The
86
- event's `payload` is symbol-keyed.
87
-
88
- **Register handlers** — a handler is any object responding to `call(event)`:
89
-
90
- ```ruby
91
- EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
92
- EventEngine.dispatch(event) # fan an event out (emit helpers call this)
93
- EventEngine.reset_handlers! # clear all handlers
94
- ```
95
-
96
- Handlers run synchronously in registration order; if one raises, the rest don't run.
97
- Keep handlers idempotent.
98
-
99
- **Configure** — `config/initializers/event_engine.rb`, logger only:
100
-
101
- ```ruby
102
- EventEngine.configure { |config| config.logger = Rails.logger }
103
- ```
104
-
105
- **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
106
- is authoritative at boot:
107
-
108
- ```bash
109
- bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
110
- bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
111
- ```
112
-
113
- A new event is version 1; changing an event's identity or payload bumps its version.
114
- Changing only `process_type` does not bump the version.
115
-
116
- ### Install
117
-
118
- 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
119
- 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
120
- `config/initializers/event_engine.rb`.
121
- 3. Define events as classes in `app/event_definitions/`.
122
- 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
123
- 5. Set `config.logger` in the initializer if you want something other than the default.
124
-
125
- Durable delivery, an event store, and prebuilt subscriber classes are separate gems
126
- (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
127
- when you need them and follow their own setup.
128
-
129
- ### EventEngine conventions
130
-
131
- - Define one `EventDefinition` class per event in `app/event_definitions/`; never
132
- hand-build event hashes.
133
- - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
134
- raw payload hashes to the emit helper.
135
- - Always set `process_type` explicitly so routing intent is clear.
136
- - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
137
- declared inputs.
138
- - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
139
- definition change; keep `event_engine:schema_check` green in CI.
140
- - Keep handlers and subscribers idempotent.
@@ -1,140 +0,0 @@
1
- ---
2
- name: event_engine-install
3
- description: Use to add EventEngine to a Rails app and set it up correctly.
4
- tools: Bash, Read, Edit
5
- ---
6
-
7
- You install EventEngine following the reference's install section exactly: add the
8
- gem, bundle, run `bin/rails g event_engine:install`, set the logger in the
9
- initializer, then dump and commit db/event_schema.rb. You do not invent steps, and
10
- you do not set up the separate delivery/store/subscribers gems unless asked.
11
-
12
- ## EventEngine
13
-
14
- > **DO NOT** explore the event_engine gem source code. This reference is the
15
- > complete user-facing API, embedded verbatim into every event_engine local so
16
- > their guidance never drifts. Keep it the single source of truth.
17
-
18
- EventEngine is a Rails engine for defining domain events as declarative classes,
19
- compiling them to a committed schema, emitting them through generated helpers, and
20
- dispatching them to registered handlers. Core builds and routes events; it ships no
21
- handlers of its own. Durable delivery, an event store, and ready-made subscriber
22
- classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
23
- `event_engine-subscribers`) — this reference covers core only.
24
-
25
- ### What it offers
26
-
27
- **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
28
-
29
- ```ruby
30
- class CowFed < EventEngine::EventDefinition
31
- event_name :cow_fed # the event's identity (required)
32
- event_type :domain # classification, e.g. :domain (required)
33
- process_type :durable # routing type (optional; set it explicitly)
34
-
35
- input :cow # a required input
36
- optional_input :farmer # an optional input
37
-
38
- required_payload :weight, from: :cow, attr: :weight
39
- optional_payload :farmer_name, from: :farmer, attr: :name
40
- end
41
- ```
42
-
43
- | DSL method | Purpose |
44
- |---|---|
45
- | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
46
- | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
47
- | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
48
- | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
49
- | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
50
- | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
51
-
52
- Duplicate input names raise `ArgumentError`; payload `from:` must reference a
53
- declared input.
54
-
55
- **process_type** — core stamps this symbol onto every emitted event but does not act
56
- on it. Which handlers receive an event is decided by each handler's `levels:`. The
57
- values:
58
-
59
- | value | intent |
60
- |---|---|
61
- | `:inline` | handled in-process, synchronously |
62
- | `:background` | handled in-process, via a background job |
63
- | `:durable` | handled when a durable outbox drains |
64
- | `:broker` | published to an external transport |
65
- | `:telemetry` | metrics / observability handlers |
66
- | `:sourced` | an append-only event store |
67
-
68
- The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
69
- etc. their behavior; core just routes to whatever is registered. If `process_type`
70
- is omitted it is `nil` — set it explicitly so routing intent is clear.
71
-
72
- **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
73
-
74
- ```ruby
75
- EventEngine.cow_fed(
76
- cow: cow, farmer: farmer, # declared inputs, by name
77
- occurred_at: Time.current, # optional, defaults to now
78
- metadata: { request_id: "abc" }, # optional
79
- idempotency_key: "…", # optional, defaults to a UUID
80
- aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
81
- event_version: 1 # optional, defaults to the latest schema version
82
- )
83
- ```
84
-
85
- Missing a required input, or passing an unknown one, raises `ArgumentError`. The
86
- event's `payload` is symbol-keyed.
87
-
88
- **Register handlers** — a handler is any object responding to `call(event)`:
89
-
90
- ```ruby
91
- EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
92
- EventEngine.dispatch(event) # fan an event out (emit helpers call this)
93
- EventEngine.reset_handlers! # clear all handlers
94
- ```
95
-
96
- Handlers run synchronously in registration order; if one raises, the rest don't run.
97
- Keep handlers idempotent.
98
-
99
- **Configure** — `config/initializers/event_engine.rb`, logger only:
100
-
101
- ```ruby
102
- EventEngine.configure { |config| config.logger = Rails.logger }
103
- ```
104
-
105
- **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
106
- is authoritative at boot:
107
-
108
- ```bash
109
- bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
110
- bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
111
- ```
112
-
113
- A new event is version 1; changing an event's identity or payload bumps its version.
114
- Changing only `process_type` does not bump the version.
115
-
116
- ### Install
117
-
118
- 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
119
- 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
120
- `config/initializers/event_engine.rb`.
121
- 3. Define events as classes in `app/event_definitions/`.
122
- 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
123
- 5. Set `config.logger` in the initializer if you want something other than the default.
124
-
125
- Durable delivery, an event store, and prebuilt subscriber classes are separate gems
126
- (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
127
- when you need them and follow their own setup.
128
-
129
- ### EventEngine conventions
130
-
131
- - Define one `EventDefinition` class per event in `app/event_definitions/`; never
132
- hand-build event hashes.
133
- - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
134
- raw payload hashes to the emit helper.
135
- - Always set `process_type` explicitly so routing intent is clear.
136
- - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
137
- declared inputs.
138
- - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
139
- definition change; keep `event_engine:schema_check` green in CI.
140
- - Keep handlers and subscribers idempotent.