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 +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/Rakefile +8 -0
- data/docs/examples.md +96 -0
- data/lib/event_engine/definition/configuration.rb +14 -0
- data/lib/event_engine/definition/null_publisher.rb +13 -0
- data/lib/event_engine/definition/version.rb +7 -0
- data/lib/event_engine/definition.rb +46 -0
- data/lib/event_engine/definition_loader.rb +23 -0
- data/lib/event_engine/domain_pack_build.rb +77 -0
- data/lib/event_engine/dsl_compiler.rb +79 -0
- data/lib/event_engine/event_definition/inputs.rb +31 -0
- data/lib/event_engine/event_definition/payloads.rb +33 -0
- data/lib/event_engine/event_definition/schemas.rb +178 -0
- data/lib/event_engine/event_definition/validation.rb +18 -0
- data/lib/event_engine/event_definition.rb +48 -0
- data/lib/event_engine/event_engine_helpers_writer.rb +100 -0
- data/lib/event_engine/event_schema.rb +80 -0
- data/lib/event_engine/lifecycle_definition.rb +77 -0
- data/lib/event_engine/schema_registry.rb +71 -0
- data/lib/event_engine/subject_registry.rb +40 -0
- data/lib/tasks/event_engine_definition.rake +19 -0
- data/sig/event_engine/definition.rbs +6 -0
- metadata +76 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class EventDefinition
|
|
3
|
+
module Payloads
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def optional_payload(name, from: nil, attr: nil)
|
|
10
|
+
payload_fields << {
|
|
11
|
+
name: name.to_sym,
|
|
12
|
+
required: false,
|
|
13
|
+
from: from,
|
|
14
|
+
attr: attr
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def required_payload(name, from: nil, attr: nil)
|
|
19
|
+
payload_fields << {
|
|
20
|
+
name: name.to_sym,
|
|
21
|
+
required: true,
|
|
22
|
+
from: from,
|
|
23
|
+
attr: attr
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def payload_fields
|
|
28
|
+
@payload_fields ||= []
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
module EventEngine
|
|
5
|
+
class EventDefinition
|
|
6
|
+
module Schemas
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend ClassMethods
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Schema < Struct.new(
|
|
12
|
+
:event_name,
|
|
13
|
+
:event_version,
|
|
14
|
+
:event_type,
|
|
15
|
+
:subject,
|
|
16
|
+
:domain,
|
|
17
|
+
:required_inputs,
|
|
18
|
+
:optional_inputs,
|
|
19
|
+
:payload_fields,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def self.from_h(hash)
|
|
24
|
+
h = hash.transform_keys(&:to_sym)
|
|
25
|
+
|
|
26
|
+
new(
|
|
27
|
+
event_name: h[:event_name]&.to_sym,
|
|
28
|
+
event_version: h[:event_version],
|
|
29
|
+
event_type: h[:event_type]&.to_sym,
|
|
30
|
+
subject: h[:subject]&.to_sym,
|
|
31
|
+
domain: h[:domain]&.to_sym,
|
|
32
|
+
required_inputs: Array(h[:required_inputs]).map(&:to_sym),
|
|
33
|
+
optional_inputs: Array(h[:optional_inputs]).map(&:to_sym),
|
|
34
|
+
payload_fields: Array(h[:payload_fields]).map { |field| payload_field_from_h(field) }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.payload_field_from_h(field)
|
|
39
|
+
f = field.transform_keys(&:to_sym)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
name: f[:name]&.to_sym,
|
|
43
|
+
required: f[:required],
|
|
44
|
+
from: f[:from]&.to_sym,
|
|
45
|
+
attr: f[:attr]&.to_sym
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fingerprint
|
|
50
|
+
Digest::SHA256.hexdigest(
|
|
51
|
+
canonical_representation
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
{
|
|
57
|
+
event_name: event_name,
|
|
58
|
+
event_version: event_version,
|
|
59
|
+
event_type: event_type,
|
|
60
|
+
subject: subject,
|
|
61
|
+
domain: domain,
|
|
62
|
+
required_inputs: required_inputs,
|
|
63
|
+
optional_inputs: optional_inputs,
|
|
64
|
+
payload_fields: payload_fields.map { |field| payload_field_h(field) },
|
|
65
|
+
fingerprint: fingerprint
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_ruby
|
|
70
|
+
<<~RUBY.strip
|
|
71
|
+
EventEngine::EventDefinition::Schema.new(
|
|
72
|
+
event_name: #{event_name.inspect},
|
|
73
|
+
event_version: #{event_version.inspect},
|
|
74
|
+
event_type: #{event_type.inspect},
|
|
75
|
+
subject: #{subject.inspect},
|
|
76
|
+
domain: #{domain.inspect},
|
|
77
|
+
required_inputs: #{required_inputs.inspect},
|
|
78
|
+
optional_inputs: #{optional_inputs.inspect},
|
|
79
|
+
payload_fields: [#{payload_fields.map { |h| ruby_hash(h) }.join(", ")}]
|
|
80
|
+
)
|
|
81
|
+
RUBY
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def canonical_representation
|
|
87
|
+
{
|
|
88
|
+
event_name: event_name.to_s,
|
|
89
|
+
event_type: event_type.to_s,
|
|
90
|
+
required_inputs: required_inputs.map(&:to_s).sort,
|
|
91
|
+
optional_inputs: optional_inputs.map(&:to_s).sort,
|
|
92
|
+
payload_fields: payload_fields
|
|
93
|
+
.map { |h| h.transform_values { |v| v.to_s } }
|
|
94
|
+
.sort_by { |h| h[:name].to_s }
|
|
95
|
+
}.to_json
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def payload_field_h(field)
|
|
99
|
+
{
|
|
100
|
+
name: field[:name],
|
|
101
|
+
from: field[:from],
|
|
102
|
+
attr: field[:attr],
|
|
103
|
+
required: field[:required]
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ruby_hash(hash)
|
|
108
|
+
inner = hash.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
109
|
+
"{#{inner}}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
module ClassMethods
|
|
114
|
+
def schema
|
|
115
|
+
errors = schema_errors
|
|
116
|
+
raise ArgumentError, errors.join(", ") if errors.any?
|
|
117
|
+
|
|
118
|
+
required = inputs.select { |_, v| v== :required }.keys
|
|
119
|
+
optional = inputs.select { |_, v| v== :optional }.keys
|
|
120
|
+
|
|
121
|
+
Schema.new(
|
|
122
|
+
event_name: @event_name,
|
|
123
|
+
event_type: @event_type,
|
|
124
|
+
subject: @subject,
|
|
125
|
+
domain: @domain,
|
|
126
|
+
required_inputs: required,
|
|
127
|
+
optional_inputs: optional,
|
|
128
|
+
payload_fields: payload_fields
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def schema_errors
|
|
133
|
+
errors = []
|
|
134
|
+
validate_identity(errors)
|
|
135
|
+
validate_payload_fields(errors)
|
|
136
|
+
errors
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def valid_schema?
|
|
140
|
+
schema_errors.empty?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def validate_identity(errors)
|
|
146
|
+
errors << "event_name is required" unless @event_name
|
|
147
|
+
errors << "event_type is required" unless @event_type
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_payload_fields(errors)
|
|
151
|
+
seen = {}
|
|
152
|
+
|
|
153
|
+
payload_fields.each do |field|
|
|
154
|
+
name = field[:name]
|
|
155
|
+
|
|
156
|
+
if seen[name]
|
|
157
|
+
errors << "duplicate payload field: #{name}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if RESERVED_PAYLOAD_FIELDS.include?(name)
|
|
161
|
+
errors << "payload field uses reserved name: #{name}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if field[:from].nil?
|
|
165
|
+
errors << "payload field #{name} must have a from:"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
unless inputs.key?(field[:from])
|
|
169
|
+
errors << "payload field #{name} references unknown input: #{field[:from]}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
seen[name] = true
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
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,48 @@
|
|
|
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
|
+
class EventDefinition
|
|
8
|
+
RESERVED_PAYLOAD_FIELDS = %i[
|
|
9
|
+
event_name
|
|
10
|
+
event_type
|
|
11
|
+
event_version
|
|
12
|
+
occurred_at
|
|
13
|
+
created_at
|
|
14
|
+
updated_at
|
|
15
|
+
published_at
|
|
16
|
+
metadata
|
|
17
|
+
idempotency_key
|
|
18
|
+
attempts
|
|
19
|
+
dead_lettered_at
|
|
20
|
+
aggregate_type
|
|
21
|
+
aggregate_id
|
|
22
|
+
aggregate_version
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
include Inputs
|
|
26
|
+
include Payloads
|
|
27
|
+
include Validation
|
|
28
|
+
include Schemas
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def event_name(value)
|
|
32
|
+
@event_name = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def event_type(value)
|
|
36
|
+
@event_type = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def subject(value)
|
|
40
|
+
@subject = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def domain(value)
|
|
44
|
+
@domain = value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class EventEngineHelpersWriter
|
|
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
|
+
ENVELOPE_KEYS = DslCompiler::RESERVED_INPUT_NAMES
|
|
14
|
+
|
|
15
|
+
def self.write(path, event_schema, root_module: "EventEngine", emit: "EventEngine.emit",
|
|
16
|
+
header: HEADER, group_by_domain: true, schema_filename: nil)
|
|
17
|
+
File.write(
|
|
18
|
+
path,
|
|
19
|
+
generate(event_schema, root_module: root_module, emit: emit, header: header,
|
|
20
|
+
group_by_domain: group_by_domain, schema_filename: schema_filename)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.generate(event_schema, root_module: "EventEngine", emit: "EventEngine.emit",
|
|
25
|
+
header: HEADER, group_by_domain: true, schema_filename: nil)
|
|
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
|
|
28
|
+
|
|
29
|
+
"#{header}module #{root_module}\n#{body}end\n"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.schema_path_accessor(filename)
|
|
33
|
+
" def self.schema_path\n File.expand_path(#{filename.inspect}, __dir__)\n end\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.grouped_body(event_schema, emit)
|
|
37
|
+
events_by_domain(event_schema).map do |domain, event_names|
|
|
38
|
+
helpers = helpers_for(event_schema, domain, event_names, emit, indent: 4)
|
|
39
|
+
|
|
40
|
+
" module #{module_name(domain)}\n#{helpers} end\n"
|
|
41
|
+
end.join
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.flat_body(event_schema, emit)
|
|
45
|
+
events_by_domain(event_schema).map do |domain, event_names|
|
|
46
|
+
helpers_for(event_schema, domain, event_names, emit, indent: 2)
|
|
47
|
+
end.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.helpers_for(event_schema, domain, event_names, emit, indent:)
|
|
51
|
+
event_names.map do |event_name|
|
|
52
|
+
helper(domain, event_schema.latest_for(event_name, domain: domain), emit, indent)
|
|
53
|
+
end.join
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.events_by_domain(event_schema)
|
|
57
|
+
event_schema.schemas_by_event.keys
|
|
58
|
+
.group_by { |(domain, _event_name)| domain }
|
|
59
|
+
.transform_values { |pairs| pairs.map { |(_domain, event_name)| event_name }.sort }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.module_name(domain)
|
|
63
|
+
domain.to_s.split("_").map(&:capitalize).join
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.helper(domain, schema, emit, indent)
|
|
67
|
+
pad = " " * indent
|
|
68
|
+
call_pad = " " * (indent + 2)
|
|
69
|
+
arg_pad = " " * (indent + 4)
|
|
70
|
+
|
|
71
|
+
arguments = [
|
|
72
|
+
schema.event_name.inspect,
|
|
73
|
+
"domain: #{domain.inspect}",
|
|
74
|
+
"inputs: #{inputs_hash(schema)}",
|
|
75
|
+
*ENVELOPE_KEYS.map { |name| "#{name}: #{name}" }
|
|
76
|
+
].map { |argument| "#{arg_pad}#{argument}" }.join(",\n")
|
|
77
|
+
|
|
78
|
+
"#{pad}def self.#{schema.event_name}(#{signature(schema)})\n" \
|
|
79
|
+
"#{call_pad}#{emit}(\n" \
|
|
80
|
+
"#{arguments}\n" \
|
|
81
|
+
"#{call_pad})\n" \
|
|
82
|
+
"#{pad}end\n"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.signature(schema)
|
|
86
|
+
required = schema.required_inputs.map { |name| "#{name}:" }
|
|
87
|
+
optional = schema.optional_inputs.map { |name| "#{name}: nil" }
|
|
88
|
+
envelope = ENVELOPE_KEYS.map { |name| "#{name}: nil" }
|
|
89
|
+
|
|
90
|
+
(required + optional + envelope).join(", ")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.inputs_hash(schema)
|
|
94
|
+
inputs = schema.required_inputs + schema.optional_inputs
|
|
95
|
+
return "{}" if inputs.empty?
|
|
96
|
+
|
|
97
|
+
"{ #{inputs.map { |name| "#{name}: #{name}" }.join(", ")} }"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class EventSchema
|
|
3
|
+
class DuplicateEventNameError < StandardError; end
|
|
4
|
+
|
|
5
|
+
def self.define(&block)
|
|
6
|
+
schema = new
|
|
7
|
+
block.call(schema)
|
|
8
|
+
schema
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@schemas_by_event = {}
|
|
13
|
+
@finalized = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(schema)
|
|
17
|
+
raise FrozenError, "EventSchema is finalized" if @finalized
|
|
18
|
+
key = key_for(schema.domain, schema.event_name)
|
|
19
|
+
version = schema.event_version
|
|
20
|
+
|
|
21
|
+
@schemas_by_event[key] ||= {}
|
|
22
|
+
guard_duplicate_event_name!(@schemas_by_event[key][version], schema)
|
|
23
|
+
@schemas_by_event[key][version] = schema
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def guard_duplicate_event_name!(existing, incoming)
|
|
27
|
+
return unless existing
|
|
28
|
+
|
|
29
|
+
raise DuplicateEventNameError,
|
|
30
|
+
"duplicate (domain, event_name) " \
|
|
31
|
+
"(#{incoming.domain.inspect}, #{incoming.event_name.inspect}): " \
|
|
32
|
+
"already registered at version #{existing.event_version.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def events(domain: nil)
|
|
36
|
+
@schemas_by_event.keys
|
|
37
|
+
.select { |(schema_domain, _name)| domain.nil? || schema_domain == domain }
|
|
38
|
+
.map { |(_domain, event_name)| event_name }
|
|
39
|
+
.uniq
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def versions_for(event_name, domain: nil)
|
|
43
|
+
version_sets_for(event_name, domain).flat_map(&:keys).uniq.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def schema_for(event_name, version, domain: nil)
|
|
47
|
+
set = version_sets_for(event_name, domain).find { |versions| versions.key?(version) }
|
|
48
|
+
set && set[version]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def latest_for(event_name, domain: nil)
|
|
52
|
+
merged = version_sets_for(event_name, domain).reduce({}, :merge)
|
|
53
|
+
return nil if merged.empty?
|
|
54
|
+
merged[merged.keys.max]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def finalize!
|
|
58
|
+
@finalized = true
|
|
59
|
+
@schemas_by_event.each_value(&:freeze)
|
|
60
|
+
@schemas_by_event.freeze
|
|
61
|
+
freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def schemas_by_event
|
|
65
|
+
@schemas_by_event
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def key_for(domain, event_name)
|
|
71
|
+
[domain, event_name]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def version_sets_for(event_name, domain = nil)
|
|
75
|
+
@schemas_by_event.select do |(schema_domain, name), _versions|
|
|
76
|
+
name == event_name && (domain.nil? || schema_domain == domain)
|
|
77
|
+
end.values
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
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 lifecycle(*verbs)
|
|
18
|
+
@verbs = verbs
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on(verb, &block)
|
|
22
|
+
verb_overrides[verb] = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verb_overrides
|
|
26
|
+
@verb_overrides ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generated_events
|
|
30
|
+
@generated_events ||= Array(@verbs).map { |verb| build_event(verb) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def materialize_all!
|
|
34
|
+
subclasses.flat_map(&:generated_events)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def declared_subject
|
|
38
|
+
@subject
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def declared_event_type
|
|
42
|
+
@event_type
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_event(verb)
|
|
48
|
+
template = self
|
|
49
|
+
name = :"#{template.declared_subject}_#{verb}"
|
|
50
|
+
|
|
51
|
+
Class.new(EventDefinition) do
|
|
52
|
+
event_name name
|
|
53
|
+
event_type template.declared_event_type
|
|
54
|
+
|
|
55
|
+
define_singleton_method(:inspect) { "EventEngine::LifecycleDefinition(#{name})" }
|
|
56
|
+
define_singleton_method(:to_s) { inspect }
|
|
57
|
+
subject template.declared_subject
|
|
58
|
+
|
|
59
|
+
template.inputs.each do |name, kind|
|
|
60
|
+
kind == :required ? input(name) : optional_input(name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
template.payload_fields.each do |field|
|
|
64
|
+
if field[:required]
|
|
65
|
+
required_payload field[:name], from: field[:from], attr: field[:attr]
|
|
66
|
+
else
|
|
67
|
+
optional_payload field[:name], from: field[:from], attr: field[:attr]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
override = template.verb_overrides[verb]
|
|
72
|
+
class_eval(&override) if override
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class SchemaRegistry
|
|
3
|
+
class UnknownEventError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class RegistryFrozenError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def initialize(event_schema = EventSchema.new)
|
|
8
|
+
@event_schema = event_schema
|
|
9
|
+
@loaded = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(schema)
|
|
13
|
+
@event_schema.register(schema)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def events
|
|
17
|
+
@event_schema.events
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def versions_for(event_name, domain: nil)
|
|
21
|
+
@event_schema.versions_for(event_name, domain: domain)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load_from_schema!(schema)
|
|
25
|
+
raise RegistryFrozenError, "EventRegistry already loaded" if loaded?
|
|
26
|
+
@event_schema = schema
|
|
27
|
+
|
|
28
|
+
@loaded = true
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset!
|
|
33
|
+
@event_schema = {}
|
|
34
|
+
@loaded = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def schema(event_name, version: nil, domain: nil)
|
|
38
|
+
raise RegistryFrozenError, "EventRegistry not loaded" unless loaded?
|
|
39
|
+
|
|
40
|
+
schema =
|
|
41
|
+
if version
|
|
42
|
+
@event_schema.schema_for(event_name, version, domain: domain)
|
|
43
|
+
else
|
|
44
|
+
@event_schema.latest_for(event_name, domain: domain)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unless schema
|
|
48
|
+
raise UnknownEventError,
|
|
49
|
+
"Unknown #{version ? "version #{version} for " : ""}event: #{event_name}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
schema
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def latest_for(event_name, domain: nil)
|
|
56
|
+
@event_schema.latest_for(event_name, domain: domain)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def event_schema
|
|
60
|
+
@event_schema
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def finalize!
|
|
64
|
+
@event_schema.finalize!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def loaded?
|
|
68
|
+
@loaded == true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class SubjectRegistry
|
|
3
|
+
class UnknownSubjectError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class Subject
|
|
6
|
+
attr_reader :name, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(name, **metadata)
|
|
9
|
+
@name = name
|
|
10
|
+
@metadata = metadata
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.define(&block)
|
|
15
|
+
registry = new
|
|
16
|
+
registry.instance_eval(&block) if block
|
|
17
|
+
registry
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@subjects = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subject(name, **metadata)
|
|
25
|
+
@subjects[name] = Subject.new(name, **metadata)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](name)
|
|
29
|
+
@subjects[name]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registered?(name)
|
|
33
|
+
@subjects.key?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def names
|
|
37
|
+
@subjects.keys
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "event_engine/definition"
|
|
2
|
+
|
|
3
|
+
namespace :event_engine do
|
|
4
|
+
namespace :definition do
|
|
5
|
+
desc "Generate the pack's helper and schema.json from its EventDefinitions"
|
|
6
|
+
task :dump do
|
|
7
|
+
config = EventEngine::Definition.configuration
|
|
8
|
+
|
|
9
|
+
definitions = EventEngine::DefinitionLoader.load!(config.definitions_path)
|
|
10
|
+
|
|
11
|
+
EventEngine::DomainPackBuild.run(
|
|
12
|
+
definitions,
|
|
13
|
+
helper_path: config.helper_path,
|
|
14
|
+
root_module: config.root_module,
|
|
15
|
+
subject_registry: config.subject_registry
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|