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.
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.2.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: 1980-01-02 00:00:00.000000000 Z
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: '8.1'
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: '8.1'
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: '8.1'
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: '8.1'
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: '8.1'
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: '8.1'
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: '8.1'
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: '8.1'
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.4'
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.7.2
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
@@ -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
@@ -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