event_engine 0.1.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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +53 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +517 -0
  5. data/Rakefile +12 -0
  6. data/app/assets/config/event_engine_manifest.js +1 -0
  7. data/app/assets/stylesheets/event_engine/application.css +15 -0
  8. data/app/controllers/event_engine/application_controller.rb +4 -0
  9. data/app/helpers/event_engine/application_helper.rb +4 -0
  10. data/app/jobs/event_engine/application_job.rb +4 -0
  11. data/app/mailers/event_engine/application_mailer.rb +6 -0
  12. data/app/models/event_engine/application_record.rb +5 -0
  13. data/app/views/layouts/event_engine/application.html.erb +15 -0
  14. data/config/routes.rb +2 -0
  15. data/lib/event_engine/configuration.rb +20 -0
  16. data/lib/event_engine/definition_loader.rb +26 -0
  17. data/lib/event_engine/dsl_compiler.rb +50 -0
  18. data/lib/event_engine/engine.rb +56 -0
  19. data/lib/event_engine/event.rb +32 -0
  20. data/lib/event_engine/event_builder.rb +45 -0
  21. data/lib/event_engine/event_definition/inputs.rb +43 -0
  22. data/lib/event_engine/event_definition/payloads.rb +47 -0
  23. data/lib/event_engine/event_definition/schemas.rb +158 -0
  24. data/lib/event_engine/event_definition/validation.rb +18 -0
  25. data/lib/event_engine/event_definition.rb +76 -0
  26. data/lib/event_engine/event_schema.rb +99 -0
  27. data/lib/event_engine/event_schema_dumper.rb +13 -0
  28. data/lib/event_engine/event_schema_loader.rb +37 -0
  29. data/lib/event_engine/event_schema_merger.rb +62 -0
  30. data/lib/event_engine/event_schema_writer.rb +47 -0
  31. data/lib/event_engine/handler_registry.rb +23 -0
  32. data/lib/event_engine/lifecycle_definition.rb +86 -0
  33. data/lib/event_engine/process_type.rb +26 -0
  34. data/lib/event_engine/railtie.rb +9 -0
  35. data/lib/event_engine/reference/guide.md +129 -0
  36. data/lib/event_engine/reference.rb +16 -0
  37. data/lib/event_engine/schema_catalog.rb +50 -0
  38. data/lib/event_engine/schema_compatibility.rb +50 -0
  39. data/lib/event_engine/schema_diff.rb +35 -0
  40. data/lib/event_engine/schema_drift_guard.rb +38 -0
  41. data/lib/event_engine/schema_registry.rb +122 -0
  42. data/lib/event_engine/subject_registry.rb +40 -0
  43. data/lib/event_engine/the_local/agents/event_engine-develop.md +142 -0
  44. data/lib/event_engine/the_local/agents/event_engine-info.md +140 -0
  45. data/lib/event_engine/the_local/agents/event_engine-install.md +140 -0
  46. data/lib/event_engine/the_local.rb +55 -0
  47. data/lib/event_engine/version.rb +3 -0
  48. data/lib/event_engine.rb +197 -0
  49. data/lib/generators/event_engine/install_generator.rb +31 -0
  50. data/lib/generators/event_engine/templates/event_schema.rb +10 -0
  51. data/lib/generators/event_engine/templates/initializer.rb +4 -0
  52. data/lib/tasks/event_engine_catalog.rake +13 -0
  53. data/lib/tasks/event_engine_schema.rake +82 -0
  54. data/lib/tasks/event_engine_schema_check.rake +20 -0
  55. data/lib/tasks/event_engine_tasks.rake +4 -0
  56. metadata +127 -0
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ EventEngine::Engine.routes.draw do
2
+ end
@@ -0,0 +1,20 @@
1
+ module EventEngine
2
+ # Holds configuration options for EventEngine core.
3
+ #
4
+ # @example
5
+ # EventEngine.configure do |config|
6
+ # config.logger = Rails.logger
7
+ # end
8
+ class Configuration
9
+ # @!attribute [rw] logger
10
+ # Logger instance for EventEngine messages.
11
+ # @return [Logger] defaults to +Rails.logger+
12
+ attr_accessor :logger
13
+
14
+ attr_accessor :metadata_defaults
15
+
16
+ def initialize
17
+ @logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module EventEngine
2
+ module DefinitionLoader
3
+ class << self
4
+ def ensure_loaded!
5
+ eager_load_definitions!
6
+ LifecycleDefinition.materialize_all!
7
+ end
8
+
9
+ def eager_load_definitions!
10
+ return if loaded?
11
+
12
+ unless defined?(Rails) && Rails.application
13
+ raise "EventEngine requires a Rails application to load definitions"
14
+ end
15
+
16
+ Rails.application.eager_load!
17
+
18
+ @loaded = true
19
+ end
20
+
21
+ def loaded?
22
+ @loaded ||= false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ module EventEngine
2
+ class DslCompiler
3
+ class InvalidEventNameError < StandardError; end
4
+
5
+ SNAKE_CASE = /\A[a-z][a-z0-9_]*\z/
6
+
7
+ def self.compile(definitions)
8
+ registry = SchemaRegistry.new
9
+ subject_violations = []
10
+ name_violations = []
11
+
12
+ Array(definitions).each do |definition|
13
+ schema = definition.schema
14
+ record_subject_violation(schema, subject_violations)
15
+ record_name_violation(schema, name_violations)
16
+ registry.register(schema)
17
+ end
18
+
19
+ raise_invalid_event_names(name_violations)
20
+ raise_unknown_subjects(subject_violations)
21
+
22
+ registry
23
+ end
24
+
25
+ def self.record_name_violation(schema, violations)
26
+ return if schema.event_name.to_s.match?(SNAKE_CASE)
27
+
28
+ violations << schema.event_name.inspect
29
+ end
30
+
31
+ def self.raise_invalid_event_names(violations)
32
+ return if violations.empty?
33
+
34
+ raise InvalidEventNameError, "event names must be snake_case: #{violations.join(", ")}"
35
+ end
36
+
37
+ def self.record_subject_violation(schema, violations)
38
+ return if schema.subject.nil?
39
+ return if EventEngine.subject_registry.registered?(schema.subject)
40
+
41
+ violations << "#{schema.event_name}: unknown subject #{schema.subject.inspect}"
42
+ end
43
+
44
+ def self.raise_unknown_subjects(violations)
45
+ return if violations.empty?
46
+
47
+ raise SubjectRegistry::UnknownSubjectError, violations.join(", ")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ module EventEngine
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace EventEngine
4
+
5
+ initializer "event_engine.load_schema_and_install_helpers" do |app|
6
+ app.config.after_initialize do
7
+ schema_path = Rails.root.join("db", "event_schema.rb")
8
+
9
+ if File.exist?(schema_path)
10
+ Engine.send(
11
+ :load_schema_and_install_helpers,
12
+ schema_path: schema_path
13
+ )
14
+ else
15
+ Engine.send(
16
+ :handle_missing_schema!,
17
+ schema_path
18
+ )
19
+ end
20
+ end
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ def load_schema_and_install_helpers(schema_path:)
27
+ EventEngine.boot_from_schema!(
28
+ schema_path: schema_path,
29
+ registry: EventEngine::SchemaRegistry.new
30
+ )
31
+ end
32
+
33
+ def handle_missing_schema!(schema_path)
34
+ if Rails.env.development? || Rails.env.test?
35
+ Rails.logger.warn(
36
+ "[EventEngine] Schema file not found at #{schema_path}. " \
37
+ "Run: bin/rails event_engine:schema:dump"
38
+ )
39
+ return
40
+ end
41
+
42
+ raise <<~MSG
43
+ EventEngine schema file missing.
44
+
45
+ Expected to find:
46
+ #{schema_path}
47
+
48
+ Run:
49
+ bin/rails event_engine:schema:dump
50
+
51
+ And commit the generated file.
52
+ MSG
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ module EventEngine
2
+ # A non-persisted, in-memory representation of an emitted event with a
3
+ # symbol-keyed payload. Passed to subscribers' +#handle(event)+ at every
4
+ # in-process level (1-3), and returned by the emitter for levels 1 and 2.
5
+ Event = Struct.new(
6
+ :event_name,
7
+ :event_type,
8
+ :event_version,
9
+ :process_type,
10
+ :subject,
11
+ :domain,
12
+ :payload,
13
+ :metadata,
14
+ :occurred_at,
15
+ :idempotency_key,
16
+ :aggregate_type,
17
+ :aggregate_id,
18
+ :aggregate_version,
19
+ keyword_init: true
20
+ ) do
21
+ # Builds an Event from a record responding to the same members (e.g. an
22
+ # OutboxEvent), normalizing the payload to symbol keys.
23
+ #
24
+ # @param record [#payload]
25
+ # @return [Event]
26
+ def self.from(record)
27
+ attrs = members.to_h { |member| [member, record.public_send(member)] }
28
+ attrs[:payload] = attrs[:payload].to_h.transform_keys(&:to_sym)
29
+ new(**attrs)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ module EventEngine
2
+ # Constructs an event payload hash from a schema and input data.
3
+ # Validates that required inputs are present and no unknown inputs are passed.
4
+ class EventBuilder
5
+ # Builds an event attributes hash from schema and input data.
6
+ #
7
+ # @param schema [EventDefinition::Schema] the event schema
8
+ # @param data [Hash] input data keyed by input name
9
+ # @return [Hash] event attributes including :event_name, :event_type, :event_version, :payload
10
+ # @raise [ArgumentError] if required inputs are missing or unknown inputs are provided
11
+ def self.build(schema:, data:)
12
+ validate_inputs!(schema, data)
13
+
14
+ payload = {}
15
+
16
+ schema.payload_fields.each do |field|
17
+ input = data[field[:from]]
18
+ next if input.nil? && !field[:required]
19
+
20
+ value = field[:attr] ? input.public_send(field[:attr]) : input
21
+ payload[field[:name]] = value
22
+ end
23
+
24
+ {
25
+ event_name: schema.event_name,
26
+ event_type: schema.event_type,
27
+ event_version: schema.event_version,
28
+ payload: payload
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def self.validate_inputs!(schema, data)
35
+ data_keys = data.keys.map(&:to_sym)
36
+
37
+ missing = schema.required_inputs - data_keys
38
+ raise ArgumentError, "missing required input: #{missing.join(', ')}" if missing.any?
39
+
40
+ allowed = schema.required_inputs + schema.optional_inputs
41
+ unknown = data_keys - allowed
42
+ raise ArgumentError, "unknown input: #{unknown.join(', ')}" if unknown.any?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ module EventEngine
2
+ class EventDefinition
3
+ # DSL methods for declaring required and optional inputs on an event definition.
4
+ module Inputs
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Declares a required input for this event.
11
+ #
12
+ # @param name [Symbol] the input name
13
+ # @raise [ArgumentError] if the input is already declared
14
+ def input(name)
15
+ name = name.to_sym
16
+ if inputs.key?(name)
17
+ raise ArgumentError, "duplicate input: #{name}"
18
+ end
19
+ inputs[name] = :required
20
+ end
21
+
22
+ # Declares an optional input for this event.
23
+ #
24
+ # @param name [Symbol] the input name
25
+ # @raise [ArgumentError] if the input is already declared
26
+ def optional_input(name)
27
+ name = name.to_sym
28
+ if inputs.key?(name)
29
+ raise ArgumentError, "duplicate input: #{name}"
30
+ end
31
+ inputs[name] = :optional
32
+ end
33
+
34
+ # Returns all declared inputs as a hash of name => :required/:optional.
35
+ #
36
+ # @return [Hash{Symbol => Symbol}]
37
+ def inputs
38
+ @inputs ||= {}
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ module EventEngine
2
+ class EventDefinition
3
+ # DSL methods for declaring payload fields on an event definition.
4
+ module Payloads
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Declares an optional payload field.
11
+ #
12
+ # @param name [Symbol] the field name in the event payload
13
+ # @param from [Symbol] the input to extract the value from
14
+ # @param attr [Symbol, nil] method to call on the input (nil for passthrough)
15
+ def optional_payload(name, from: nil, attr: nil)
16
+ payload_fields << {
17
+ name: name.to_sym,
18
+ required: false,
19
+ from: from,
20
+ attr: attr
21
+ }
22
+ end
23
+
24
+ # Declares a required payload field.
25
+ #
26
+ # @param name [Symbol] the field name in the event payload
27
+ # @param from [Symbol] the input to extract the value from
28
+ # @param attr [Symbol, nil] method to call on the input (nil for passthrough)
29
+ def required_payload(name, from: nil, attr: nil)
30
+ payload_fields << {
31
+ name: name.to_sym,
32
+ required: true,
33
+ from: from,
34
+ attr: attr
35
+ }
36
+ end
37
+
38
+ # Returns all declared payload field definitions.
39
+ #
40
+ # @return [Array<Hash>]
41
+ def payload_fields
42
+ @payload_fields ||= []
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,158 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,76 @@
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
@@ -0,0 +1,99 @@
1
+ module EventEngine
2
+ # Container for all event schemas, organized by event name and version.
3
+ # This is the data structure loaded from the compiled +db/event_schema.rb+ file
4
+ # and used by {SchemaRegistry} at runtime.
5
+ class EventSchema
6
+ class DuplicateEventNameError < StandardError; end
7
+
8
+ # Creates an EventSchema using a block DSL (used by the schema file).
9
+ #
10
+ # @yieldparam schema [EventSchema]
11
+ # @return [EventSchema]
12
+ def self.define(&block)
13
+ schema = new
14
+ block.call(schema)
15
+ schema
16
+ end
17
+
18
+ def initialize
19
+ @schemas_by_event = {}
20
+ @finalized = false
21
+ end
22
+
23
+ # Registers a schema for a specific event name and version.
24
+ #
25
+ # @param schema [EventDefinition::Schema]
26
+ # @raise [FrozenError] if the schema has been finalized
27
+ def register(schema)
28
+ raise FrozenError, "EventSchema is finalized" if @finalized
29
+ event_name = schema.event_name
30
+ version = schema.event_version
31
+
32
+ @schemas_by_event[event_name] ||= {}
33
+ guard_duplicate_event_name!(@schemas_by_event[event_name][version], schema)
34
+ @schemas_by_event[event_name][version] = schema
35
+ end
36
+
37
+ def guard_duplicate_event_name!(existing, incoming)
38
+ return unless existing
39
+
40
+ raise DuplicateEventNameError,
41
+ "duplicate event_name #{incoming.event_name.inspect}: " \
42
+ "already registered with domain #{existing.domain.inspect}, " \
43
+ "cannot register again with domain #{incoming.domain.inspect}"
44
+ end
45
+
46
+ # Returns all registered event names.
47
+ #
48
+ # @return [Array<Symbol>]
49
+ def events
50
+ @schemas_by_event.keys
51
+ end
52
+
53
+ # Returns sorted version numbers for a given event.
54
+ #
55
+ # @param event_name [Symbol]
56
+ # @return [Array<Integer>]
57
+ def versions_for(event_name)
58
+ versions = @schemas_by_event[event_name]
59
+ return [] unless versions
60
+ versions.keys.sort
61
+ end
62
+
63
+ # Returns the schema for a specific event name and version.
64
+ #
65
+ # @param event_name [Symbol]
66
+ # @param version [Integer]
67
+ # @return [EventDefinition::Schema, nil]
68
+ def schema_for(event_name, version)
69
+ @schemas_by_event.dig(event_name, version)
70
+ end
71
+
72
+ # Returns the latest (highest version) schema for an event.
73
+ #
74
+ # @param event_name [Symbol]
75
+ # @return [EventDefinition::Schema, nil]
76
+ def latest_for(event_name)
77
+ versions = @schemas_by_event[event_name]
78
+ return nil unless versions && !versions.empty?
79
+ versions[versions.keys.max]
80
+ end
81
+
82
+ # Freezes the schema, preventing further registrations.
83
+ #
84
+ # @return [void]
85
+ def finalize!
86
+ @finalized = true
87
+ @schemas_by_event.each_value(&:freeze)
88
+ @schemas_by_event.freeze
89
+ freeze
90
+ end
91
+
92
+ # Returns the internal hash of schemas keyed by event name and version.
93
+ #
94
+ # @return [Hash{Symbol => Hash{Integer => EventDefinition::Schema}}]
95
+ def schemas_by_event
96
+ @schemas_by_event
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,13 @@
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