acta 0.2.0 → 0.3.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 +44 -0
- data/README.md +228 -33
- data/RELEASING.md +107 -0
- data/docs/README.md +31 -0
- data/docs/event_driven_pub_sub.md +258 -0
- data/gemfiles/rails_7_2.gemfile +20 -0
- data/gemfiles/rails_8_0.gemfile +20 -0
- data/gemfiles/rails_8_1.gemfile +20 -0
- data/lib/acta/model.rb +7 -7
- data/lib/acta/reactor.rb +22 -0
- data/lib/acta/testing.rb +33 -0
- data/lib/acta/types/array.rb +35 -0
- data/lib/acta/types/model.rb +39 -0
- data/lib/acta/version.rb +1 -1
- data/lib/acta.rb +16 -1
- data/lib/generators/acta/install/install_generator.rb +6 -6
- metadata +21 -16
- data/PLAN.md +0 -158
- data/lib/acta/array_type.rb +0 -30
- data/lib/acta/model_type.rb +0 -32
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acta
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Gladhill
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-04-28 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activejob
|
|
@@ -15,56 +15,56 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '7.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '7.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: activemodel
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '7.2'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '7.2'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: activerecord
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '7.2'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
53
|
+
version: '7.2'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: activesupport
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
60
|
+
version: '7.2'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '
|
|
67
|
+
version: '7.2'
|
|
68
68
|
description: |-
|
|
69
69
|
Acta ships a small, opinionated set of primitives for event-driven and
|
|
70
70
|
event-sourced Rails applications: events, handlers, projections, reactors,
|
|
@@ -80,8 +80,8 @@ files:
|
|
|
80
80
|
- ".tool-versions"
|
|
81
81
|
- CHANGELOG.md
|
|
82
82
|
- LICENSE
|
|
83
|
-
- PLAN.md
|
|
84
83
|
- README.md
|
|
84
|
+
- RELEASING.md
|
|
85
85
|
- Rakefile
|
|
86
86
|
- app/controllers/acta/web/application_controller.rb
|
|
87
87
|
- app/controllers/acta/web/events_controller.rb
|
|
@@ -90,13 +90,17 @@ files:
|
|
|
90
90
|
- app/views/acta/web/events/show.html.erb
|
|
91
91
|
- app/views/layouts/acta/web/application.html.erb
|
|
92
92
|
- config/routes.rb
|
|
93
|
+
- docs/README.md
|
|
94
|
+
- docs/event_driven_pub_sub.md
|
|
95
|
+
- gemfiles/rails_7_2.gemfile
|
|
96
|
+
- gemfiles/rails_8_0.gemfile
|
|
97
|
+
- gemfiles/rails_8_1.gemfile
|
|
93
98
|
- lib/acta.rb
|
|
94
99
|
- lib/acta/actor.rb
|
|
95
100
|
- lib/acta/adapters.rb
|
|
96
101
|
- lib/acta/adapters/base.rb
|
|
97
102
|
- lib/acta/adapters/postgres.rb
|
|
98
103
|
- lib/acta/adapters/sqlite.rb
|
|
99
|
-
- lib/acta/array_type.rb
|
|
100
104
|
- lib/acta/command.rb
|
|
101
105
|
- lib/acta/current.rb
|
|
102
106
|
- lib/acta/errors.rb
|
|
@@ -104,7 +108,6 @@ files:
|
|
|
104
108
|
- lib/acta/events_query.rb
|
|
105
109
|
- lib/acta/handler.rb
|
|
106
110
|
- lib/acta/model.rb
|
|
107
|
-
- lib/acta/model_type.rb
|
|
108
111
|
- lib/acta/projection.rb
|
|
109
112
|
- lib/acta/projection_managed.rb
|
|
110
113
|
- lib/acta/railtie.rb
|
|
@@ -116,7 +119,9 @@ files:
|
|
|
116
119
|
- lib/acta/testing.rb
|
|
117
120
|
- lib/acta/testing/dsl.rb
|
|
118
121
|
- lib/acta/testing/matchers.rb
|
|
122
|
+
- lib/acta/types/array.rb
|
|
119
123
|
- lib/acta/types/encrypted_string.rb
|
|
124
|
+
- lib/acta/types/model.rb
|
|
120
125
|
- lib/acta/version.rb
|
|
121
126
|
- lib/acta/web.rb
|
|
122
127
|
- lib/acta/web/engine.rb
|
|
@@ -128,10 +133,10 @@ homepage: https://github.com/whoojemaflip/acta
|
|
|
128
133
|
licenses:
|
|
129
134
|
- MIT
|
|
130
135
|
metadata:
|
|
131
|
-
homepage_uri: https://github.com/whoojemaflip/acta
|
|
132
136
|
source_code_uri: https://github.com/whoojemaflip/acta
|
|
133
137
|
changelog_uri: https://github.com/whoojemaflip/acta/blob/main/CHANGELOG.md
|
|
134
138
|
bug_tracker_uri: https://github.com/whoojemaflip/acta/issues
|
|
139
|
+
rubygems_mfa_required: 'true'
|
|
135
140
|
rdoc_options: []
|
|
136
141
|
require_paths:
|
|
137
142
|
- lib
|
|
@@ -139,14 +144,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
139
144
|
requirements:
|
|
140
145
|
- - ">="
|
|
141
146
|
- !ruby/object:Gem::Version
|
|
142
|
-
version: '3.
|
|
147
|
+
version: '3.2'
|
|
143
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
149
|
requirements:
|
|
145
150
|
- - ">="
|
|
146
151
|
- !ruby/object:Gem::Version
|
|
147
152
|
version: '0'
|
|
148
153
|
requirements: []
|
|
149
|
-
rubygems_version: 3.
|
|
154
|
+
rubygems_version: 3.6.2
|
|
150
155
|
specification_version: 4
|
|
151
156
|
summary: Lightweight event-driven and event-sourced primitives for Rails.
|
|
152
157
|
test_files: []
|
data/PLAN.md
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# Acta Implementation Plan
|
|
2
|
-
|
|
3
|
-
Companion to the design doc (private, at `~/Sites/Journal/ideas/event_source_rails.md`).
|
|
4
|
-
This file is version-controlled alongside the code and tracks the milestone
|
|
5
|
-
breakdown for reaching v1.0.
|
|
6
|
-
|
|
7
|
-
## Conventions
|
|
8
|
-
|
|
9
|
-
- **Ruby hash shorthand** (Ruby 3.1+): `{ name:, age: }` when variables in
|
|
10
|
-
scope match keys. Applied everywhere.
|
|
11
|
-
- **RSpec** exclusively for v1. Minitest matcher support considered post-v1.
|
|
12
|
-
- **TDD**: every milestone is a sequence of small red → green → refactor
|
|
13
|
-
cycles. One behaviour per commit where practical.
|
|
14
|
-
- **Rubocop**: `rubocop-rails-omakase` style. Clean before each commit.
|
|
15
|
-
- **Commits**: atomic, imperative mood, one logical change each.
|
|
16
|
-
- **Semver** from v0.1. Public API stability begins at v1.0.
|
|
17
|
-
|
|
18
|
-
## Environment
|
|
19
|
-
|
|
20
|
-
- Ruby: 3.4+
|
|
21
|
-
- Rails floor: 8.1+
|
|
22
|
-
- Local: `~/Sites/acta`
|
|
23
|
-
- Remote: `git@github.com:whoojemaflip/acta.git`
|
|
24
|
-
|
|
25
|
-
## Milestone breakdown
|
|
26
|
-
|
|
27
|
-
Each milestone is independently shippable.
|
|
28
|
-
|
|
29
|
-
### M0 — Scaffolding ✅
|
|
30
|
-
|
|
31
|
-
Gem skeleton, RSpec, rubocop-rails-omakase, CI, README, LICENSE, CHANGELOG,
|
|
32
|
-
`PLAN.md` in repo. Baseline green build.
|
|
33
|
-
|
|
34
|
-
### M1 — First emit (the round-trip milestone) ✅
|
|
35
|
-
|
|
36
|
-
**Goal:** `Acta.emit(event)` persists a row; `Acta.events.last` reads it back.
|
|
37
|
-
|
|
38
|
-
1. Adapter seam — spec `Acta::Adapters::Base` interface; SQLite adapter stub.
|
|
39
|
-
2. Migration generator — `rails g acta:install` creates the events table.
|
|
40
|
-
3. `Acta::Model` — AM::Attributes + AM::Model + `to_acta_hash` / `from_acta_hash`
|
|
41
|
-
+ `validate!` in initialize raising `Acta::InvalidEvent`.
|
|
42
|
-
4. `Acta::Event < Acta::Model` — adds `uuid`, `event_type`, `event_version`,
|
|
43
|
-
`occurred_at`, `recorded_at`, `actor`.
|
|
44
|
-
5. `Acta::Actor` value object — `type`, `id`, `source`, `metadata`.
|
|
45
|
-
6. `Acta::Current` — CurrentAttributes with `actor`.
|
|
46
|
-
7. `Acta.configure` — connection + single-store `:default` registration
|
|
47
|
-
(latent store concept).
|
|
48
|
-
8. `Acta.emit(event)` — strict on missing actor (`Acta::MissingActor`);
|
|
49
|
-
persists via adapter; returns the persisted event.
|
|
50
|
-
9. `Acta.events` — query API returning `Acta::Event` instances from the log.
|
|
51
|
-
10. Error leaves so far: `Error`, `InvalidEvent`, `MissingActor`,
|
|
52
|
-
`ConfigurationError`, `AdapterError`.
|
|
53
|
-
|
|
54
|
-
**Checkpoint:** end-to-end spec that configures, emits, queries, asserts.
|
|
55
|
-
|
|
56
|
-
### M2 — Streams & concurrency ✅
|
|
57
|
-
|
|
58
|
-
1. Stream DSL — `stream :order, key: :order_id` on event classes.
|
|
59
|
-
2. Sequence calculation in SQLite adapter (BEGIN IMMEDIATE + SELECT MAX).
|
|
60
|
-
3. `ConcurrencyConflict` on unique-index violation.
|
|
61
|
-
4. Stream-scoped query — `Acta.events.for_stream(type:, key:)`.
|
|
62
|
-
5. `on_concurrent_write :raise` machinery (wires into M6 commands).
|
|
63
|
-
|
|
64
|
-
### M3 — Handlers & dispatch ✅
|
|
65
|
-
|
|
66
|
-
1. `Acta::Handler` base class + `on EventClass do |event| ... end` DSL.
|
|
67
|
-
2. Auto-registration via inheritance + Rails `eager_load_paths`.
|
|
68
|
-
3. Dispatch on emit (sync base handlers).
|
|
69
|
-
4. Registry isolation for specs — `Acta.reset_handlers!`.
|
|
70
|
-
|
|
71
|
-
### M4 — Projections ✅
|
|
72
|
-
|
|
73
|
-
1. `Acta::Projection < Acta::Handler` with sync+transactional contract.
|
|
74
|
-
2. Projections run inside emit transaction.
|
|
75
|
-
3. `ProjectionError` wraps underlying exception + projection class.
|
|
76
|
-
4. `Acta.rebuild!` — truncate projections, replay log, re-run projections.
|
|
77
|
-
5. Replay skips reactors (prep for M5).
|
|
78
|
-
|
|
79
|
-
### M5 — Reactors ✅
|
|
80
|
-
|
|
81
|
-
1. `Acta::Reactor < Acta::Handler` with after-commit + ActiveJob default.
|
|
82
|
-
2. `Acta::ReactorJob` — loads event by uuid, dispatches to reactor class.
|
|
83
|
-
3. `sync true` opt-in.
|
|
84
|
-
4. Skip on replay.
|
|
85
|
-
5. Actor propagation via `Acta::Current` serialized into ActiveJob.
|
|
86
|
-
|
|
87
|
-
### M6 — Commands ✅
|
|
88
|
-
|
|
89
|
-
1. `Acta::Command < Acta::Model` — param validation via AM::Attributes.
|
|
90
|
-
2. `stream :order, key: :order_id` on command — declares aggregate identity.
|
|
91
|
-
3. `on_concurrent_write :raise` — captures stream sequence at instantiation.
|
|
92
|
-
4. `.call(**params)` entry; `emit event` as instance method;
|
|
93
|
-
`InvalidCommand` on validation failure.
|
|
94
|
-
5. Auto-loading from `app/commands/`.
|
|
95
|
-
|
|
96
|
-
### M7 — Testing DSL (`Acta::Testing`) ✅
|
|
97
|
-
|
|
98
|
-
1. RSpec matchers: `emit(EventClass).with(...)`, `emit_events([...])`,
|
|
99
|
-
`not_to emit_any_events`.
|
|
100
|
-
2. `given_events { ... }` — seeds the log directly without running reactors.
|
|
101
|
-
3. `when_command(cmd)` — runs command, captures emitted events.
|
|
102
|
-
4. `then_emitted(EventClass, **attrs)` / `then_emitted_nothing_else`.
|
|
103
|
-
5. `Acta.test_mode { ... }` — inline reactors for the block.
|
|
104
|
-
6. Replay determinism helper —
|
|
105
|
-
`expect_projections_deterministic { ... }`.
|
|
106
|
-
|
|
107
|
-
### M8 — `Acta::Serializable` (AR piggyback) ✅
|
|
108
|
-
|
|
109
|
-
1. Concern adding `to_acta_hash` / `self.from_acta_hash(hash)` on AR classes.
|
|
110
|
-
2. `acta_serialize only: / except:` configuration.
|
|
111
|
-
3. Type dispatch for AR classes in event attributes.
|
|
112
|
-
4. Arrays of AR (`array_of:`) support.
|
|
113
|
-
5. Nested AR-in-AR round-trip.
|
|
114
|
-
6. STI support via `type` column capture.
|
|
115
|
-
7. Schema-drift tolerance — filter unknown keys on deserialize.
|
|
116
|
-
|
|
117
|
-
### M9 — Postgres adapter ✅
|
|
118
|
-
|
|
119
|
-
1. `Acta::Adapters::Postgres` implementation.
|
|
120
|
-
2. Advisory locks (`pg_advisory_xact_lock(hashtext(...))`) per stream.
|
|
121
|
-
3. `jsonb` column type + `uuid` column type + `gen_random_uuid()`.
|
|
122
|
-
4. Shared behaviour specs — `it_behaves_like "an Acta adapter"`.
|
|
123
|
-
5. CI matrix with both SQLite and Postgres.
|
|
124
|
-
6. Concurrency-specific specs exercising genuine concurrent writers.
|
|
125
|
-
|
|
126
|
-
### M10 — v1.0 polish ✅
|
|
127
|
-
|
|
128
|
-
1. Observability via `ActiveSupport::Notifications` —
|
|
129
|
-
`acta.event_emitted`, `acta.projection_applied`, `acta.reactor_enqueued`.
|
|
130
|
-
2. Remaining error leaves — `UnknownEventType`, `ReplayError`, gaps.
|
|
131
|
-
3. README rewrite with full worked examples.
|
|
132
|
-
4. Tag v1.0.0.
|
|
133
|
-
|
|
134
|
-
## Milestone dependencies
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
M0 → M1 → M2 → M3 → M4 ─┐
|
|
138
|
-
└──→ M5 ─┐
|
|
139
|
-
M6 ──────┴─→ M7 → M8 → M9 → M10
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
M4 and M5 are independent after M3. M6 can start after M2. M8 benefits from
|
|
143
|
-
M7. M9 can start anytime after M1 but is most valuable after M8.
|
|
144
|
-
|
|
145
|
-
## Out of scope for v1
|
|
146
|
-
|
|
147
|
-
- Upcasters (column reserved)
|
|
148
|
-
- Multi-store (latent concept, not exposed)
|
|
149
|
-
- MySQL adapter
|
|
150
|
-
- `Acta::Saga` / process managers
|
|
151
|
-
- LISTEN/NOTIFY or other pub/sub transport
|
|
152
|
-
- Snapshots
|
|
153
|
-
|
|
154
|
-
## Quality gates per commit
|
|
155
|
-
|
|
156
|
-
- `bundle exec rspec` green
|
|
157
|
-
- `bundle exec rubocop` clean
|
|
158
|
-
- Change has a spec unless it's pure refactoring
|
data/lib/acta/array_type.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_model/type"
|
|
4
|
-
|
|
5
|
-
module Acta
|
|
6
|
-
class ArrayType < ActiveModel::Type::Value
|
|
7
|
-
def initialize(element_type)
|
|
8
|
-
super()
|
|
9
|
-
@element_type = element_type
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def cast(value)
|
|
13
|
-
return nil if value.nil?
|
|
14
|
-
|
|
15
|
-
Array(value).map { |el| @element_type.cast(el) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def serialize(value)
|
|
19
|
-
return nil if value.nil?
|
|
20
|
-
|
|
21
|
-
value.map { |el| @element_type.serialize(el) }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def deserialize(value)
|
|
25
|
-
return nil if value.nil?
|
|
26
|
-
|
|
27
|
-
value.map { |el| @element_type.deserialize(el) }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/acta/model_type.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_model/type"
|
|
4
|
-
|
|
5
|
-
module Acta
|
|
6
|
-
class ModelType < ActiveModel::Type::Value
|
|
7
|
-
def initialize(wrapped_class)
|
|
8
|
-
super()
|
|
9
|
-
@wrapped_class = wrapped_class
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def cast(value)
|
|
13
|
-
case value
|
|
14
|
-
when nil then nil
|
|
15
|
-
when @wrapped_class then value
|
|
16
|
-
when Hash then @wrapped_class.from_acta_hash(value)
|
|
17
|
-
else
|
|
18
|
-
raise ArgumentError, "Cannot cast #{value.class} (#{value.inspect}) to #{@wrapped_class}"
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def serialize(value)
|
|
23
|
-
return nil if value.nil?
|
|
24
|
-
|
|
25
|
-
value.to_acta_hash
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def deserialize(value)
|
|
29
|
-
cast(value)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|