event_engine-event_definition 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d4fd0fd21eecd6b764d2c91c230bff8733c055148918fdcc3dc027b80cecddb3
4
+ data.tar.gz: 39a989288b99eb7fbe86777f787c357bfedee66e36f1800ce9432915ec92ea93
5
+ SHA512:
6
+ metadata.gz: 0b245cffa43807384cc64d41ae50cca49a967636dd08156766ba5af768dc2c456191bc9eadf886cac8db40c1d57ede617275124fa4da4f906998b01d988b2755
7
+ data.tar.gz: e1c634fe59d2e0afe642c65057939c3123513586ad1402e273bcb1352e38e630148d51a148f9404b537240c5fd55443548d7bd485c755c57d2964857387e420e
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-07-20
9
+
10
+ First published release of `event_engine-event_definition`, the plain-Ruby
11
+ event-definition contract for the EventEngine pipeline.
12
+
13
+ ### Added
14
+ - EventDefinition DSL and shared schema-contract value objects (`Schema`,
15
+ `EventSchema`, `SchemaRegistry`, `SubjectRegistry`, `LifecycleDefinition`,
16
+ `DslCompiler`), with no Rails dependency.
17
+ - Configurable publisher port (`EventEngine::Definition.publisher`), defaulting
18
+ to a `NullPublisher` that fails loudly until a real adapter is assigned.
19
+ - Generated namespaced singleton helper methods that publish domain and input
20
+ payloads through the publisher port.
21
+ - `DomainPackBuild`, which writes a flat helper file alongside its `schema.json`
22
+ for a domain pack and exposes a `schema_path` accessor in the generated helper.
23
+ - `DefinitionLoader.load!` to require a pack's definition files, including
24
+ lifecycle-generated events.
25
+ - `EventEngine::Definition.configure` for pack-generation settings.
26
+ - `event_engine:definition:dump` rake task to generate a pack without Rails.
27
+
28
+ [0.2.0]: https://github.com/DYB-Development/event_engine-event_definition/releases/tag/v0.2.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 tylercschneider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # EventEngine::Definition
2
+
3
+ The plain-Ruby event-definition contract for the [EventEngine](https://github.com/DYB-Development/event_engine) pipeline.
4
+
5
+ EventEngine is a schema-first event pipeline: domain events are **declared** with a Ruby DSL, **compiled** into a canonical schema, and **emitted** through generated helper methods that hand each event to a publisher. This gem holds the plain-Ruby foundation of that pipeline — the `EventEngine::EventDefinition` DSL, the shared schema value objects, and the build step that turns a set of definitions into a committed helper file — with **no Rails dependency**.
6
+
7
+ Lightweight domain-pack gems depend on this contract; the full dispatch, registry, and Rails-engine machinery lives in [`event_engine`](https://github.com/DYB-Development/event_engine) and is wired in at runtime.
8
+
9
+ ## Installation
10
+
11
+ Requires Ruby `>= 3.2.0`.
12
+
13
+ Add the gem to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "event_engine-event_definition"
17
+ ```
18
+
19
+ Then run `bundle install`, and require it:
20
+
21
+ ```ruby
22
+ require "event_engine/definition"
23
+ ```
24
+
25
+ ## A worked example
26
+
27
+ ### The data you want to capture
28
+
29
+ Say a lead just signed up in your Rails app. You have the record in hand:
30
+
31
+ ```ruby
32
+ lead = Lead.create!(
33
+ email: "ada@example.com",
34
+ name: "Ada Lovelace",
35
+ company: "Analytical Engines",
36
+ source: "webinar"
37
+ )
38
+ # => #<Lead id: 42, email: "ada@example.com", name: "Ada Lovelace",
39
+ # company: "Analytical Engines", source: "webinar", created_at: …>
40
+ ```
41
+
42
+ You want to announce that a lead was created so the rest of the system — analytics, CRM sync, a welcome email — can react. Done ad hoc, every place that raises this "event" builds its own hash: one sends `{ email: … }`, another `{ email_address: …, lead: 42 }`, a third forgets `source`. The keys drift, fields go missing, and every consumer has to defend against all of it.
43
+
44
+ Defining the event fixes the shape **once**, so every producer emits the same data in the same format and every consumer can rely on it — with the names checked and the shape fingerprinted so it can't change silently.
45
+
46
+ ### Declare the event
47
+
48
+ ```ruby
49
+ class LeadCreated < EventEngine::EventDefinition
50
+ event_name :lead_created # the event's identity
51
+ event_type :domain # how you classify it
52
+ domain :marketing # the bounded context it belongs to
53
+
54
+ input :lead # the object the event is built from
55
+
56
+ required_payload :lead_id, from: :lead, attr: :id
57
+ required_payload :email, from: :lead, attr: :email
58
+ required_payload :name, from: :lead, attr: :name
59
+ optional_payload :company, from: :lead, attr: :company
60
+ optional_payload :source, from: :lead, attr: :source
61
+ end
62
+ ```
63
+
64
+ Read each payload line as: *"the event carries `lead_id`, and its value comes from `lead.id`."* The `input :lead` names the object you hand in; each `from:` points back at that input, and each `attr:` is the attribute read off it.
65
+
66
+ ### Generate the pack (a build step)
67
+
68
+ The helper doesn't exist until you generate it. `event_engine-event_definition` ships a rake task for that. Configure it once, then run it whenever a definition changes:
69
+
70
+ ```ruby
71
+ # Rakefile (in your domain pack)
72
+ require "event_engine/definition"
73
+ load "tasks/event_engine_definition.rake"
74
+
75
+ EventEngine::Definition.configure do |config|
76
+ config.definitions_path = "app/event_definitions" # where your EventDefinitions live
77
+ config.helper_path = "lib/generated/marketing_events.rb" # where to write the helper
78
+ config.root_module = "MarketingEvents" # the module that wraps the helpers
79
+ end
80
+ ```
81
+
82
+ ```sh
83
+ $ rake event_engine:definition:dump
84
+ ```
85
+
86
+ The task loads every definition under `definitions_path` — **no Rails required** — and writes two files you commit:
87
+
88
+ - a `MarketingEvents` module with one typed method per event, and
89
+ - a `schema.json` alongside it — the committed contract downstream consumers read.
90
+
91
+ This is a build-time step; your app never runs it while serving requests. If your events declare a `subject`, register them too: `config.subject_registry = EventEngine::SubjectRegistry.define { subject :lead }`.
92
+
93
+ ### Emit the event (at runtime)
94
+
95
+ With the helper generated, producing the event anywhere in your app is one call. You hand it the whole `lead`; the contract decides what is captured off it:
96
+
97
+ ```ruby
98
+ MarketingEvents.lead_created(lead: lead)
99
+ ```
100
+
101
+ Every `lead_created` event, from anywhere in the app, carries exactly the shape you declared:
102
+
103
+ ```ruby
104
+ {
105
+ lead_id: 42,
106
+ email: "ada@example.com",
107
+ name: "Ada Lovelace",
108
+ company: "Analytical Engines",
109
+ source: "webinar"
110
+ }
111
+ ```
112
+
113
+ > **Where the work happens:** this gem *records* the `from:`/`attr:` mapping and forwards the raw `lead` under `inputs:`. Reading `lead.id`, `lead.email`, … to build that payload is done by the publisher the `event_engine` runtime supplies — see [How it fits](#how-it-fits-with-event_engine). Until one is configured the default publisher raises, so wire it once at boot:
114
+ >
115
+ > ```ruby
116
+ > EventEngine::Definition.publisher = my_publisher # any object with #publish(event_name, **envelope)
117
+ > ```
118
+
119
+ ### More examples
120
+
121
+ See **[docs/examples.md](docs/examples.md)** for an event built from **multiple inputs** (an order and its customer) and a **lifecycle** that generates one event per step (started / completed / failed).
122
+
123
+ ## The DSL reference
124
+
125
+ ### Declarations, field by field
126
+
127
+ | Declaration | Required to be valid? | What it does |
128
+ |---|---|---|
129
+ | `event_name :x` | **Yes** — `schema` raises without it | The event's unique, snake_case identity. |
130
+ | `event_type :x` | **Yes** — `schema` raises without it | A classification symbol you choose (e.g. `:domain`, `:product`). Not enumerated by this gem. |
131
+ | `domain :x` | No | The bounded context the event belongs to. Keys the event in the registry and scopes the generated helpers. |
132
+ | `subject :x` | No | The aggregate the event is about. If set, it must be registered in a `SubjectRegistry` when the definitions are compiled. |
133
+ | `input :x` | — | Declares an **input** the event is built from. |
134
+ | `optional_input :x` | — | Same, but the input may be omitted at the call site. |
135
+ | `required_payload :name, from:, attr:` | — | Declares a **payload field** in the emitted event. |
136
+ | `optional_payload :name, from:, attr:` | — | Same, but the field is not a guaranteed part of the payload. |
137
+
138
+ ### Inputs vs. payload
139
+
140
+ These are two different layers:
141
+
142
+ - **Inputs are the arguments you hand the event** — the whole objects your code already has. Each becomes a keyword argument on the generated helper.
143
+ - `input :lead` → `lead:` is **required** at the call site.
144
+ - `optional_input :campaign` → `campaign:` **defaults to `nil`** and may be omitted.
145
+
146
+ - **Payload fields are the flat data the event carries**, described as a mapping *off* those inputs (`from:` = which input, `attr:` = which attribute on it).
147
+ - `required_payload` → the field is a **guaranteed** part of the payload.
148
+ - `optional_payload` → the field may be absent.
149
+ - The `required` flag is stored in the schema and is part of the event's **fingerprint**, so changing it changes the contract.
150
+
151
+ ### Reserved names
152
+
153
+ Compilation rejects definitions that use names owned by the event envelope:
154
+
155
+ - **Payload field names** may not be any of:
156
+ `event_name`, `event_type`, `event_version`, `occurred_at`, `created_at`, `updated_at`, `published_at`, `metadata`, `idempotency_key`, `attempts`, `dead_lettered_at`, `aggregate_type`, `aggregate_id`, `aggregate_version`.
157
+ - **Input names** may not collide with the envelope keys:
158
+ `event_version`, `occurred_at`, `metadata`, `idempotency_key`, `aggregate_type`, `aggregate_id`, `aggregate_version`.
159
+
160
+ A payload field must also have a `from:` that references a declared input, and each event name must be snake_case.
161
+
162
+ ### Inspecting the compiled schema
163
+
164
+ Every definition compiles to a `Schema` value object:
165
+
166
+ ```ruby
167
+ schema = LeadCreated.schema
168
+
169
+ schema.event_name # => :lead_created
170
+ schema.required_inputs # => [:lead]
171
+ schema.fingerprint # => "…sha256 of the event's structure…"
172
+ schema.to_h # => plain data hash (JSON-safe)
173
+ schema.to_ruby # => a Ruby source string that rebuilds the Schema
174
+ ```
175
+
176
+ The fingerprint is a stable hash of the event's **structure** (name, type, inputs, payload fields) — incidental fields like `domain` don't change it, so a matching fingerprint means a matching contract.
177
+
178
+ ## How it fits with `event_engine`
179
+
180
+ This gem defines and generates; `event_engine` runs. The seam between them is the **publisher port** (`EventEngine::Definition.publisher`):
181
+
182
+ ```
183
+ this gem event_engine (host runtime)
184
+ ──────────────────────────────── ──────────────────────────────
185
+ EventDefinition DSL registers as the publisher
186
+ │ compile ────────────────────►
187
+ ▼ reads the raw inputs via the
188
+ generated helper ──publish(event)──► schema's from:/attr: mapping,
189
+ + committed schema.json then dispatches, persists, brokers…
190
+ ```
191
+
192
+ A domain pack depends only on `event_engine-event_definition` to declare its events and build its helper file. In an app that also has `event_engine` installed, `event_engine` provides a real publisher adapter and assigns it to `EventEngine::Definition.publisher`, so calling a generated helper hands the event to the full runtime. Nothing in this gem knows how events are dispatched — that decision lives entirely in `event_engine`.
193
+
194
+ ## Development
195
+
196
+ After checking out the repo, run `bin/setup` to install dependencies. Then:
197
+
198
+ - `rake test` — run the test suite
199
+ - `bin/console` — an interactive prompt with the gem loaded
200
+
201
+ ## License
202
+
203
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/docs/examples.md ADDED
@@ -0,0 +1,96 @@
1
+ # EventEngine::Definition — examples
2
+
3
+ Each example follows the same three beats: the **data** you already have, the **event** you define, and the **call** you make. For the full DSL reference and how this plugs into `event_engine`, see the [README](../README.md).
4
+
5
+ The `SalesEvents` / `ProductEvents` modules below are the helpers produced by generating the pack — the `rake event_engine:definition:dump` build step in the README. These examples assume that step has already run, so they focus on defining the event and calling it.
6
+
7
+ ## An event built from multiple inputs
8
+
9
+ You have an order and the customer who placed it — and maybe a coupon:
10
+
11
+ ```ruby
12
+ order = Order.create!(total_cents: 4_200) # => #<Order id: 100, total_cents: 4200, …>
13
+ customer = order.customer # => #<Customer id: 7, email: "ada@example.com", …>
14
+ coupon = Coupon.find_by(code: "WELCOME") # optional — may be nil
15
+ ```
16
+
17
+ Define an event that captures a consistent slice of each input:
18
+
19
+ ```ruby
20
+ class OrderPlaced < EventEngine::EventDefinition
21
+ event_name :order_placed
22
+ event_type :domain
23
+ domain :sales
24
+
25
+ input :order
26
+ input :customer
27
+ optional_input :coupon
28
+
29
+ required_payload :order_id, from: :order, attr: :id
30
+ required_payload :total_cents, from: :order, attr: :total_cents
31
+ required_payload :customer_id, from: :customer, attr: :id
32
+ required_payload :customer_email, from: :customer, attr: :email
33
+ optional_payload :coupon_code, from: :coupon, attr: :code
34
+ end
35
+ ```
36
+
37
+ Emit it — pass each input; optional ones you can leave off:
38
+
39
+ ```ruby
40
+ SalesEvents.order_placed(order: order, customer: customer, coupon: coupon)
41
+ # or, with no coupon:
42
+ SalesEvents.order_placed(order: order, customer: customer)
43
+ ```
44
+
45
+ Captured:
46
+
47
+ ```ruby
48
+ {
49
+ order_id: 100,
50
+ total_cents: 4200,
51
+ customer_id: 7,
52
+ customer_email: "ada@example.com",
53
+ coupon_code: "WELCOME"
54
+ }
55
+ ```
56
+
57
+ ## A lifecycle: one definition, an event per step
58
+
59
+ You have a long-running job:
60
+
61
+ ```ruby
62
+ export = Export.create!(format: "csv") # => #<Export id: 9, format: "csv", …>
63
+ ```
64
+
65
+ `LifecycleDefinition` stamps out one event per verb from a shared base, and `on` layers extra data onto a single step:
66
+
67
+ ```ruby
68
+ class ExportCsv < EventEngine::LifecycleDefinition
69
+ subject :export_csv
70
+ event_type :product
71
+
72
+ input :export
73
+ required_payload :format, from: :export, attr: :format
74
+
75
+ lifecycle :started, :completed, :failed
76
+
77
+ on :failed do
78
+ input :error
79
+ required_payload :error_class, from: :error, attr: :class
80
+ end
81
+ end
82
+ ```
83
+
84
+ That gives you three events — `export_csv_started`, `export_csv_completed`, and `export_csv_failed` — each carrying `format`, and the failed one also carrying `error_class`.
85
+
86
+ Once the pack is generated, emit the step that happened:
87
+
88
+ ```ruby
89
+ ProductEvents.export_csv_failed(export: export, error: some_error)
90
+ ```
91
+
92
+ Captured:
93
+
94
+ ```ruby
95
+ { format: "csv", error_class: "Timeout::Error" }
96
+ ```
@@ -0,0 +1,14 @@
1
+ require "event_engine/subject_registry"
2
+
3
+ module EventEngine
4
+ module Definition
5
+ class Configuration
6
+ attr_accessor :definitions_path, :helper_path, :root_module
7
+ attr_writer :subject_registry
8
+
9
+ def subject_registry
10
+ @subject_registry ||= SubjectRegistry.new
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module EventEngine
2
+ module Definition
3
+ class PublisherNotConfigured < Error; end
4
+
5
+ class NullPublisher
6
+ def publish(_event_name, **_envelope)
7
+ raise PublisherNotConfigured,
8
+ "No EventEngine::Definition.publisher configured. Wire a publisher " \
9
+ "adapter (e.g. event_engine) before emitting events."
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventEngine
4
+ module Definition
5
+ VERSION = "0.2.0"
6
+ end
7
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "definition/version"
4
+
5
+ require "event_engine/subject_registry"
6
+ require "event_engine/event_definition"
7
+ require "event_engine/definition_loader"
8
+ require "event_engine/event_schema"
9
+ require "event_engine/schema_registry"
10
+ require "event_engine/lifecycle_definition"
11
+ require "event_engine/dsl_compiler"
12
+ require "event_engine/definition/configuration"
13
+
14
+ module EventEngine
15
+ module Definition
16
+ class Error < StandardError; end
17
+
18
+ class << self
19
+ attr_writer :publisher
20
+
21
+ def publisher
22
+ @publisher ||= NullPublisher.new
23
+ end
24
+
25
+ def reset_publisher!
26
+ @publisher = nil
27
+ end
28
+
29
+ def configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ def configure
34
+ yield(configuration)
35
+ end
36
+
37
+ def reset_configuration!
38
+ @configuration = nil
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ require "event_engine/definition/null_publisher"
45
+ require "event_engine/event_engine_helpers_writer"
46
+ require "event_engine/domain_pack_build"
@@ -0,0 +1,23 @@
1
+ require "event_engine/event_definition"
2
+ require "event_engine/lifecycle_definition"
3
+
4
+ module EventEngine
5
+ module DefinitionLoader
6
+ def self.load!(path)
7
+ before_events = EventEngine::EventDefinition.subclasses
8
+ before_lifecycles = EventEngine::LifecycleDefinition.subclasses
9
+ require_ruby_files(path)
10
+
11
+ declared_events = EventEngine::EventDefinition.subclasses - before_events
12
+ new_lifecycles = EventEngine::LifecycleDefinition.subclasses - before_lifecycles
13
+
14
+ declared_events + new_lifecycles.flat_map(&:generated_events)
15
+ end
16
+
17
+ def self.require_ruby_files(path)
18
+ Dir.glob(File.join(path, "**", "*.rb")).sort.each do |file|
19
+ require File.expand_path(file)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "event_engine/dsl_compiler"
5
+ require "event_engine/subject_registry"
6
+ require "event_engine/event_engine_helpers_writer"
7
+
8
+ module EventEngine
9
+ class DomainPackBuild
10
+ PORT_EMIT = "EventEngine::Definition.publisher.publish"
11
+
12
+ SCHEMA_FILENAME = "schema.json"
13
+
14
+ HEADER = <<~RUBY.freeze
15
+ # This file is authoritative in production.
16
+ # It is generated from this pack's EventDefinitions.
17
+ # Do not edit manually.
18
+
19
+ RUBY
20
+
21
+ def self.run(definitions, helper_path:, root_module:, header: HEADER,
22
+ subject_registry: SubjectRegistry.new)
23
+ new(
24
+ definitions,
25
+ helper_path: helper_path,
26
+ root_module: root_module,
27
+ header: header,
28
+ subject_registry: subject_registry
29
+ ).run
30
+ end
31
+
32
+ def initialize(definitions, helper_path:, root_module:, header:, subject_registry:)
33
+ @definitions = definitions
34
+ @helper_path = helper_path
35
+ @root_module = root_module
36
+ @header = header
37
+ @subject_registry = subject_registry
38
+ end
39
+
40
+ def run
41
+ event_schema = compile
42
+ write_helper(event_schema)
43
+ write_schema_json(event_schema)
44
+ self
45
+ end
46
+
47
+ def schema_path
48
+ File.join(File.dirname(@helper_path), SCHEMA_FILENAME)
49
+ end
50
+
51
+ private
52
+
53
+ def compile
54
+ DslCompiler.compile(@definitions, subject_registry: @subject_registry).event_schema
55
+ end
56
+
57
+ def write_schema_json(event_schema)
58
+ File.write(schema_path, JSON.pretty_generate(schemas(event_schema)))
59
+ end
60
+
61
+ def schemas(event_schema)
62
+ event_schema.schemas_by_event.values.flat_map(&:values).map(&:to_h)
63
+ end
64
+
65
+ def write_helper(event_schema)
66
+ EventEngineHelpersWriter.write(
67
+ @helper_path,
68
+ event_schema,
69
+ root_module: @root_module,
70
+ emit: PORT_EMIT,
71
+ header: @header,
72
+ group_by_domain: false,
73
+ schema_filename: SCHEMA_FILENAME
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,79 @@
1
+ module EventEngine
2
+ class DslCompiler
3
+ class InvalidEventNameError < StandardError; end
4
+ class ReservedInputNameError < StandardError; end
5
+
6
+ SNAKE_CASE = /\A[a-z][a-z0-9_]*\z/
7
+
8
+ RESERVED_INPUT_NAMES = %i[
9
+ event_version
10
+ occurred_at
11
+ metadata
12
+ idempotency_key
13
+ aggregate_type
14
+ aggregate_id
15
+ aggregate_version
16
+ ].freeze
17
+
18
+ def self.compile(definitions, subject_registry: SubjectRegistry.new)
19
+ registry = SchemaRegistry.new
20
+ subject_violations = []
21
+ name_violations = []
22
+ reserved_violations = []
23
+
24
+ Array(definitions).each do |definition|
25
+ schema = definition.schema
26
+ record_subject_violation(schema, subject_violations, subject_registry)
27
+ record_name_violation(schema, name_violations)
28
+ record_reserved_input_violation(schema, reserved_violations)
29
+ registry.register(schema)
30
+ end
31
+
32
+ raise_invalid_event_names(name_violations)
33
+ raise_reserved_input_names(reserved_violations)
34
+ raise_unknown_subjects(subject_violations)
35
+
36
+ registry
37
+ end
38
+
39
+ def self.record_name_violation(schema, violations)
40
+ return if schema.event_name.to_s.match?(SNAKE_CASE)
41
+
42
+ violations << schema.event_name.inspect
43
+ end
44
+
45
+ def self.raise_invalid_event_names(violations)
46
+ return if violations.empty?
47
+
48
+ raise InvalidEventNameError, "event names must be snake_case: #{violations.join(", ")}"
49
+ end
50
+
51
+ def self.record_reserved_input_violation(schema, violations)
52
+ inputs = schema.required_inputs + schema.optional_inputs
53
+ collisions = inputs & RESERVED_INPUT_NAMES
54
+ return if collisions.empty?
55
+
56
+ violations << "#{schema.event_name}: #{collisions.join(", ")}"
57
+ end
58
+
59
+ def self.raise_reserved_input_names(violations)
60
+ return if violations.empty?
61
+
62
+ raise ReservedInputNameError,
63
+ "input names collide with reserved envelope keys: #{violations.join("; ")}"
64
+ end
65
+
66
+ def self.record_subject_violation(schema, violations, subject_registry)
67
+ return if schema.subject.nil?
68
+ return if subject_registry.registered?(schema.subject)
69
+
70
+ violations << "#{schema.event_name}: unknown subject #{schema.subject.inspect}"
71
+ end
72
+
73
+ def self.raise_unknown_subjects(violations)
74
+ return if violations.empty?
75
+
76
+ raise SubjectRegistry::UnknownSubjectError, violations.join(", ")
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ module EventEngine
2
+ class EventDefinition
3
+ module Inputs
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def input(name)
10
+ name = name.to_sym
11
+ if inputs.key?(name)
12
+ raise ArgumentError, "duplicate input: #{name}"
13
+ end
14
+ inputs[name] = :required
15
+ end
16
+
17
+ def optional_input(name)
18
+ name = name.to_sym
19
+ if inputs.key?(name)
20
+ raise ArgumentError, "duplicate input: #{name}"
21
+ end
22
+ inputs[name] = :optional
23
+ end
24
+
25
+ def inputs
26
+ @inputs ||= {}
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end