ecs_on_rails 0.2.1
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 +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/lib/ecs_on_rails.rb +10 -0
- data/lib/ecs_rails/component.rb +68 -0
- data/lib/ecs_rails/config.rb +31 -0
- data/lib/ecs_rails/dsl.rb +516 -0
- data/lib/ecs_rails/entity.rb +233 -0
- data/lib/ecs_rails/errors.rb +28 -0
- data/lib/ecs_rails/lazy.rb +244 -0
- data/lib/ecs_rails/preloading.rb +84 -0
- data/lib/ecs_rails/presence.rb +125 -0
- data/lib/ecs_rails/querying.rb +102 -0
- data/lib/ecs_rails/railtie.rb +14 -0
- data/lib/ecs_rails/registry.rb +152 -0
- data/lib/ecs_rails/relationships.rb +316 -0
- data/lib/ecs_rails/validations.rb +150 -0
- data/lib/ecs_rails/version.rb +5 -0
- data/lib/ecs_rails.rb +47 -0
- data/lib/generators/ecs_rails/component/component_generator.rb +101 -0
- data/lib/generators/ecs_rails/component/templates/component_spec.rb.tt +16 -0
- data/lib/generators/ecs_rails/component/templates/migration.rb.tt +25 -0
- data/lib/generators/ecs_rails/component/templates/model.rb.tt +13 -0
- data/lib/generators/ecs_rails/install/install_generator.rb +83 -0
- data/lib/generators/ecs_rails/install/templates/application_component.rb.tt +16 -0
- data/lib/generators/ecs_rails/install/templates/application_entity.rb.tt +15 -0
- data/lib/generators/ecs_rails/install/templates/initializer.rb.tt +19 -0
- data/lib/generators/ecs_rails/install/templates/migration.rb.tt +20 -0
- data/lib/generators/ecs_rails/relationship/relationship_generator.rb +94 -0
- data/lib/generators/ecs_rails/relationship/templates/migration.rb.tt +32 -0
- metadata +138 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bef233f6d1933696953db3058563f4e4178c67aeb6d56f6d2f241f64f23b81f3
|
|
4
|
+
data.tar.gz: a7806e71e299991ae122dd30ce78a009bf181ed55c11d8b43444be2a2f6ef1d8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7fd3624f55f3264bbaf3c84cb355a63cc5e0e5e66eabd43d04ad01c1d87bff202dd8d4e851d6006c66c383c742702da548115161ab33dd2ddc7d1f37b0e86295
|
|
7
|
+
data.tar.gz: 926baa1d0cfca36f1453d0cc4abb02d463a04d49b499b2a6eb72f54534a4486ccfe27e1452a6b0a2206f2589062cd4f53aa1b05ab07fed7b4479e19515b92ecd
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.1] — 2026-07-23
|
|
10
|
+
|
|
11
|
+
First RubyGems release.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The gem is published as **`ecs_on_rails`**. Both `ecs-rails` and `ecs_rails`
|
|
16
|
+
are unavailable: an unrelated `ecs-rails` gem already exists, and RubyGems
|
|
17
|
+
treats `-`, `_` and case as equivalent when comparing names, so every spelling
|
|
18
|
+
of "ecs rails" collides with it.
|
|
19
|
+
- **No API change.** The require path is still `ecs_rails`, the module is still
|
|
20
|
+
`EcsRails`, and the generators are still `ecs_rails:install`,
|
|
21
|
+
`ecs_rails:component` and `ecs_rails:relationship`. Only the name you put in
|
|
22
|
+
your Gemfile differs:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "ecs_on_rails" # Gemfile
|
|
26
|
+
require "ecs_rails" # everywhere else
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A `lib/ecs_on_rails.rb` shim requires `ecs_rails`, so a bare `gem
|
|
30
|
+
"ecs_on_rails"` works under `Bundler.require`.
|
|
31
|
+
|
|
32
|
+
## [0.2.0] — 2026-07-22
|
|
33
|
+
|
|
34
|
+
Adds cross-entity relationships. Demo-validated by the bulletin-board app.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Relationship query & preload sugar (RFC-0013). `Entity.with_related(:author, user)` /
|
|
39
|
+
`without_related(:author)` / `includes_related(:author)` query and preload a
|
|
40
|
+
relationship by its declared name, so the backing component class never appears in
|
|
41
|
+
application code. Thin sugar over the component verbs.
|
|
42
|
+
- Relationship DSL (RFC-0012). `relates_to :author, User` on an entity declares a
|
|
43
|
+
cross-entity link with no relationship component file — the DSL defines the
|
|
44
|
+
backing component dynamically. `post.author` / `post.author =` reach the target;
|
|
45
|
+
`rails g ecs_rails:relationship Post author:User` emits the migration. Deleting
|
|
46
|
+
the target nullifies the link; deleting the owner cascades.
|
|
47
|
+
|
|
48
|
+
## [0.1.0] — 2026-07-20
|
|
49
|
+
|
|
50
|
+
Feature-complete, demo-validated. Not yet published to RubyGems. See the
|
|
51
|
+
[v0.1 retrospective](../docs/retrospective-v0.1.md).
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Component preloading (RFC-0011). `Entity.includes_components(*Components)` batches
|
|
56
|
+
component loads (all declared, or a named subset) so a list view issues a bounded
|
|
57
|
+
number of queries instead of one per component per row. A thin wrapper over
|
|
58
|
+
ActiveRecord's native `preload`, which already works with lazy components.
|
|
59
|
+
- Component query DSL (RFC-0010). `Entity.with_component(Component, **conditions)` /
|
|
60
|
+
`Entity.without_component(Component)` query entities by which components they
|
|
61
|
+
have, compiling to correlated `EXISTS` / `NOT EXISTS` subqueries that apply the
|
|
62
|
+
entity-model scope automatically (a shared component can't leak across entity
|
|
63
|
+
types). Chainable with ordinary ActiveRecord. Avoids `.with` (AR's CTEs).
|
|
64
|
+
- Component presence (RFC-0009). `entity.add(Component)` / `entity.has?(Component)` /
|
|
65
|
+
`entity.remove(Component)` and a generated `entity.<component>?` predicate, so
|
|
66
|
+
marker components (Moderator, Administrator) — which carry no state and so are
|
|
67
|
+
never persisted by the lazy save cascade — work.
|
|
68
|
+
- Validation error merging (RFC-0007). `entity.valid?` reflects its touched
|
|
69
|
+
components' validity; component errors merge under `entity.errors[:"email.address"]`
|
|
70
|
+
and read naturally in a form. A non-dirty virtual component is not validated.
|
|
71
|
+
- Method delegation (RFC-0005). Component methods and attribute accessors are
|
|
72
|
+
callable on the entity — `user.send_welcome_email`, `user.address = "x"`.
|
|
73
|
+
Name clashes between two components raise `DelegationConflict` at load time;
|
|
74
|
+
`except:`/`only:` are the escape hatch.
|
|
75
|
+
- Lazy / virtual components (RFC-0006). `entity.email` always returns an
|
|
76
|
+
`Email`, never `nil` — a missing row yields an in-memory component with every
|
|
77
|
+
attribute at its database default. `entity.save` cascades to the components
|
|
78
|
+
you touched, inserting a row only for those that are dirty, in one
|
|
79
|
+
transaction. Reading a component costs a `SELECT` and nothing else.
|
|
80
|
+
- The `component` DSL (RFC-0004). `component Name` on an entity declares what it
|
|
81
|
+
is composed from, generates the reader, and wires the `has_one`.
|
|
82
|
+
- `ApplicationComponent` (RFC-0003) and entity subclass resolution — a loaded
|
|
83
|
+
entity comes back as its real subclass (`User`, not `ApplicationEntity`).
|
|
84
|
+
- `ApplicationEntity` (RFC-0001) — immutable identity rows in one shared
|
|
85
|
+
`entities` table, discriminated by `model`.
|
|
86
|
+
- The component registry (RFC-0002), reload-safe by keying on class name.
|
|
87
|
+
- `ecs_rails:install` and `ecs_rails:component` generators (RFC-0008).
|
|
88
|
+
- Gem scaffold, MIT licence, and RSpec + PostgreSQL test harness.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Hutchens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ECS Rails
|
|
2
|
+
|
|
3
|
+
An Entity–Component–System reimagining of ActiveRecord that stays idiomatic to
|
|
4
|
+
Ruby on Rails.
|
|
5
|
+
|
|
6
|
+
> The full API below is implemented and tested (534 examples on real
|
|
7
|
+
> PostgreSQL). A companion bulletin-board app is built entirely on it and runs
|
|
8
|
+
> live at **[ecs-rails.kranzky.com](https://ecs-rails.kranzky.com)**. See the
|
|
9
|
+
> [v0.1 retrospective](https://github.com/kranzky/ecs_rails/blob/main/docs/retrospective-v0.1.md)
|
|
10
|
+
> for the full story of how it was designed.
|
|
11
|
+
|
|
12
|
+
## The idea
|
|
13
|
+
|
|
14
|
+
Replace one-table-per-model with one-table-per-component. An entity is a
|
|
15
|
+
lightweight identity row; all state and behaviour live in small, reusable
|
|
16
|
+
components that are composed onto it.
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
class User < ApplicationEntity
|
|
20
|
+
component Name
|
|
21
|
+
component Email
|
|
22
|
+
component Avatar
|
|
23
|
+
component Moderator # a marker: no data, presence is the meaning
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Email < ApplicationComponent
|
|
27
|
+
validates :address, presence: true
|
|
28
|
+
|
|
29
|
+
def send_welcome_email
|
|
30
|
+
# self is the Email, never the User
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
user = User.create! # one row in `entities`, no component rows
|
|
37
|
+
user.email # => #<Email> — virtual, not persisted
|
|
38
|
+
user.email.address = "a@b.com"
|
|
39
|
+
user.save! # now `emails` gets a row
|
|
40
|
+
|
|
41
|
+
user.send_welcome_email # delegated to the Email component
|
|
42
|
+
user.errors[:"email.address"] # component errors merge onto the entity
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every v0.1 capability, working today:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# Lazy components — no row until a value differs from its default.
|
|
49
|
+
user.avatar.persisted? # => false, costs no INSERT
|
|
50
|
+
|
|
51
|
+
# Presence / markers — a user IS a moderator when the row exists.
|
|
52
|
+
user.add(Moderator); user.moderator? # => true
|
|
53
|
+
user.remove(Moderator)
|
|
54
|
+
|
|
55
|
+
# Query by composition — avoids AR's .with (CTEs); scopes to the entity model.
|
|
56
|
+
Post.with_component(PublishState, state: "published")
|
|
57
|
+
User.without_component(Avatar)
|
|
58
|
+
|
|
59
|
+
# Preload to bound the query count on a list view.
|
|
60
|
+
Post.with_component(PublishState).includes_components(Title, Body, Likes)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Components are shared by *type*, so `Likes` behaves identically on a `Post` and
|
|
64
|
+
a `Comment` — reuse without STI and without polymorphic associations.
|
|
65
|
+
|
|
66
|
+
## Getting started
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Gemfile — note the packaging name differs from the require path (see Names)
|
|
70
|
+
gem "ecs_on_rails"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
bundle install
|
|
75
|
+
rails g ecs_rails:install
|
|
76
|
+
rails g ecs_rails:component Email address:string verified:boolean
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Entities go in `app/entities`, components in `app/entities/components`
|
|
80
|
+
([configurable](https://github.com/kranzky/ecs_rails/blob/main/docs/adr/0010-entity-component-directory-layout.md)); the
|
|
81
|
+
install generator wires the autoloading.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- **[Architecture](https://github.com/kranzky/ecs_rails/blob/main/docs/architecture.md)** — the invariants. Start here.
|
|
86
|
+
- **[v0.1 retrospective](https://github.com/kranzky/ecs_rails/blob/main/docs/retrospective-v0.1.md)** — what was built, what
|
|
87
|
+
the demo found, what's next.
|
|
88
|
+
- **[ADRs](https://github.com/kranzky/ecs_rails/tree/main/docs/adr)** — why the design is the way it is (14 decisions,
|
|
89
|
+
several amended by their own demo).
|
|
90
|
+
- **[RFCs](https://github.com/kranzky/ecs_rails/tree/main/docs/rfc)** — the 13 features, each one commit.
|
|
91
|
+
- **[Backlog](https://github.com/kranzky/ecs_rails/blob/main/docs/backlog.md)** — what deliberately isn't built yet.
|
|
92
|
+
- **[Friction log](https://github.com/kranzky/ecs_rails/blob/main/docs/friction-log.md)** — the demo's running verdict on
|
|
93
|
+
the API.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
Requires Ruby >= 3.2 and a running PostgreSQL.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
createdb ecs_rails_test
|
|
101
|
+
bundle install
|
|
102
|
+
bundle exec rspec
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Set `DATABASE_URL` to point the suite at a different database.
|
|
106
|
+
|
|
107
|
+
## Names
|
|
108
|
+
|
|
109
|
+
`ecs_rails` everywhere except the Gemfile — see
|
|
110
|
+
[ADR-0007](https://github.com/kranzky/ecs_rails/blob/main/docs/adr/0007-monorepo-and-licensing.md#three-different-names).
|
|
111
|
+
|
|
112
|
+
| | |
|
|
113
|
+
|---|---|
|
|
114
|
+
| GitHub repo | [`ecs_rails`](https://github.com/kranzky/ecs_rails) |
|
|
115
|
+
| RubyGems gem | `ecs_on_rails` |
|
|
116
|
+
| Ruby module | `EcsRails` |
|
|
117
|
+
| `require` | `ecs_rails` |
|
|
118
|
+
| Generators | `ecs_rails:install`, `:component`, `:relationship` |
|
|
119
|
+
|
|
120
|
+
Only the published gem name differs. RubyGems collapses `-`, `_` and case when
|
|
121
|
+
comparing names, so `ecs-rails`, `ecs_rails` and `ecsrails` are one name — and
|
|
122
|
+
it belongs to an unrelated, still-maintained gem. `ecs_on_rails` keeps the
|
|
123
|
+
`rails` keyword without the `rails-` prefix that convention reserves for Rails
|
|
124
|
+
Core Team gems.
|
|
125
|
+
|
|
126
|
+
## Licence
|
|
127
|
+
|
|
128
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/lib/ecs_on_rails.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The gem is published as "ecs_on_rails", but its canonical require path and
|
|
4
|
+
# module are "ecs_rails" / EcsRails. Bundler requires a gem by its own name, so
|
|
5
|
+
# `Bundler.require` (i.e. a bare `gem "ecs_on_rails"` in a Gemfile) attempts
|
|
6
|
+
# `require "ecs_on_rails"` and would raise LoadError without this file.
|
|
7
|
+
#
|
|
8
|
+
# Requiring "ecs_rails" directly is equivalent and is what the documentation
|
|
9
|
+
# uses throughout.
|
|
10
|
+
require "ecs_rails"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EcsRails
|
|
4
|
+
# An ordinary ActiveRecord model that belongs to exactly one entity.
|
|
5
|
+
#
|
|
6
|
+
# Implements RFC-0003. See docs/architecture.md §1 for the invariants.
|
|
7
|
+
#
|
|
8
|
+
# Host apps subclass this once, as ApplicationComponent, then subclass that
|
|
9
|
+
# per component type:
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationComponent < EcsRails::Component
|
|
12
|
+
# self.abstract_class = true
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# class Email < ApplicationComponent
|
|
16
|
+
# validates :address, presence: true
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Email.where(verified: false) # queried directly; no join to entities
|
|
20
|
+
# Email.first.entity # => #<User> — the real subclass (ADR-0008)
|
|
21
|
+
#
|
|
22
|
+
# A component owns its own table and knows nothing about entity subclasses.
|
|
23
|
+
# Scopes, validations, callbacks and associations all work as normal: this is
|
|
24
|
+
# a plain AR model that happens to hang off an entity.
|
|
25
|
+
#
|
|
26
|
+
# The one sanctioned exception to "knows nothing about entity subclasses" is a
|
|
27
|
+
# relationship component's `class_name:` — see ADR-0006.
|
|
28
|
+
class Component < ActiveRecord::Base
|
|
29
|
+
self.abstract_class = true
|
|
30
|
+
|
|
31
|
+
# Lazy / virtual components (RFC-0006), from the component's side: what
|
|
32
|
+
# "dirty" means — the question that decides whether this component is worth
|
|
33
|
+
# a row — and the after_destroy that resets its entity's reader back to a
|
|
34
|
+
# virtual instance (architecture.md §3).
|
|
35
|
+
include Lazy::Component
|
|
36
|
+
|
|
37
|
+
# Every component belongs to exactly one entity (architecture.md §1).
|
|
38
|
+
#
|
|
39
|
+
# The association targets the abstract ApplicationEntity, which has no table
|
|
40
|
+
# of its own — but EcsRails::Entity sets table_name to "entities", so the
|
|
41
|
+
# generated query is an ordinary select against that table. The loaded row's
|
|
42
|
+
# `model` column then decides which subclass to instantiate, via
|
|
43
|
+
# Entity.discriminate_class_for_record. That is what makes `email.entity`
|
|
44
|
+
# return a User rather than an ApplicationEntity (RFC-0003, ADR-0008).
|
|
45
|
+
#
|
|
46
|
+
# `optional: false` is explicit rather than inherited from
|
|
47
|
+
# `belongs_to_required_by_default`. That config is applied by the Rails
|
|
48
|
+
# railtie via load_defaults, so its value depends on the host app's
|
|
49
|
+
# configured defaults and is not visible in bare ActiveRecord. RFC-0003
|
|
50
|
+
# requires entity_id to be required unconditionally, so we state it here
|
|
51
|
+
# rather than inherit a host's setting. (Same reasoning as RFC-0001's
|
|
52
|
+
# readonly guard in entity.rb.)
|
|
53
|
+
#
|
|
54
|
+
# Deliberately no `dependent:` option. Cascade is owned by the database:
|
|
55
|
+
# every component table has an ON DELETE CASCADE foreign key to
|
|
56
|
+
# entities(id), so entity.destroy removes the component rows
|
|
57
|
+
# (architecture.md §3). Declaring `dependent: :destroy` on this side would
|
|
58
|
+
# invert the ownership — destroying a component would destroy its entity,
|
|
59
|
+
# and with it every sibling component. That is the wrong direction and
|
|
60
|
+
# destructive; the DB layer already has the right one.
|
|
61
|
+
belongs_to :entity, class_name: "ApplicationEntity", optional: false
|
|
62
|
+
|
|
63
|
+
# The unique index on entity_id (ADR-0005) is the real enforcement, and a
|
|
64
|
+
# uniqueness validation here would cost a SELECT on every save while still
|
|
65
|
+
# racing. RFC-0003 asserts RecordNotUnique from the database, so the guard
|
|
66
|
+
# is left where it is enforced.
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EcsRails
|
|
4
|
+
# Generator configuration for the directory layout (ADR-0010).
|
|
5
|
+
#
|
|
6
|
+
# The gem RUNTIME never reads this: entities and components are keyed by class
|
|
7
|
+
# name (RFC-0002) and by the `model` discriminator (ADR-0002), so the registry
|
|
8
|
+
# does not care where a class file lives. This object exists purely so the
|
|
9
|
+
# generators (RFC-0008) know where to write, and so the initializer they emit
|
|
10
|
+
# can echo the chosen path.
|
|
11
|
+
#
|
|
12
|
+
# `entities_path` is the single knob. Components always live in a `components`
|
|
13
|
+
# subdirectory of it, which is why `components_path` is derived rather than
|
|
14
|
+
# separately settable — ADR-0010 deliberately exposes one path, not two.
|
|
15
|
+
class Config
|
|
16
|
+
# Entities land here; the default is the ADR-0010 layout. Set it to
|
|
17
|
+
# "app/models" to restore the pre-ADR-0010 single-directory layout.
|
|
18
|
+
attr_accessor :entities_path
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@entities_path = "app/entities"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Components live in a `components` subdirectory of the entities path. The
|
|
25
|
+
# generated initializer collapses this directory so Zeitwerk treats the
|
|
26
|
+
# `components/` segment as transparent (ADR-0010 "How it works").
|
|
27
|
+
def components_path
|
|
28
|
+
"#{entities_path}/components"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|