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
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,62 @@
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
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,23 @@
1
+ module EventEngine
2
+ class HandlerRegistry
3
+ def initialize
4
+ @handlers = []
5
+ end
6
+
7
+ def register(handler, levels:)
8
+ @handlers << { handler: handler, levels: levels }
9
+ end
10
+
11
+ def dispatch(event)
12
+ @handlers.each do |registration|
13
+ levels = registration[:levels]
14
+ registration[:handler].call(event) if levels == :all || levels.include?(event.process_type)
15
+ end
16
+ event
17
+ end
18
+
19
+ def clear!
20
+ @handlers.clear
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,86 @@
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
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,9 @@
1
+ module EventEngine
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/event_engine_tasks.rake"
5
+ load "tasks/event_engine_schema.rake"
6
+ load "tasks/event_engine_schema_check.rake"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,129 @@
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.
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,50 @@
1
+ module EventEngine
2
+ class SchemaCatalog
3
+ def initialize(schema_registry:, subject_registry:)
4
+ @schema_registry = schema_registry
5
+ @subject_registry = subject_registry
6
+ end
7
+
8
+ def to_markdown
9
+ (["# Event Catalog"] + event_sections).join("\n\n") + "\n"
10
+ end
11
+
12
+ private
13
+
14
+ def event_sections
15
+ @schema_registry.events.map do |event|
16
+ section(@schema_registry.latest_for(event))
17
+ end
18
+ end
19
+
20
+ def section(schema)
21
+ ([
22
+ "## #{schema.event_name} (v#{schema.event_version})",
23
+ "- Type: #{schema.event_type}",
24
+ subject_line(schema)
25
+ ] + payload_lines(schema)).compact.join("\n")
26
+ end
27
+
28
+ def payload_lines(schema)
29
+ return [] if schema.payload_fields.empty?
30
+
31
+ ["- Payload:"] + schema.payload_fields.map do |field|
32
+ " - #{field[:name]} (#{field[:required] ? "required" : "optional"})"
33
+ end
34
+ end
35
+
36
+ def subject_line(schema)
37
+ return nil unless schema.subject
38
+
39
+ details = subject_details(schema.subject)
40
+ details.empty? ? "- Subject: #{schema.subject}" : "- Subject: #{schema.subject} (#{details})"
41
+ end
42
+
43
+ def subject_details(name)
44
+ registered = @subject_registry[name]
45
+ return "" unless registered
46
+
47
+ registered.metadata.map { |key, value| "#{key}: #{value}" }.join(", ")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ module EventEngine
2
+ class SchemaCompatibility
3
+ def self.violations(old_registry:, new_registry:)
4
+ new_registry.events.flat_map do |event|
5
+ previous = old_registry.latest_for(event)
6
+ next [] unless previous
7
+
8
+ current = new_registry.latest_for(event)
9
+ new(old: previous, new: current).breaking_changes.map do |change|
10
+ "#{event}: #{change}"
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(old:, new:)
16
+ @old = old
17
+ @new = new
18
+ end
19
+
20
+ def breaking_changes
21
+ removed_required_fields + newly_required_fields
22
+ end
23
+
24
+ private
25
+
26
+ def removed_required_fields
27
+ (required_names(@old) - field_names(@new)).map do |name|
28
+ "required payload field removed: #{name}"
29
+ end
30
+ end
31
+
32
+ def newly_required_fields
33
+ (optional_names(@old) & required_names(@new)).map do |name|
34
+ "payload field became required: #{name}"
35
+ end
36
+ end
37
+
38
+ def field_names(schema)
39
+ schema.payload_fields.map { |field| field[:name] }
40
+ end
41
+
42
+ def required_names(schema)
43
+ schema.payload_fields.select { |field| field[:required] }.map { |field| field[:name] }
44
+ end
45
+
46
+ def optional_names(schema)
47
+ schema.payload_fields.reject { |field| field[:required] }.map { |field| field[:name] }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ module EventEngine
2
+ class SchemaDiff
3
+ def initialize(expected:, actual:)
4
+ @expected = expected
5
+ @actual = actual
6
+ end
7
+
8
+ def changed?
9
+ @expected != @actual
10
+ end
11
+
12
+ def to_s
13
+ expected_lines = @expected.lines
14
+ actual_lines = @actual.lines
15
+
16
+ Array.new([ expected_lines.size, actual_lines.size ].max) do |index|
17
+ line_diff(expected_lines[index], actual_lines[index])
18
+ end.compact.join
19
+ end
20
+
21
+ private
22
+
23
+ def line_diff(expected_line, actual_line)
24
+ return " #{expected_line}" if expected_line == actual_line
25
+
26
+ [ marked("-", expected_line), marked("+", actual_line) ].compact.join
27
+ end
28
+
29
+ def marked(sign, line)
30
+ return nil if line.nil?
31
+
32
+ "#{sign}#{line}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ require "event_engine/schema_diff"
2
+
3
+ module EventEngine
4
+ class SchemaDriftGuard
5
+ class DriftError < StandardError; end
6
+
7
+ def self.check!(schema_path:, definitions:)
8
+ raise DriftError, "Schema file does not exist: #{schema_path}" unless File.exist?(schema_path)
9
+
10
+ committed = File.read(schema_path)
11
+ regenerated = dump_to_string(definitions)
12
+
13
+ return true if committed == regenerated
14
+
15
+ raise DriftError, <<~MSG
16
+ EventEngine schema drift detected.
17
+
18
+ The DSL definitions do not match #{schema_path}.
19
+
20
+ #{SchemaDiff.new(expected: committed, actual: regenerated)}
21
+ Run:
22
+ bin/rails event_engine:schema:dump
23
+
24
+ And commit the updated schema file.
25
+ MSG
26
+ end
27
+
28
+ def self.dump_to_string(definitions)
29
+ Tempfile.create("event_schema") do |file|
30
+ EventEngine::EventSchemaDumper.dump!(
31
+ definitions: definitions,
32
+ path: file.path
33
+ )
34
+ File.read(file.path)
35
+ end
36
+ end
37
+ end
38
+ end