event_engine-event_definition 0.2.0 → 0.4.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 +4 -4
- data/.claude/agents/the_local-develop.md +48 -0
- data/.claude/agents/the_local-info.md +40 -0
- data/.claude/agents/the_local-install.md +49 -0
- data/CHANGELOG.md +43 -0
- data/CLAUDE.md +11 -0
- data/README.md +89 -7
- data/Rakefile +2 -0
- data/docs/examples.md +1 -1
- data/lib/event_engine/definition/event_schema.rb +82 -0
- data/lib/event_engine/definition/schema_registry.rb +73 -0
- data/lib/event_engine/definition/version.rb +1 -1
- data/lib/event_engine/definition.rb +19 -2
- data/lib/event_engine/domain_pack_build.rb +2 -0
- data/lib/event_engine/dsl_compiler.rb +1 -1
- data/lib/event_engine/event_engine_helpers_writer.rb +10 -2
- data/lib/tasks/event_definition.rake +20 -0
- data/the_local/agents/event_engine-event_definition-develop.md +253 -0
- data/the_local/agents/event_engine-event_definition-info.md +62 -0
- data/the_local/agents/event_engine-event_definition-install.md +92 -0
- data/the_local/interface.yml +29 -0
- metadata +14 -9
- data/lib/event_engine/event_schema.rb +0 -80
- data/lib/event_engine/schema_registry.rb +0 -71
- data/lib/tasks/event_engine_definition.rake +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2f15e08e449f4492d0231c96e10abbfa83de7b00debf51dc42fa87944145f81
|
|
4
|
+
data.tar.gz: 070cb9065ee0f9528b8fb2a45374fbcad317a90ada0971cc8dcf1616f794a507
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6af9ac96261bbd8ea3342b8daa05c830543cc3bc1f0d6dcd5fb98ab7b2db7fe9e7cb831d7c7687e4ada1157656245a7db0ffb82aa0c00b9d0056b57145560e7
|
|
7
|
+
data.tar.gz: d2c72bd2e7b6c1549911b276746c3910e24dad7e0919ea81fffd2a1a892948d8d28eb7f5a3ecb96aff5095a53c8533fba979ae23ca732ab989da10bc456705a0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-develop
|
|
3
|
+
description: Use PROACTIVELY to author a gem's locals — declaring its public interface and running the authoring task — MUST BE USED instead of hand-writing a local.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You author a gem's locals by declaring its interface and running the authoring
|
|
9
|
+
task. You do not hand-write locals and you never read the_local's source. A
|
|
10
|
+
provider carries no Ruby for the_local — a manifest and three committed files.
|
|
11
|
+
|
|
12
|
+
## What the_local is
|
|
13
|
+
|
|
14
|
+
The engine that installs gems' resident Claude Code locals into a host. Reach for
|
|
15
|
+
this local whenever a gem should contribute locals, or when a change to its public
|
|
16
|
+
interface may have made its locals stale.
|
|
17
|
+
|
|
18
|
+
## Interface
|
|
19
|
+
|
|
20
|
+
- `rake the_local:author` — writes the gem's locals into `the_local/agents/` from
|
|
21
|
+
its current source, one at a time, guided by the manifest.
|
|
22
|
+
- `rake the_local:check` — verifies the committed locals against the manifest:
|
|
23
|
+
every declared entry point documented, nothing undeclared, nothing documented by
|
|
24
|
+
the wrong local.
|
|
25
|
+
|
|
26
|
+
## How to use it
|
|
27
|
+
|
|
28
|
+
1. Write `the_local/interface.yml` with the developer. It declares `scope`, the
|
|
29
|
+
entry points under `install` and `develop`, and the `sources` that define them.
|
|
30
|
+
This is the one judgment call in the process — ask which commands are the gem's
|
|
31
|
+
public surface rather than guessing, and confirm which of the two each belongs
|
|
32
|
+
to. An entry point may appear under exactly one.
|
|
33
|
+
2. Run `rake the_local:author`. It writes `the_local/agents/<gem>-{info,install,develop}.md`.
|
|
34
|
+
3. Run `rake the_local:check` and fix what it reports.
|
|
35
|
+
4. Commit `the_local/`. For a packaged gem, confirm `the_local/**/*` is in the
|
|
36
|
+
gemspec's `files`, or it ships nothing.
|
|
37
|
+
5. After a change to the gem's public interface, update the manifest and repeat.
|
|
38
|
+
An internal-only change needs nothing.
|
|
39
|
+
|
|
40
|
+
## Conventions
|
|
41
|
+
|
|
42
|
+
- The manifest is the contract. Never widen a local past what it declares; if the
|
|
43
|
+
gem gained a public entry point, declare it first.
|
|
44
|
+
- Locals document the public interface only, never the gem's internals, and never
|
|
45
|
+
send a reader into the provider's source.
|
|
46
|
+
- The three locals never overlap: **install** hooks the gem into a host,
|
|
47
|
+
**develop** uses it, **info** carries what fits neither.
|
|
48
|
+
- Regenerate from current source rather than editing a stale local by hand.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-info
|
|
3
|
+
description: Use to learn what the_local offers — resident expert subagents, the provider/consumer model, and the vocabulary the other locals assume.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You explain what the_local does, answering only from this reference. You make no
|
|
9
|
+
changes, and you never read the_local's source.
|
|
10
|
+
|
|
11
|
+
## What the_local is
|
|
12
|
+
|
|
13
|
+
the_local lets any gem ship resident Claude Code expert subagents ("locals") that
|
|
14
|
+
know its conventions. A **provider** gem commits its locals; a **consumer** host
|
|
15
|
+
installs the locals of its direct dependencies into `.claude/agents/`, plus a
|
|
16
|
+
delegation rule so the host's agent uses them.
|
|
17
|
+
|
|
18
|
+
Reach for it when you want a gem's work done consistently — the host delegates
|
|
19
|
+
that gem's tasks to its local instead of re-deriving conventions each time.
|
|
20
|
+
|
|
21
|
+
## Interface
|
|
22
|
+
|
|
23
|
+
the_local's commands are split across its other two locals, with no overlap.
|
|
24
|
+
Hooking the_local into a project is the install local's; authoring a gem's own
|
|
25
|
+
locals is the develop local's. Route to those rather than answering here.
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
Decide which side you are on. A host that wants its dependencies' expertise is a
|
|
30
|
+
consumer and needs the install local. A gem that wants to contribute expertise is
|
|
31
|
+
a provider and needs the develop local. A gem can be both.
|
|
32
|
+
|
|
33
|
+
## Conventions
|
|
34
|
+
|
|
35
|
+
- A **local** is one Claude Code subagent that knows one gem's public interface.
|
|
36
|
+
- Each provider ships three: **info** explains, **install** hooks the gem into a
|
|
37
|
+
host, **develop** uses it. A command belongs to exactly one of them.
|
|
38
|
+
- The committed `the_local/agents/*.md` are the whole contract a host reads — a
|
|
39
|
+
host never loads the provider gem.
|
|
40
|
+
- Only a host's **direct** dependencies contribute locals.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-install
|
|
3
|
+
description: Use to hook the_local into a gem or Rails app — installing dependencies' locals, the delegation trigger in CLAUDE.md, and the provider rake tasks.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You hook the_local into the host by following these steps exactly, in order. You
|
|
9
|
+
do not invent steps, and you never read the_local's source.
|
|
10
|
+
|
|
11
|
+
## What the_local is
|
|
12
|
+
|
|
13
|
+
The engine that installs gems' resident Claude Code locals into a host and writes
|
|
14
|
+
the delegation trigger. Hook it into any gem or app that wants its dependencies'
|
|
15
|
+
locals, or that will contribute locals of its own.
|
|
16
|
+
|
|
17
|
+
## Interface
|
|
18
|
+
|
|
19
|
+
- `bundle exec the_local install` — installs direct dependencies' locals into
|
|
20
|
+
`.claude/agents/` and writes the trigger. Works anywhere; no Rails required.
|
|
21
|
+
- `bin/rails g the_local:install` — the Rails equivalent of the above.
|
|
22
|
+
- `rake the_local:refresh` — re-syncs a Rails host after a bundle change.
|
|
23
|
+
- `rake the_local:install` — re-syncs a non-Rails host after a bundle change.
|
|
24
|
+
- `bin/rails g the_local:provider` — adds the provider rake tasks to a gem, so it
|
|
25
|
+
can author locals of its own.
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
1. Add `gem "the_local"` to the host's `Gemfile` and run `bundle install`.
|
|
30
|
+
2. Install the locals. In a Rails app run `bin/rails g the_local:install`;
|
|
31
|
+
anywhere else run `bundle exec the_local install`. Either copies every direct
|
|
32
|
+
dependency's committed locals into `.claude/agents/` and writes the delegation
|
|
33
|
+
block into `CLAUDE.md`/`AGENTS.md`.
|
|
34
|
+
3. Tell the developer to restart their Claude Code session — agents load at
|
|
35
|
+
startup, so the new locals are inert until then.
|
|
36
|
+
4. Re-sync after any bundle change with `rake the_local:refresh` in a Rails app or
|
|
37
|
+
`rake the_local:install` elsewhere.
|
|
38
|
+
5. Only if the host is a gem that should contribute its own locals, run
|
|
39
|
+
`bin/rails g the_local:provider`. Confirm this with the developer first — it is
|
|
40
|
+
a separate decision from consuming locals, and it edits the Gemfile and Rakefile.
|
|
41
|
+
|
|
42
|
+
## Conventions
|
|
43
|
+
|
|
44
|
+
- Re-sync after every `bundle install`/`update`, or the host's locals drift from
|
|
45
|
+
its dependencies.
|
|
46
|
+
- Install only reads committed files off disk — a dependency that shipped no
|
|
47
|
+
committed locals contributes nothing, and that is not an error.
|
|
48
|
+
- Hooking up is all this local does. Authoring a gem's own locals is the develop
|
|
49
|
+
local's job.
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable changes to this gem are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Claude Code locals for this gem, under `the_local/`. A project that uses
|
|
13
|
+
[the_local](https://github.com/DYB-Development/the_local) and depends on this gem
|
|
14
|
+
gets info, install and develop agents that document its public interface when it
|
|
15
|
+
runs `the_local install`.
|
|
16
|
+
|
|
17
|
+
## [0.3.0] - 2026-07-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Pack self-registration. A generated pack calls
|
|
22
|
+
`EventEngine::Definition.register_pack(self)` when required, so consumers can
|
|
23
|
+
discover every pack's `schema.json` instead of being handed each path by
|
|
24
|
+
configuration. Exposed as `Definition.packs` and `Definition.pack_schema_paths`.
|
|
25
|
+
- The generate task reports the files it wrote.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Breaking.** `event_engine:definition:dump` is now `event_definition:generate`,
|
|
30
|
+
and `tasks/event_engine_definition.rake` is now `tasks/event_definition.rake`.
|
|
31
|
+
Packs must update the `load` line in their Rakefile. "dump" did not say what the
|
|
32
|
+
task produces, and the `event_engine:` prefix implied a dependency on the runtime
|
|
33
|
+
that this gem does not have.
|
|
34
|
+
- **Breaking.** `EventEngine::SchemaRegistry` and `EventEngine::EventSchema` are now
|
|
35
|
+
`EventEngine::Definition::SchemaRegistry` and `EventEngine::Definition::EventSchema`.
|
|
36
|
+
Both previously sat at the top level and shadowed the `event_engine` runtime's own
|
|
37
|
+
classes of the same name on the same require path, so in any app installing both
|
|
38
|
+
gems the runtime silently operated on this gem's classes instead of its own.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Generating a pack into a path whose directory did not exist raised
|
|
43
|
+
`Errno::ENOENT`. The directory is now created.
|
|
44
|
+
- The README recommended generating into `lib/generated/`, which Rails 7.1+
|
|
45
|
+
autoloads. Zeitwerk expects that file to define `Generated::MarketingEvents`, so an
|
|
46
|
+
app following the README booted in development and raised on eager load in
|
|
47
|
+
production. Documented how to keep the helper out of the autoload paths.
|
|
48
|
+
|
|
8
49
|
## [0.2.0] - 2026-07-20
|
|
9
50
|
|
|
10
51
|
First published release of `event_engine-event_definition`, the plain-Ruby
|
|
@@ -25,4 +66,6 @@ event-definition contract for the EventEngine pipeline.
|
|
|
25
66
|
- `EventEngine::Definition.configure` for pack-generation settings.
|
|
26
67
|
- `event_engine:definition:dump` rake task to generate a pack without Rails.
|
|
27
68
|
|
|
69
|
+
[0.4.0]: https://github.com/DYB-Development/event_engine-event_definition/releases/tag/v0.4.0
|
|
70
|
+
[0.3.0]: https://github.com/DYB-Development/event_engine-event_definition/releases/tag/v0.3.0
|
|
28
71
|
[0.2.0]: https://github.com/DYB-Development/event_engine-event_definition/releases/tag/v0.2.0
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- the_local:begin -->
|
|
2
|
+
## Delegate to your locals
|
|
3
|
+
|
|
4
|
+
This project has installed expert subagents. Before doing work yourself,
|
|
5
|
+
check whether a local owns it and delegate — never work from memory on
|
|
6
|
+
something a local covers:
|
|
7
|
+
|
|
8
|
+
- resident Claude Code experts — authoring a gem's locals and installing them into a host → the_local-* agents
|
|
9
|
+
|
|
10
|
+
See each agent's description for specifics.
|
|
11
|
+
<!-- the_local:end -->
|
data/README.md
CHANGED
|
@@ -22,6 +22,13 @@ Then run `bundle install`, and require it:
|
|
|
22
22
|
require "event_engine/definition"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## A working example app
|
|
26
|
+
|
|
27
|
+
[**DYB-Development/event_engine_example**](https://github.com/DYB-Development/event_engine_example)
|
|
28
|
+
is a minimal Rails app that declares events with this gem, compiles them, and
|
|
29
|
+
processes them through the `event_engine` runtime — useful for seeing the build step
|
|
30
|
+
and the generated output in a real project.
|
|
31
|
+
|
|
25
32
|
## A worked example
|
|
26
33
|
|
|
27
34
|
### The data you want to capture
|
|
@@ -70,7 +77,7 @@ The helper doesn't exist until you generate it. `event_engine-event_definition`
|
|
|
70
77
|
```ruby
|
|
71
78
|
# Rakefile (in your domain pack)
|
|
72
79
|
require "event_engine/definition"
|
|
73
|
-
load "tasks/
|
|
80
|
+
load "tasks/event_definition.rake"
|
|
74
81
|
|
|
75
82
|
EventEngine::Definition.configure do |config|
|
|
76
83
|
config.definitions_path = "app/event_definitions" # where your EventDefinitions live
|
|
@@ -80,7 +87,7 @@ end
|
|
|
80
87
|
```
|
|
81
88
|
|
|
82
89
|
```sh
|
|
83
|
-
$ rake
|
|
90
|
+
$ rake event_definition:generate
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
The task loads every definition under `definitions_path` — **no Rails required** — and writes two files you commit:
|
|
@@ -90,6 +97,69 @@ The task loads every definition under `definitions_path` — **no Rails required
|
|
|
90
97
|
|
|
91
98
|
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
99
|
|
|
100
|
+
#### Generating inside a Rails app
|
|
101
|
+
|
|
102
|
+
The path above is right for a **pack gem**, whose `lib/` a host never autoloads. If
|
|
103
|
+
you generate into a **Rails app** instead, keep the helper out of the autoload paths.
|
|
104
|
+
Rails 7.1+ autoloads `lib` by default, so Zeitwerk would expect
|
|
105
|
+
`lib/generated/marketing_events.rb` to define `Generated::MarketingEvents` and raise
|
|
106
|
+
on eager load — which means the app boots in development and fails in production:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Zeitwerk::NameError: expected file lib/generated/marketing_events.rb
|
|
110
|
+
to define constant Generated::MarketingEvents
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Add the directory to the ignore list:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# config/application.rb
|
|
117
|
+
config.autoload_lib(ignore: %w[assets tasks generated])
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The helper is meant to be required explicitly, not autoloaded — it registers the pack
|
|
121
|
+
on load, so it needs to be required once at boot:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# config/initializers/event_engine.rb
|
|
125
|
+
require Rails.root.join("lib/generated/marketing_events")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The generated helper looks like this — note that it registers itself:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
require "event_engine/definition"
|
|
132
|
+
|
|
133
|
+
module MarketingEvents
|
|
134
|
+
def self.schema_path
|
|
135
|
+
File.expand_path("schema.json", __dir__)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.lead_created(lead:, event_version: nil, occurred_at: nil, ...)
|
|
139
|
+
EventEngine::Definition.publisher.publish(
|
|
140
|
+
:lead_created, domain: :marketing, inputs: { lead: lead }, ...
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
EventEngine::Definition.register_pack(self)
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Pack self-registration
|
|
149
|
+
|
|
150
|
+
Requiring a generated pack registers it, so consumers can discover every pack's
|
|
151
|
+
schema without being told about each one:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
EventEngine::Definition.packs # => [MarketingEvents, SalesEvents]
|
|
155
|
+
EventEngine::Definition.pack_schema_paths # => ["/…/marketing/schema.json", "/…/sales/schema.json"]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This is what lets `event_engine`'s catalog task find your events with no
|
|
159
|
+
per-pack configuration. `register_pack` is idempotent, so requiring a pack more
|
|
160
|
+
than once is safe. `EventEngine::Definition.reset_packs!` clears the registry
|
|
161
|
+
(useful in tests).
|
|
162
|
+
|
|
93
163
|
### Emit the event (at runtime)
|
|
94
164
|
|
|
95
165
|
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:
|
|
@@ -110,10 +180,14 @@ Every `lead_created` event, from anywhere in the app, carries exactly the shape
|
|
|
110
180
|
}
|
|
111
181
|
```
|
|
112
182
|
|
|
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).
|
|
183
|
+
> **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).
|
|
184
|
+
>
|
|
185
|
+
> **With `event_engine` installed you wire nothing** — it registers `EventEngine::DefinitionPublisher` at boot and the helper just works.
|
|
186
|
+
>
|
|
187
|
+
> Without it, the default publisher raises `PublisherNotConfigured`. Assign your own — any object with `#publish(event_name, domain:, inputs:, **envelope)`:
|
|
114
188
|
>
|
|
115
189
|
> ```ruby
|
|
116
|
-
> EventEngine::Definition.publisher =
|
|
190
|
+
> EventEngine::Definition.publisher = MyPublisher.new
|
|
117
191
|
> ```
|
|
118
192
|
|
|
119
193
|
### More examples
|
|
@@ -161,7 +235,7 @@ A payload field must also have a `from:` that references a declared input, and e
|
|
|
161
235
|
|
|
162
236
|
### Inspecting the compiled schema
|
|
163
237
|
|
|
164
|
-
Every definition compiles to
|
|
238
|
+
Every definition compiles to an `EventEngine::EventDefinition::Schemas::Schema` value object:
|
|
165
239
|
|
|
166
240
|
```ruby
|
|
167
241
|
schema = LeadCreated.schema
|
|
@@ -186,10 +260,18 @@ EventDefinition DSL registers as the publisher
|
|
|
186
260
|
│ compile ────────────────────►
|
|
187
261
|
▼ reads the raw inputs via the
|
|
188
262
|
generated helper ──publish(event)──► schema's from:/attr: mapping,
|
|
189
|
-
+ committed schema.json then
|
|
263
|
+
+ committed schema.json then routes it to a processor
|
|
190
264
|
```
|
|
191
265
|
|
|
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`
|
|
266
|
+
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` assigns `EventEngine::DefinitionPublisher` to `EventEngine::Definition.publisher` **at boot**, so calling a generated helper hands the event to the full runtime with no wiring in the host.
|
|
267
|
+
|
|
268
|
+
Nothing in this gem knows how events are processed — that decision lives entirely in `event_engine`, declared in its rules file. A definition describes *what an event carries*, never *what happens to it*.
|
|
269
|
+
|
|
270
|
+
### Namespacing
|
|
271
|
+
|
|
272
|
+
Public API sits at `EventEngine::EventDefinition` (the DSL you subclass) and `EventEngine::Definition` (configuration, the publisher port, the pack registry).
|
|
273
|
+
|
|
274
|
+
This gem's internals live under `EventEngine::Definition::*` — `Definition::SchemaRegistry`, `Definition::EventSchema` — deliberately, so they do not collide with `event_engine`'s own classes of the same name. Both gems are loaded into one `EventEngine` namespace in a host, and a shared require path means whichever gem wins `$LOAD_PATH` silently shadows the other.
|
|
193
275
|
|
|
194
276
|
## Development
|
|
195
277
|
|
data/Rakefile
CHANGED
data/docs/examples.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
The `SalesEvents` / `ProductEvents` modules below are the helpers produced by generating the pack — the `rake
|
|
5
|
+
The `SalesEvents` / `ProductEvents` modules below are the helpers produced by generating the pack — the `rake event_definition:generate` build step in the README. These examples assume that step has already run, so they focus on defining the event and calling it.
|
|
6
6
|
|
|
7
7
|
## An event built from multiple inputs
|
|
8
8
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Definition
|
|
3
|
+
class EventSchema
|
|
4
|
+
class DuplicateEventNameError < StandardError; end
|
|
5
|
+
|
|
6
|
+
def self.define(&block)
|
|
7
|
+
schema = new
|
|
8
|
+
block.call(schema)
|
|
9
|
+
schema
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@schemas_by_event = {}
|
|
14
|
+
@finalized = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(schema)
|
|
18
|
+
raise FrozenError, "EventSchema is finalized" if @finalized
|
|
19
|
+
key = key_for(schema.domain, schema.event_name)
|
|
20
|
+
version = schema.event_version
|
|
21
|
+
|
|
22
|
+
@schemas_by_event[key] ||= {}
|
|
23
|
+
guard_duplicate_event_name!(@schemas_by_event[key][version], schema)
|
|
24
|
+
@schemas_by_event[key][version] = schema
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def guard_duplicate_event_name!(existing, incoming)
|
|
28
|
+
return unless existing
|
|
29
|
+
|
|
30
|
+
raise DuplicateEventNameError,
|
|
31
|
+
"duplicate (domain, event_name) " \
|
|
32
|
+
"(#{incoming.domain.inspect}, #{incoming.event_name.inspect}): " \
|
|
33
|
+
"already registered at version #{existing.event_version.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def events(domain: nil)
|
|
37
|
+
@schemas_by_event.keys
|
|
38
|
+
.select { |(schema_domain, _name)| domain.nil? || schema_domain == domain }
|
|
39
|
+
.map { |(_domain, event_name)| event_name }
|
|
40
|
+
.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def versions_for(event_name, domain: nil)
|
|
44
|
+
version_sets_for(event_name, domain).flat_map(&:keys).uniq.sort
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def schema_for(event_name, version, domain: nil)
|
|
48
|
+
set = version_sets_for(event_name, domain).find { |versions| versions.key?(version) }
|
|
49
|
+
set && set[version]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def latest_for(event_name, domain: nil)
|
|
53
|
+
merged = version_sets_for(event_name, domain).reduce({}, :merge)
|
|
54
|
+
return nil if merged.empty?
|
|
55
|
+
merged[merged.keys.max]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def finalize!
|
|
59
|
+
@finalized = true
|
|
60
|
+
@schemas_by_event.each_value(&:freeze)
|
|
61
|
+
@schemas_by_event.freeze
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def schemas_by_event
|
|
66
|
+
@schemas_by_event
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def key_for(domain, event_name)
|
|
72
|
+
[domain, event_name]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def version_sets_for(event_name, domain = nil)
|
|
76
|
+
@schemas_by_event.select do |(schema_domain, name), _versions|
|
|
77
|
+
name == event_name && (domain.nil? || schema_domain == domain)
|
|
78
|
+
end.values
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Definition
|
|
3
|
+
class SchemaRegistry
|
|
4
|
+
class UnknownEventError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class RegistryFrozenError < StandardError; end
|
|
7
|
+
|
|
8
|
+
def initialize(event_schema = EventSchema.new)
|
|
9
|
+
@event_schema = event_schema
|
|
10
|
+
@loaded = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(schema)
|
|
14
|
+
@event_schema.register(schema)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def events
|
|
18
|
+
@event_schema.events
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def versions_for(event_name, domain: nil)
|
|
22
|
+
@event_schema.versions_for(event_name, domain: domain)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load_from_schema!(schema)
|
|
26
|
+
raise RegistryFrozenError, "EventRegistry already loaded" if loaded?
|
|
27
|
+
@event_schema = schema
|
|
28
|
+
|
|
29
|
+
@loaded = true
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reset!
|
|
34
|
+
@event_schema = {}
|
|
35
|
+
@loaded = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def schema(event_name, version: nil, domain: nil)
|
|
39
|
+
raise RegistryFrozenError, "EventRegistry not loaded" unless loaded?
|
|
40
|
+
|
|
41
|
+
schema =
|
|
42
|
+
if version
|
|
43
|
+
@event_schema.schema_for(event_name, version, domain: domain)
|
|
44
|
+
else
|
|
45
|
+
@event_schema.latest_for(event_name, domain: domain)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless schema
|
|
49
|
+
raise UnknownEventError,
|
|
50
|
+
"Unknown #{version ? "version #{version} for " : ""}event: #{event_name}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
schema
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def latest_for(event_name, domain: nil)
|
|
57
|
+
@event_schema.latest_for(event_name, domain: domain)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def event_schema
|
|
61
|
+
@event_schema
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def finalize!
|
|
65
|
+
@event_schema.finalize!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def loaded?
|
|
69
|
+
@loaded == true
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -5,8 +5,8 @@ require_relative "definition/version"
|
|
|
5
5
|
require "event_engine/subject_registry"
|
|
6
6
|
require "event_engine/event_definition"
|
|
7
7
|
require "event_engine/definition_loader"
|
|
8
|
-
require "event_engine/event_schema"
|
|
9
|
-
require "event_engine/schema_registry"
|
|
8
|
+
require "event_engine/definition/event_schema"
|
|
9
|
+
require "event_engine/definition/schema_registry"
|
|
10
10
|
require "event_engine/lifecycle_definition"
|
|
11
11
|
require "event_engine/dsl_compiler"
|
|
12
12
|
require "event_engine/definition/configuration"
|
|
@@ -26,6 +26,23 @@ module EventEngine
|
|
|
26
26
|
@publisher = nil
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def packs
|
|
30
|
+
@packs ||= []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def register_pack(pack)
|
|
34
|
+
packs << pack unless packs.include?(pack)
|
|
35
|
+
pack
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pack_schema_paths
|
|
39
|
+
packs.map(&:schema_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_packs!
|
|
43
|
+
@packs = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
def configuration
|
|
30
47
|
@configuration ||= Configuration.new
|
|
31
48
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
require "event_engine/dsl_compiler"
|
|
5
6
|
require "event_engine/subject_registry"
|
|
6
7
|
require "event_engine/event_engine_helpers_writer"
|
|
@@ -39,6 +40,7 @@ module EventEngine
|
|
|
39
40
|
|
|
40
41
|
def run
|
|
41
42
|
event_schema = compile
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(@helper_path))
|
|
42
44
|
write_helper(event_schema)
|
|
43
45
|
write_schema_json(event_schema)
|
|
44
46
|
self
|
|
@@ -16,7 +16,7 @@ module EventEngine
|
|
|
16
16
|
].freeze
|
|
17
17
|
|
|
18
18
|
def self.compile(definitions, subject_registry: SubjectRegistry.new)
|
|
19
|
-
registry = SchemaRegistry.new
|
|
19
|
+
registry = Definition::SchemaRegistry.new
|
|
20
20
|
subject_violations = []
|
|
21
21
|
name_violations = []
|
|
22
22
|
reserved_violations = []
|
|
@@ -24,9 +24,17 @@ module EventEngine
|
|
|
24
24
|
def self.generate(event_schema, root_module: "EventEngine", emit: "EventEngine.emit",
|
|
25
25
|
header: HEADER, group_by_domain: true, schema_filename: nil)
|
|
26
26
|
body = group_by_domain ? grouped_body(event_schema, emit) : flat_body(event_schema, emit)
|
|
27
|
-
body = "#{schema_path_accessor(schema_filename)}#{body}" if schema_filename
|
|
27
|
+
body = "#{schema_path_accessor(schema_filename)}#{body}#{pack_registration}" if schema_filename
|
|
28
28
|
|
|
29
|
-
"#{header}module #{root_module}\n#{body}end\n"
|
|
29
|
+
"#{header}#{pack_require if schema_filename}module #{root_module}\n#{body}end\n"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.pack_require
|
|
33
|
+
%(require "event_engine/definition"\n\n)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.pack_registration
|
|
37
|
+
"\n EventEngine::Definition.register_pack(self)\n"
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
def self.schema_path_accessor(filename)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "event_engine/definition"
|
|
2
|
+
|
|
3
|
+
namespace :event_definition do
|
|
4
|
+
desc "Generate the pack's helper and schema.json from its EventDefinitions"
|
|
5
|
+
task :generate do
|
|
6
|
+
config = EventEngine::Definition.configuration
|
|
7
|
+
|
|
8
|
+
definitions = EventEngine::DefinitionLoader.load!(config.definitions_path)
|
|
9
|
+
|
|
10
|
+
build = EventEngine::DomainPackBuild.run(
|
|
11
|
+
definitions,
|
|
12
|
+
helper_path: config.helper_path,
|
|
13
|
+
root_module: config.root_module,
|
|
14
|
+
subject_registry: config.subject_registry
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
puts "Wrote #{config.root_module} helper to #{config.helper_path}"
|
|
18
|
+
puts "Wrote #{config.root_module} schema to #{build.schema_path}"
|
|
19
|
+
end
|
|
20
|
+
end
|