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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +210 -399
- data/Rakefile +0 -1
- data/lib/event_engine/catalog_entry.rb +84 -0
- data/lib/event_engine/configuration.rb +9 -9
- data/lib/event_engine/definition_publisher.rb +22 -0
- data/lib/event_engine/event.rb +0 -8
- data/lib/event_engine/event_builder.rb +0 -8
- data/lib/event_engine/event_schema.rb +32 -57
- data/lib/event_engine/event_schema_json_loader.rb +19 -0
- data/lib/event_engine/handler_registry.rb +4 -4
- data/lib/event_engine/invalid_rules_error.rb +8 -0
- data/lib/event_engine/processing_rules.rb +39 -0
- data/lib/event_engine/processor_registry.rb +19 -0
- data/lib/event_engine/processor_resolver.rb +16 -0
- data/lib/event_engine/railtie.rb +54 -4
- data/lib/event_engine/rules_file.rb +14 -0
- data/lib/event_engine/schema_catalog_builder.rb +18 -0
- data/lib/event_engine/schema_registry.rb +7 -58
- data/lib/event_engine/unregistered_processor_error.rb +8 -0
- data/lib/event_engine/unroutable_event_error.rb +9 -0
- data/lib/event_engine/unrouted_events_error.rb +9 -0
- data/lib/event_engine/version.rb +1 -1
- data/lib/event_engine.rb +117 -123
- data/lib/tasks/event_engine_catalog.rake +15 -6
- data/lib/tasks/event_engine_rules.rake +10 -0
- data/the_local/agents/event_engine-develop.md +154 -0
- data/the_local/agents/event_engine-info.md +82 -0
- data/the_local/agents/event_engine-install.md +99 -0
- data/the_local/interface.yml +27 -0
- metadata +45 -48
- data/app/assets/config/event_engine_manifest.js +0 -1
- data/app/assets/stylesheets/event_engine/application.css +0 -15
- data/app/controllers/event_engine/application_controller.rb +0 -4
- data/app/helpers/event_engine/application_helper.rb +0 -4
- data/app/jobs/event_engine/application_job.rb +0 -4
- data/app/mailers/event_engine/application_mailer.rb +0 -6
- data/app/models/event_engine/application_record.rb +0 -5
- data/app/views/layouts/event_engine/application.html.erb +0 -15
- data/config/routes.rb +0 -2
- data/lib/event_engine/definition_loader.rb +0 -26
- data/lib/event_engine/dsl_compiler.rb +0 -50
- data/lib/event_engine/engine.rb +0 -56
- data/lib/event_engine/event_definition/inputs.rb +0 -43
- data/lib/event_engine/event_definition/payloads.rb +0 -47
- data/lib/event_engine/event_definition/schemas.rb +0 -158
- data/lib/event_engine/event_definition/validation.rb +0 -18
- data/lib/event_engine/event_definition.rb +0 -76
- data/lib/event_engine/event_schema_dumper.rb +0 -13
- data/lib/event_engine/event_schema_loader.rb +0 -37
- data/lib/event_engine/event_schema_merger.rb +0 -62
- data/lib/event_engine/event_schema_writer.rb +0 -47
- data/lib/event_engine/lifecycle_definition.rb +0 -86
- data/lib/event_engine/process_type.rb +0 -26
- data/lib/event_engine/reference/guide.md +0 -129
- data/lib/event_engine/reference.rb +0 -16
- data/lib/event_engine/schema_catalog.rb +0 -50
- data/lib/event_engine/schema_compatibility.rb +0 -50
- data/lib/event_engine/schema_diff.rb +0 -35
- data/lib/event_engine/schema_drift_guard.rb +0 -38
- data/lib/event_engine/subject_registry.rb +0 -40
- data/lib/event_engine/the_local/agents/event_engine-develop.md +0 -142
- data/lib/event_engine/the_local/agents/event_engine-info.md +0 -140
- data/lib/event_engine/the_local/agents/event_engine-install.md +0 -140
- data/lib/event_engine/the_local.rb +0 -55
- data/lib/generators/event_engine/install_generator.rb +0 -31
- data/lib/generators/event_engine/templates/event_schema.rb +0 -10
- data/lib/generators/event_engine/templates/initializer.rb +0 -4
- data/lib/tasks/event_engine_schema.rake +0 -82
- data/lib/tasks/event_engine_schema_check.rake +0 -20
- data/lib/tasks/event_engine_tasks.rake +0 -4
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventDefinition
|
|
3
|
-
# Provides schema generation and fingerprinting for event definitions.
|
|
4
|
-
module Schemas
|
|
5
|
-
def self.included(base)
|
|
6
|
-
base.extend ClassMethods
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# Immutable representation of a compiled event schema.
|
|
10
|
-
# Holds the event identity, inputs, and payload field definitions.
|
|
11
|
-
# Used both at development time (compilation) and runtime (registry).
|
|
12
|
-
class Schema < Struct.new(
|
|
13
|
-
:event_name,
|
|
14
|
-
:event_version,
|
|
15
|
-
:event_type,
|
|
16
|
-
:process_type,
|
|
17
|
-
:subject,
|
|
18
|
-
:domain,
|
|
19
|
-
:required_inputs,
|
|
20
|
-
:optional_inputs,
|
|
21
|
-
:payload_fields,
|
|
22
|
-
keyword_init: true
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# Returns a SHA256 fingerprint of the schema's canonical representation.
|
|
26
|
-
# Used to detect schema changes and trigger version bumps.
|
|
27
|
-
#
|
|
28
|
-
# @return [String] hex-encoded SHA256 digest
|
|
29
|
-
def fingerprint
|
|
30
|
-
Digest::SHA256.hexdigest(
|
|
31
|
-
canonical_representation
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Serializes the schema to a Ruby source string for the schema file.
|
|
36
|
-
#
|
|
37
|
-
# @return [String]
|
|
38
|
-
def to_ruby
|
|
39
|
-
<<~RUBY.strip
|
|
40
|
-
EventEngine::EventDefinition::Schema.new(
|
|
41
|
-
event_name: #{event_name.inspect},
|
|
42
|
-
event_version: #{event_version.inspect},
|
|
43
|
-
event_type: #{event_type.inspect},
|
|
44
|
-
process_type: #{process_type.inspect},
|
|
45
|
-
subject: #{subject.inspect},
|
|
46
|
-
domain: #{domain.inspect},
|
|
47
|
-
required_inputs: #{required_inputs.inspect},
|
|
48
|
-
optional_inputs: #{optional_inputs.inspect},
|
|
49
|
-
payload_fields: [#{payload_fields.map { |h| ruby_hash(h) }.join(", ")}]
|
|
50
|
-
)
|
|
51
|
-
RUBY
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def canonical_representation
|
|
57
|
-
{
|
|
58
|
-
event_name: event_name.to_s,
|
|
59
|
-
event_type: event_type.to_s,
|
|
60
|
-
required_inputs: required_inputs.map(&:to_s).sort,
|
|
61
|
-
optional_inputs: optional_inputs.map(&:to_s).sort,
|
|
62
|
-
payload_fields: payload_fields
|
|
63
|
-
.map { |h| h.transform_values { |v| v.to_s } }
|
|
64
|
-
.sort_by { |h| h[:name].to_s }
|
|
65
|
-
}.to_json
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def ruby_hash(hash)
|
|
69
|
-
inner = hash.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
70
|
-
"{#{inner}}"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
module ClassMethods
|
|
75
|
-
# Builds and returns a {Schema} from this definition's DSL declarations.
|
|
76
|
-
#
|
|
77
|
-
# @return [Schema]
|
|
78
|
-
# @raise [ArgumentError] if the definition has validation errors
|
|
79
|
-
def schema
|
|
80
|
-
errors = schema_errors
|
|
81
|
-
raise ArgumentError, errors.join(", ") if errors.any?
|
|
82
|
-
|
|
83
|
-
required = inputs.select { |_, v| v== :required }.keys
|
|
84
|
-
optional = inputs.select { |_, v| v== :optional }.keys
|
|
85
|
-
|
|
86
|
-
Schema.new(
|
|
87
|
-
event_name: @event_name,
|
|
88
|
-
event_type: @event_type,
|
|
89
|
-
process_type: @process_type,
|
|
90
|
-
subject: @subject,
|
|
91
|
-
domain: @domain,
|
|
92
|
-
required_inputs: required,
|
|
93
|
-
optional_inputs: optional,
|
|
94
|
-
payload_fields: payload_fields
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Returns validation errors for this definition, if any.
|
|
99
|
-
#
|
|
100
|
-
# @return [Array<String>]
|
|
101
|
-
def schema_errors
|
|
102
|
-
errors = []
|
|
103
|
-
validate_identity(errors)
|
|
104
|
-
validate_process_type(errors)
|
|
105
|
-
validate_payload_fields(errors)
|
|
106
|
-
errors
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Whether this definition has a valid schema (no errors).
|
|
110
|
-
#
|
|
111
|
-
# @return [Boolean]
|
|
112
|
-
def valid_schema?
|
|
113
|
-
schema_errors.empty?
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
private
|
|
117
|
-
|
|
118
|
-
def validate_identity(errors)
|
|
119
|
-
errors << "event_name is required" unless @event_name
|
|
120
|
-
errors << "event_type is required" unless @event_type
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def validate_process_type(errors)
|
|
124
|
-
return if @process_type.nil? || ProcessType.known?(@process_type)
|
|
125
|
-
errors << "process_type is unknown: #{@process_type.inspect}"
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def validate_payload_fields(errors)
|
|
129
|
-
seen = {}
|
|
130
|
-
|
|
131
|
-
payload_fields.each do |field|
|
|
132
|
-
name = field[:name]
|
|
133
|
-
|
|
134
|
-
if seen[name]
|
|
135
|
-
errors << "duplicate payload field: #{name}"
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
if RESERVED_PAYLOAD_FIELDS.include?(name)
|
|
139
|
-
errors << "payload field uses reserved name: #{name}"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
if field[:from].nil?
|
|
143
|
-
errors << "payload field #{name} must have a from:"
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
unless inputs.key?(field[:from])
|
|
147
|
-
errors << "payload field #{name} references unknown input: #{field[:from]}"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# attr: is optional - when omitted, input value is used directly (passthrough)
|
|
151
|
-
|
|
152
|
-
seen[name] = true
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventDefinition
|
|
3
|
-
module Validation
|
|
4
|
-
def validate_inputs!(inputs)
|
|
5
|
-
declared = self.class.inputs
|
|
6
|
-
provided = inputs.keys.map(&:to_sym)
|
|
7
|
-
|
|
8
|
-
return if declared.empty?
|
|
9
|
-
|
|
10
|
-
missing = declared - provided
|
|
11
|
-
raise ArgumentError, "missing input: #{missing.join(', ')}" if missing.any?
|
|
12
|
-
|
|
13
|
-
extra = provided - declared
|
|
14
|
-
raise ArgumentError, "undeclared input: #{extra.join(', ')}" if extra.any?
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
require "event_engine/event_definition/inputs"
|
|
2
|
-
require "event_engine/event_definition/payloads"
|
|
3
|
-
require "event_engine/event_definition/validation"
|
|
4
|
-
require "event_engine/event_definition/schemas"
|
|
5
|
-
|
|
6
|
-
module EventEngine
|
|
7
|
-
# Base class for defining events using the EventEngine DSL.
|
|
8
|
-
#
|
|
9
|
-
# Subclass this to declare an event's name, type, inputs, and payload fields.
|
|
10
|
-
# Definitions are compiled into a schema file at development time and are
|
|
11
|
-
# not used at runtime.
|
|
12
|
-
#
|
|
13
|
-
# @example Define an event
|
|
14
|
-
# class CowFed < EventEngine::EventDefinition
|
|
15
|
-
# input :cow
|
|
16
|
-
# optional_input :farmer
|
|
17
|
-
#
|
|
18
|
-
# event_name :cow_fed
|
|
19
|
-
# event_type :domain
|
|
20
|
-
#
|
|
21
|
-
# required_payload :weight, from: :cow, attr: :weight
|
|
22
|
-
# optional_payload :farmer_name, from: :farmer, attr: :name
|
|
23
|
-
# end
|
|
24
|
-
class EventDefinition
|
|
25
|
-
# Payload field names reserved by the outbox schema.
|
|
26
|
-
RESERVED_PAYLOAD_FIELDS = %i[
|
|
27
|
-
event_name
|
|
28
|
-
event_type
|
|
29
|
-
event_version
|
|
30
|
-
occurred_at
|
|
31
|
-
created_at
|
|
32
|
-
updated_at
|
|
33
|
-
published_at
|
|
34
|
-
metadata
|
|
35
|
-
idempotency_key
|
|
36
|
-
attempts
|
|
37
|
-
dead_lettered_at
|
|
38
|
-
aggregate_type
|
|
39
|
-
aggregate_id
|
|
40
|
-
aggregate_version
|
|
41
|
-
].freeze
|
|
42
|
-
|
|
43
|
-
include Inputs
|
|
44
|
-
include Payloads
|
|
45
|
-
include Validation
|
|
46
|
-
include Schemas
|
|
47
|
-
|
|
48
|
-
class << self
|
|
49
|
-
# Sets the event name for this definition.
|
|
50
|
-
#
|
|
51
|
-
# @param value [Symbol] the event name (e.g. +:cow_fed+)
|
|
52
|
-
def event_name(value)
|
|
53
|
-
@event_name = value
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Sets the event type for this definition.
|
|
57
|
-
#
|
|
58
|
-
# @param value [Symbol] the event type (e.g. +:domain+, +:integration+)
|
|
59
|
-
def event_type(value)
|
|
60
|
-
@event_type = value
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def process_type(value)
|
|
64
|
-
@process_type = value
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def subject(value)
|
|
68
|
-
@subject = value
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def domain(value)
|
|
72
|
-
@domain = value
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventSchemaDumper
|
|
3
|
-
def self.dump!(definitions:, path:)
|
|
4
|
-
compiled_schema = DslCompiler.compile(definitions)
|
|
5
|
-
compiled_schema.finalize!
|
|
6
|
-
|
|
7
|
-
loaded_schema = EventSchemaLoader.load(path)
|
|
8
|
-
merged_schema = EventSchemaMerger.merge(compiled_schema, loaded_schema)
|
|
9
|
-
|
|
10
|
-
EventSchemaWriter.write(path, merged_schema)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventSchemaLoader
|
|
3
|
-
def self.load(path)
|
|
4
|
-
registry = SchemaRegistry.new
|
|
5
|
-
return registry unless File.exist?(path)
|
|
6
|
-
|
|
7
|
-
contents = File.read(path.to_s)
|
|
8
|
-
return registry if contents.strip.empty?
|
|
9
|
-
|
|
10
|
-
sandbox = Module.new
|
|
11
|
-
sandbox.const_set(:EventEngine, EventEngine)
|
|
12
|
-
|
|
13
|
-
schema =
|
|
14
|
-
sandbox.module_eval(contents, path.to_s)
|
|
15
|
-
|
|
16
|
-
unless schema.is_a?(EventEngine::EventSchema)
|
|
17
|
-
raise <<~MSG
|
|
18
|
-
Invalid EventEngine schema file.
|
|
19
|
-
|
|
20
|
-
Expected #{path} to return an EventSchema from:
|
|
21
|
-
EventEngine::EventSchema.define { ... }
|
|
22
|
-
|
|
23
|
-
But got:
|
|
24
|
-
#{schema.inspect}
|
|
25
|
-
MSG
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
schema.schemas_by_event.each_value do |versions|
|
|
29
|
-
versions.each_value do |s|
|
|
30
|
-
registry.register(s)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
registry
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventSchemaMerger
|
|
3
|
-
def self.merge(compiled_registry, file_registry)
|
|
4
|
-
merged = EventSchema.new
|
|
5
|
-
|
|
6
|
-
file_loaded_schema = file_registry.event_schema
|
|
7
|
-
|
|
8
|
-
file_loaded_schema.events.each do |event|
|
|
9
|
-
file_loaded_schema.versions_for(event).each do |version|
|
|
10
|
-
merged.register(file_loaded_schema.schema_for(event, version))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Merge compiled schemas
|
|
15
|
-
compiled_registry.events.each do |event|
|
|
16
|
-
compiled_schema = compiled_registry.latest_for(event)
|
|
17
|
-
|
|
18
|
-
existing_versions = merged.versions_for(event)
|
|
19
|
-
latest_version = existing_versions.max
|
|
20
|
-
latest_schema = latest_version && merged.schema_for(event, latest_version)
|
|
21
|
-
|
|
22
|
-
if no_schema_change?(latest_schema, compiled_schema)
|
|
23
|
-
next
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
new_version = version(latest_version)
|
|
27
|
-
new_schema = compiled_schema.dup
|
|
28
|
-
new_schema.event_version = new_version
|
|
29
|
-
|
|
30
|
-
merged.register(new_schema)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
merged.finalize!
|
|
34
|
-
|
|
35
|
-
merged
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.changed?(compiled_registry, file_registry)
|
|
39
|
-
compiled_registry.events.any? do |event|
|
|
40
|
-
compiled_schema = compiled_registry.latest_for(event)
|
|
41
|
-
|
|
42
|
-
existing_versions = file_registry.versions_for(event)
|
|
43
|
-
latest_version = existing_versions.max
|
|
44
|
-
latest_schema = latest_version && file_registry.schema(event, version: latest_version)
|
|
45
|
-
|
|
46
|
-
# New event entirely
|
|
47
|
-
return true unless latest_schema
|
|
48
|
-
|
|
49
|
-
# Fingerprint mismatch means a new version would be created
|
|
50
|
-
latest_schema.fingerprint != compiled_schema.fingerprint
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.no_schema_change?(latest_schema, compiled_schema)
|
|
55
|
-
latest_schema && latest_schema.fingerprint == compiled_schema.fingerprint
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.version(latest_version)
|
|
59
|
-
(latest_version || 0) + 1
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
class EventSchemaWriter
|
|
3
|
-
HEADER = <<~RUBY.freeze
|
|
4
|
-
# This file is authoritative in production.
|
|
5
|
-
# It is generated from EventDefinitions via:
|
|
6
|
-
#
|
|
7
|
-
# bin/rails event_engine:schema:dump
|
|
8
|
-
#
|
|
9
|
-
# Do not edit manually.
|
|
10
|
-
|
|
11
|
-
RUBY
|
|
12
|
-
|
|
13
|
-
def self.write(path, event_schema)
|
|
14
|
-
schemas =
|
|
15
|
-
event_schema
|
|
16
|
-
.schemas_by_event
|
|
17
|
-
.flat_map { |_event, versions| versions.values }
|
|
18
|
-
.sort_by { |s| [s.event_name.to_s, s.event_version] }
|
|
19
|
-
|
|
20
|
-
File.open(path, "w") do |io|
|
|
21
|
-
io.write(HEADER)
|
|
22
|
-
io.write("EventEngine::EventSchema.define do |schema|\n")
|
|
23
|
-
|
|
24
|
-
schemas.each do |definition|
|
|
25
|
-
write_definition(io, definition)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
io.write("end\n")
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.write_definition(io, definition)
|
|
33
|
-
io.write(" schema.register(\n")
|
|
34
|
-
indent(io, 4) { definition.to_ruby }
|
|
35
|
-
io.write(" )\n")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.indent(io, spaces)
|
|
39
|
-
padding = " " * spaces
|
|
40
|
-
yield.each_line do |line|
|
|
41
|
-
io.write(padding)
|
|
42
|
-
io.write(line)
|
|
43
|
-
io.write("\n")
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
require "event_engine/event_definition"
|
|
2
|
-
|
|
3
|
-
module EventEngine
|
|
4
|
-
class LifecycleDefinition
|
|
5
|
-
include EventDefinition::Inputs
|
|
6
|
-
include EventDefinition::Payloads
|
|
7
|
-
|
|
8
|
-
class << self
|
|
9
|
-
def subject(value)
|
|
10
|
-
@subject = value
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def event_type(value)
|
|
14
|
-
@event_type = value
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def process_type(value)
|
|
18
|
-
@process_type = value
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def lifecycle(*verbs)
|
|
22
|
-
@verbs = verbs
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def on(verb, &block)
|
|
26
|
-
verb_overrides[verb] = block
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def verb_overrides
|
|
30
|
-
@verb_overrides ||= {}
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def generated_events
|
|
34
|
-
@generated_events ||= Array(@verbs).map { |verb| build_event(verb) }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def materialize_all!
|
|
38
|
-
subclasses.flat_map(&:generated_events)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def declared_subject
|
|
42
|
-
@subject
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def declared_event_type
|
|
46
|
-
@event_type
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def declared_process_type
|
|
50
|
-
@process_type
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def build_event(verb)
|
|
56
|
-
template = self
|
|
57
|
-
name = :"#{template.declared_subject}_#{verb}"
|
|
58
|
-
|
|
59
|
-
Class.new(EventDefinition) do
|
|
60
|
-
event_name name
|
|
61
|
-
event_type template.declared_event_type
|
|
62
|
-
|
|
63
|
-
define_singleton_method(:inspect) { "EventEngine::LifecycleDefinition(#{name})" }
|
|
64
|
-
define_singleton_method(:to_s) { inspect }
|
|
65
|
-
subject template.declared_subject
|
|
66
|
-
process_type template.declared_process_type if template.declared_process_type
|
|
67
|
-
|
|
68
|
-
template.inputs.each do |name, kind|
|
|
69
|
-
kind == :required ? input(name) : optional_input(name)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
template.payload_fields.each do |field|
|
|
73
|
-
if field[:required]
|
|
74
|
-
required_payload field[:name], from: field[:from], attr: field[:attr]
|
|
75
|
-
else
|
|
76
|
-
optional_payload field[:name], from: field[:from], attr: field[:attr]
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
override = template.verb_overrides[verb]
|
|
81
|
-
class_eval(&override) if override
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
module ProcessType
|
|
3
|
-
ALL = %i[inline background durable broker telemetry sourced].freeze
|
|
4
|
-
|
|
5
|
-
PROCESSORS = {
|
|
6
|
-
inline: :subscribers,
|
|
7
|
-
background: :subscribers,
|
|
8
|
-
durable: :delivery,
|
|
9
|
-
broker: :delivery,
|
|
10
|
-
telemetry: :telemetry,
|
|
11
|
-
sourced: :sourcing
|
|
12
|
-
}.freeze
|
|
13
|
-
|
|
14
|
-
def self.all
|
|
15
|
-
ALL
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.processor_for(type)
|
|
19
|
-
PROCESSORS[type]
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.known?(type)
|
|
23
|
-
ALL.include?(type)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
## EventEngine
|
|
2
|
-
|
|
3
|
-
> **DO NOT** explore the event_engine gem source code. This reference is the
|
|
4
|
-
> complete user-facing API, embedded verbatim into every event_engine local so
|
|
5
|
-
> their guidance never drifts. Keep it the single source of truth.
|
|
6
|
-
|
|
7
|
-
EventEngine is a Rails engine for defining domain events as declarative classes,
|
|
8
|
-
compiling them to a committed schema, emitting them through generated helpers, and
|
|
9
|
-
dispatching them to registered handlers. Core builds and routes events; it ships no
|
|
10
|
-
handlers of its own. Durable delivery, an event store, and ready-made subscriber
|
|
11
|
-
classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
|
|
12
|
-
`event_engine-subscribers`) — this reference covers core only.
|
|
13
|
-
|
|
14
|
-
### What it offers
|
|
15
|
-
|
|
16
|
-
**Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
class CowFed < EventEngine::EventDefinition
|
|
20
|
-
event_name :cow_fed # the event's identity (required)
|
|
21
|
-
event_type :domain # classification, e.g. :domain (required)
|
|
22
|
-
process_type :durable # routing type (optional; set it explicitly)
|
|
23
|
-
|
|
24
|
-
input :cow # a required input
|
|
25
|
-
optional_input :farmer # an optional input
|
|
26
|
-
|
|
27
|
-
required_payload :weight, from: :cow, attr: :weight
|
|
28
|
-
optional_payload :farmer_name, from: :farmer, attr: :name
|
|
29
|
-
end
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
| DSL method | Purpose |
|
|
33
|
-
|---|---|
|
|
34
|
-
| `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
|
|
35
|
-
| `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
|
|
36
|
-
| `process_type(:symbol)` | Routing type (optional). One of the six values below. |
|
|
37
|
-
| `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
|
|
38
|
-
| `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
|
|
39
|
-
| `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
|
|
40
|
-
|
|
41
|
-
Duplicate input names raise `ArgumentError`; payload `from:` must reference a
|
|
42
|
-
declared input.
|
|
43
|
-
|
|
44
|
-
**process_type** — core stamps this symbol onto every emitted event but does not act
|
|
45
|
-
on it. Which handlers receive an event is decided by each handler's `levels:`. The
|
|
46
|
-
values:
|
|
47
|
-
|
|
48
|
-
| value | intent |
|
|
49
|
-
|---|---|
|
|
50
|
-
| `:inline` | handled in-process, synchronously |
|
|
51
|
-
| `:background` | handled in-process, via a background job |
|
|
52
|
-
| `:durable` | handled when a durable outbox drains |
|
|
53
|
-
| `:broker` | published to an external transport |
|
|
54
|
-
| `:telemetry` | metrics / observability handlers |
|
|
55
|
-
| `:sourced` | an append-only event store |
|
|
56
|
-
|
|
57
|
-
The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
|
|
58
|
-
etc. their behavior; core just routes to whatever is registered. If `process_type`
|
|
59
|
-
is omitted it is `nil` — set it explicitly so routing intent is clear.
|
|
60
|
-
|
|
61
|
-
**Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
EventEngine.cow_fed(
|
|
65
|
-
cow: cow, farmer: farmer, # declared inputs, by name
|
|
66
|
-
occurred_at: Time.current, # optional, defaults to now
|
|
67
|
-
metadata: { request_id: "abc" }, # optional
|
|
68
|
-
idempotency_key: "…", # optional, defaults to a UUID
|
|
69
|
-
aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
|
|
70
|
-
event_version: 1 # optional, defaults to the latest schema version
|
|
71
|
-
)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Missing a required input, or passing an unknown one, raises `ArgumentError`. The
|
|
75
|
-
event's `payload` is symbol-keyed.
|
|
76
|
-
|
|
77
|
-
**Register handlers** — a handler is any object responding to `call(event)`:
|
|
78
|
-
|
|
79
|
-
```ruby
|
|
80
|
-
EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
|
|
81
|
-
EventEngine.dispatch(event) # fan an event out (emit helpers call this)
|
|
82
|
-
EventEngine.reset_handlers! # clear all handlers
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Handlers run synchronously in registration order; if one raises, the rest don't run.
|
|
86
|
-
Keep handlers idempotent.
|
|
87
|
-
|
|
88
|
-
**Configure** — `config/initializers/event_engine.rb`, logger only:
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
EventEngine.configure { |config| config.logger = Rails.logger }
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
|
|
95
|
-
is authoritative at boot:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
|
|
99
|
-
bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
A new event is version 1; changing an event's identity or payload bumps its version.
|
|
103
|
-
Changing only `process_type` does not bump the version.
|
|
104
|
-
|
|
105
|
-
### Install
|
|
106
|
-
|
|
107
|
-
1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
|
|
108
|
-
2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
|
|
109
|
-
`config/initializers/event_engine.rb`.
|
|
110
|
-
3. Define events as classes in `app/event_definitions/`.
|
|
111
|
-
4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
|
|
112
|
-
5. Set `config.logger` in the initializer if you want something other than the default.
|
|
113
|
-
|
|
114
|
-
Durable delivery, an event store, and prebuilt subscriber classes are separate gems
|
|
115
|
-
(`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
|
|
116
|
-
when you need them and follow their own setup.
|
|
117
|
-
|
|
118
|
-
### EventEngine conventions
|
|
119
|
-
|
|
120
|
-
- Define one `EventDefinition` class per event in `app/event_definitions/`; never
|
|
121
|
-
hand-build event hashes.
|
|
122
|
-
- Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
|
|
123
|
-
raw payload hashes to the emit helper.
|
|
124
|
-
- Always set `process_type` explicitly so routing intent is clear.
|
|
125
|
-
- Emit only through the generated `EventEngine.<event_name>` helpers, passing the
|
|
126
|
-
declared inputs.
|
|
127
|
-
- Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
|
|
128
|
-
definition change; keep `event_engine:schema_check` green in CI.
|
|
129
|
-
- Keep handlers and subscribers idempotent.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
# Single source of truth for the EventEngine API reference. The companion
|
|
3
|
-
# Claude Code subagents (and any future doc generator) read from here so they
|
|
4
|
-
# can never disagree about how the gem is used.
|
|
5
|
-
module Reference
|
|
6
|
-
DIR = File.expand_path("reference", __dir__)
|
|
7
|
-
|
|
8
|
-
def self.content
|
|
9
|
-
read("guide.md")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.read(name)
|
|
13
|
-
File.read(File.join(DIR, name)).chomp
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|