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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EcsRails
|
|
4
|
+
# Component presence as a first-class operation: add / has? / remove.
|
|
5
|
+
#
|
|
6
|
+
# Implements RFC-0009, decided by ADR-0009. Surfaced by the demo: a marker
|
|
7
|
+
# component (Moderator, Administrator) carries no state, so it is never
|
|
8
|
+
# `ecs_dirty?` (ADR-0003 excludes entity_id as identity), and RFC-0006's save
|
|
9
|
+
# cascade therefore never writes it. The natural path does nothing at all:
|
|
10
|
+
#
|
|
11
|
+
# user.moderator # a virtual Moderator
|
|
12
|
+
# user.save! # writes no moderators row — a marker is never dirty
|
|
13
|
+
# user.moderator.persisted? # => false, forever
|
|
14
|
+
#
|
|
15
|
+
# Presence cannot be inferred from lazy state (reading must stay free, RFC-0006)
|
|
16
|
+
# and a marker has nothing to dirty, so presence is its own verb:
|
|
17
|
+
#
|
|
18
|
+
# user.add(Moderator) # persist the row now (idempotent, validated)
|
|
19
|
+
# user.has?(Moderator) # => true — a row exists
|
|
20
|
+
# user.moderator? # => the same question, per-component sugar (DSL)
|
|
21
|
+
# user.remove(Moderator) # destroy the row (idempotent)
|
|
22
|
+
#
|
|
23
|
+
# These are useful for *every* component — `has?(Email)`, `remove(Avatar)` —
|
|
24
|
+
# not just empty ones; a marker is simply the case where presence is the only
|
|
25
|
+
# state (ADR-0009). Mirrors Flecs' add/has/remove vocabulary.
|
|
26
|
+
#
|
|
27
|
+
# This module is the entity side. The `<reader>?` predicate is generated per
|
|
28
|
+
# component by the DSL, into generated_component_methods (see
|
|
29
|
+
# EcsRails::DSL#define_component_predicate); it is just `has?(ThatComponent)`.
|
|
30
|
+
module Presence
|
|
31
|
+
module Entity
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
# Ensures a row exists for `component_class` on this entity and returns the
|
|
35
|
+
# now-persisted instance (RFC-0009).
|
|
36
|
+
#
|
|
37
|
+
# Immediate, not deferred to the next save: the whole reason `add` exists
|
|
38
|
+
# is that the save cascade never persists a marker. Idempotent: an existing
|
|
39
|
+
# row is returned untouched, never a second INSERT — the unique entity_id
|
|
40
|
+
# index (ADR-0005) would forbid one, and correctness should not depend on
|
|
41
|
+
# catching that. Validated: it uses `save!`, so `add(Email)` raises
|
|
42
|
+
# RecordInvalid (address is required) while `add(Moderator)` always
|
|
43
|
+
# succeeds. You cannot add an empty required component (ADR-0009).
|
|
44
|
+
def add(component_class)
|
|
45
|
+
name = ecs_presence_reader(component_class)
|
|
46
|
+
|
|
47
|
+
# The reader goes through RFC-0006's memo. When no row exists on this
|
|
48
|
+
# instance it loads one (a SELECT) or builds a virtual; when a row does
|
|
49
|
+
# exist — including one this instance already added — it is already the
|
|
50
|
+
# persisted instance, so we return it and no second INSERT is attempted.
|
|
51
|
+
component = public_send(name)
|
|
52
|
+
return component if component.persisted?
|
|
53
|
+
|
|
54
|
+
# A virtual instance: persist it. entity_id is restamped for the same
|
|
55
|
+
# reason the cascade does (RFC-0006) — on a freshly built virtual for a
|
|
56
|
+
# new entity it may not have been set yet. The memo now holds this same,
|
|
57
|
+
# persisted instance, so a later `entity.moderator` answers from it with
|
|
58
|
+
# no further query.
|
|
59
|
+
component.entity_id = id
|
|
60
|
+
component.save!
|
|
61
|
+
component
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# True iff a row exists for `component_class` on this entity (RFC-0009).
|
|
65
|
+
#
|
|
66
|
+
# Side-effect-free, like `valid?` (RFC-0007): it never materialises,
|
|
67
|
+
# persists, or dirties anything. It consults RFC-0006's memo first — a
|
|
68
|
+
# component dirtied-and-saved on this instance is present without a fresh
|
|
69
|
+
# query — and otherwise issues a bare existence check (SELECT ... EXISTS),
|
|
70
|
+
# a load of nothing.
|
|
71
|
+
def has?(component_class)
|
|
72
|
+
name = ecs_presence_reader(component_class)
|
|
73
|
+
|
|
74
|
+
# Memo first: if this instance already loaded (and persisted) the
|
|
75
|
+
# component, the answer is known without touching the database. A merely
|
|
76
|
+
# virtual memo entry is *not* proof of absence — a row could have been
|
|
77
|
+
# written elsewhere — so it falls through to the existence check rather
|
|
78
|
+
# than answering false from memory.
|
|
79
|
+
cached = @ecs_components&.[](name)
|
|
80
|
+
return true if cached&.persisted?
|
|
81
|
+
|
|
82
|
+
# Bare existence check: no row is instantiated, so nothing is loaded and
|
|
83
|
+
# nothing is dirtied. One query per component, as everywhere else in the
|
|
84
|
+
# gem (architecture.md §7 non-goal: query optimisation).
|
|
85
|
+
component_class.where(entity_id: id).exists?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Destroys the row for `component_class` if present, and resets the reader
|
|
89
|
+
# to a virtual default instance — exactly RFC-0006's `component.destroy`
|
|
90
|
+
# semantics. Idempotent when absent. Returns the entity (RFC-0009).
|
|
91
|
+
def remove(component_class)
|
|
92
|
+
name = ecs_presence_reader(component_class)
|
|
93
|
+
|
|
94
|
+
# Reach the component through the reader so a present row is loaded with
|
|
95
|
+
# its callbacks intact. `component.destroy` fires Lazy::Component's
|
|
96
|
+
# after_destroy, which calls #ecs_forget_component here — dropping the
|
|
97
|
+
# memo and nilling the association, so the next read rebuilds a virtual.
|
|
98
|
+
# When nothing is there to destroy the read leaves a virtual in the memo,
|
|
99
|
+
# which is already the reset state, so remove is idempotent.
|
|
100
|
+
component = public_send(name)
|
|
101
|
+
component.destroy if component.persisted?
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Resolves a declared component class to its reader name, or raises.
|
|
108
|
+
#
|
|
109
|
+
# `add`/`has?`/`remove` accept only a component the entity actually
|
|
110
|
+
# declares (RFC-0009); anything else — an undeclared component, or a class
|
|
111
|
+
# that is not a component at all — is InvalidComponent, checked before any
|
|
112
|
+
# database work. The declared set already accounts for inheritance
|
|
113
|
+
# (DSL#components walks the ancestry), so a subclass sees its parents'
|
|
114
|
+
# components too.
|
|
115
|
+
def ecs_presence_reader(component_class)
|
|
116
|
+
unless self.class.components.include?(component_class)
|
|
117
|
+
raise InvalidComponent,
|
|
118
|
+
"#{component_class.inspect} is not a component of #{self.class.name}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
component_class.model_name.singular.to_sym
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EcsRails
|
|
4
|
+
# The class/relation-level query DSL: filter entities by which components they
|
|
5
|
+
# have.
|
|
6
|
+
#
|
|
7
|
+
# Implements RFC-0010, decided by ADR-0011. Surfaced by the demo, where every
|
|
8
|
+
# list view hand-rolled a cross-component subquery whose correctness silently
|
|
9
|
+
# rode the entity's default_scope.
|
|
10
|
+
#
|
|
11
|
+
# Post.with_component(PublishState, state: "published") # posts that HAVE a
|
|
12
|
+
# # matching row
|
|
13
|
+
# User.without_component(Avatar) # users with NO avatar
|
|
14
|
+
# Post.with_component(Name).with_component(Avatar) # AND — chainable
|
|
15
|
+
#
|
|
16
|
+
# Extended into EcsRails::Entity, so these are class methods on every entity
|
|
17
|
+
# class. ActiveRecord delegates class methods to relations (via `scoping`), so
|
|
18
|
+
# `Post.where(...).with_component(...)` chains too: the method runs with `all`
|
|
19
|
+
# returning the current relation, which already carries the entity-model scope.
|
|
20
|
+
#
|
|
21
|
+
# ## Why EXISTS, not JOIN (ADR-0011)
|
|
22
|
+
#
|
|
23
|
+
# Each call compiles to a correlated `EXISTS` / `NOT EXISTS` subquery rather
|
|
24
|
+
# than a join: EXISTS matches an entity once (no duplicate rows), N calls are N
|
|
25
|
+
# independent `AND EXISTS` clauses that compose without table aliasing, and
|
|
26
|
+
# `NOT EXISTS` is the natural, NULL-safe form of "without" (unlike `NOT IN` or a
|
|
27
|
+
# `LEFT JOIN ... IS NULL`).
|
|
28
|
+
#
|
|
29
|
+
# ## Why the entity-model scope is correct for free (ADR-0011)
|
|
30
|
+
#
|
|
31
|
+
# A component table is blind to entity type — `Name` has rows for Users *and*
|
|
32
|
+
# Posts. The scope that keeps `Post.with_component(Name)` from returning Users
|
|
33
|
+
# is NOT added by this DSL. It falls out of the method running on `all`, which
|
|
34
|
+
# is Post's own default-scoped relation (`model = 'posts'`, ADR-0002). The DSL
|
|
35
|
+
# only appends the `EXISTS` clause. Building from `unscoped` or from
|
|
36
|
+
# `ApplicationEntity` would drop that scope and leak — so we build from `all`.
|
|
37
|
+
module Querying
|
|
38
|
+
# Entities that HAVE a row for `component_class` (RFC-0010). With
|
|
39
|
+
# `conditions`, the row must also match them (hash equality, like `where`).
|
|
40
|
+
#
|
|
41
|
+
# Returns an ActiveRecord::Relation, chainable with `where`, `order`, `limit`,
|
|
42
|
+
# and further `with_component` / `without_component` calls (which AND).
|
|
43
|
+
def with_component(component_class, **conditions)
|
|
44
|
+
all.where(ecs_component_exists_sql(component_class, conditions, negate: false))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Entities that have NO row for `component_class` (RFC-0010). No conditions
|
|
48
|
+
# form — "without a *matching* row" is ambiguous and unneeded (see the RFC's
|
|
49
|
+
# Non-goals). A virtual/lazy component has no row, so it counts as absent,
|
|
50
|
+
# which is the intuitive reading of "without" (ADR-0009).
|
|
51
|
+
def without_component(component_class)
|
|
52
|
+
all.where(ecs_component_exists_sql(component_class, {}, negate: true))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Builds the `EXISTS (...)` / `NOT EXISTS (...)` fragment for one component.
|
|
58
|
+
#
|
|
59
|
+
# The subquery is built from `component_class.where(conditions)` so that
|
|
60
|
+
# ActiveRecord sanitises the condition values — they are treated as data,
|
|
61
|
+
# never SQL, closing the injection hole (ADR-0011). The correlation is an
|
|
62
|
+
# Arel column comparison, `component.entity_id = entities.id`, so it renders
|
|
63
|
+
# as properly quoted identifiers against the OUTER entities table — never a
|
|
64
|
+
# hand-built string.
|
|
65
|
+
#
|
|
66
|
+
# `arel_table` here is the entity class's own table (`entities`); the outer
|
|
67
|
+
# query the fragment is appended to selects from that same table, so `id`
|
|
68
|
+
# correlates to each candidate entity row.
|
|
69
|
+
def ecs_component_exists_sql(component_class, conditions, negate:)
|
|
70
|
+
ecs_validate_queryable_component!(component_class)
|
|
71
|
+
|
|
72
|
+
subquery = component_class
|
|
73
|
+
.where(conditions)
|
|
74
|
+
.where(component_class.arel_table[:entity_id].eq(arel_table[primary_key]))
|
|
75
|
+
.select("1")
|
|
76
|
+
|
|
77
|
+
keyword = negate ? "NOT EXISTS" : "EXISTS"
|
|
78
|
+
"#{keyword} (#{subquery.to_sql})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A queryable component is any concrete EcsRails::Component. Unlike the
|
|
82
|
+
# presence API (RFC-0009), it need NOT be declared on this entity: querying
|
|
83
|
+
# `Post.with_component(Avatar)` when Post has no Avatar is a valid,
|
|
84
|
+
# always-empty query, not an error (RFC-0010) — and keeps the DSL from
|
|
85
|
+
# needing the registry. Anything that is not a concrete component raises
|
|
86
|
+
# InvalidComponent, before any database work.
|
|
87
|
+
def ecs_validate_queryable_component!(component_class)
|
|
88
|
+
unless component_class.is_a?(Class) && component_class < EcsRails::Component
|
|
89
|
+
raise InvalidComponent,
|
|
90
|
+
"#{component_class.inspect} is not an EcsRails::Component subclass"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# An abstract component owns no table (architecture.md §1), so its EXISTS
|
|
94
|
+
# subquery could never resolve. Fail here rather than at the database.
|
|
95
|
+
return unless component_class.abstract_class?
|
|
96
|
+
|
|
97
|
+
raise InvalidComponent,
|
|
98
|
+
"#{component_class.name} is abstract and owns no table; " \
|
|
99
|
+
"query a concrete component"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module EcsRails
|
|
6
|
+
# Hooks the gem into a host Rails application.
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
# The registry keys entries by class name and resolves lazily, so it
|
|
9
|
+
# survives development-mode reloading. See RFC-0002.
|
|
10
|
+
config.to_prepare do
|
|
11
|
+
EcsRails.registry.clear! if EcsRails.registry.respond_to?(:clear!)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Required explicitly rather than relying on ActiveRecord having pulled them in:
|
|
4
|
+
# #constantize is load-bearing for reload safety.
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
require "active_support/core_ext/string/filters"
|
|
7
|
+
|
|
8
|
+
module EcsRails
|
|
9
|
+
# Records which components each entity declares. See RFC-0002.
|
|
10
|
+
#
|
|
11
|
+
# The registry is a process-wide singleton (`EcsRails.registry`) populated by the
|
|
12
|
+
# `component` DSL (RFC-0004) at class-load time, and read by generators,
|
|
13
|
+
# delegation and — later — systems.
|
|
14
|
+
#
|
|
15
|
+
# Reload safety is the whole design constraint. In development Rails does not
|
|
16
|
+
# mutate a reloaded class in place: it removes the constant and autoloads a
|
|
17
|
+
# brand-new Class object under the same name. A registry that held class
|
|
18
|
+
# objects would pin the old, orphaned constants forever, and every lookup
|
|
19
|
+
# would hand back classes the rest of the app has already forgotten. So
|
|
20
|
+
# nothing here stores a Class: entries are keyed by class *name*, and names are
|
|
21
|
+
# resolved back to live constants via #constantize at read time.
|
|
22
|
+
class Registry
|
|
23
|
+
# One `component Foo` declaration on one entity class.
|
|
24
|
+
#
|
|
25
|
+
# A value object over *names*. #entity_class / #component_class resolve on
|
|
26
|
+
# every call, so a Declaration handed out before a reload still resolves to
|
|
27
|
+
# the post-reload constants.
|
|
28
|
+
class Declaration
|
|
29
|
+
attr_reader :entity_class_name, :component_class_name, :options
|
|
30
|
+
|
|
31
|
+
def initialize(entity_class_name:, component_class_name:, options: {})
|
|
32
|
+
@entity_class_name = entity_class_name
|
|
33
|
+
@component_class_name = component_class_name
|
|
34
|
+
@options = options.dup.freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Raises NameError if the constant has gone away. See #components_for.
|
|
39
|
+
def entity_class
|
|
40
|
+
entity_class_name.constantize
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def component_class
|
|
44
|
+
component_class_name.constantize
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ==(other)
|
|
48
|
+
other.is_a?(Declaration) &&
|
|
49
|
+
entity_class_name == other.entity_class_name &&
|
|
50
|
+
component_class_name == other.component_class_name &&
|
|
51
|
+
options == other.options
|
|
52
|
+
end
|
|
53
|
+
alias eql? ==
|
|
54
|
+
|
|
55
|
+
def hash
|
|
56
|
+
[self.class, entity_class_name, component_class_name, options].hash
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def inspect
|
|
60
|
+
"#<#{self.class} #{entity_class_name} => #{component_class_name} #{options.inspect}>"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
clear!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Records one declaration. Returns the Declaration.
|
|
69
|
+
#
|
|
70
|
+
# Raises DuplicateComponent if this entity already declares this component —
|
|
71
|
+
# per ADR-0005 a component appears at most once per entity, and RFC-0004
|
|
72
|
+
# relies on the raise to catch a doubled `component` line at class-load time.
|
|
73
|
+
def register(entity_class:, component_class:, options: {})
|
|
74
|
+
entity_name = name_for(entity_class)
|
|
75
|
+
component_name = name_for(component_class)
|
|
76
|
+
|
|
77
|
+
declarations = (@declarations[entity_name] ||= [])
|
|
78
|
+
|
|
79
|
+
if declarations.any? { |declaration| declaration.component_class_name == component_name }
|
|
80
|
+
raise DuplicateComponent,
|
|
81
|
+
"#{entity_name} already declares #{component_name}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
declaration = Declaration.new(
|
|
85
|
+
entity_class_name: entity_name,
|
|
86
|
+
component_class_name: component_name,
|
|
87
|
+
options: options
|
|
88
|
+
)
|
|
89
|
+
declarations << declaration
|
|
90
|
+
declaration
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The declarations for an entity, in declaration order.
|
|
94
|
+
#
|
|
95
|
+
# Resolution is lazy, so this never raises for a stale entry; asking the
|
|
96
|
+
# returned Declaration for #component_class does. That is deliberate: a
|
|
97
|
+
# dangling name means the registry has drifted out of sync with the app, and
|
|
98
|
+
# silently dropping the entry would make generators emit an incomplete schema
|
|
99
|
+
# and delegation quietly stop working. #clear! is the supported way to drop
|
|
100
|
+
# entries, and the Railtie calls it on every reload.
|
|
101
|
+
def components_for(entity_class)
|
|
102
|
+
declarations = @declarations[name_for(entity_class)]
|
|
103
|
+
declarations ? declarations.dup : []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Every entity class declaring this component, as live class objects.
|
|
107
|
+
def entities_for(component_class)
|
|
108
|
+
component_name = name_for(component_class)
|
|
109
|
+
|
|
110
|
+
@declarations.each_value.with_object([]) do |declarations, entities|
|
|
111
|
+
declarations.each do |declaration|
|
|
112
|
+
entities << declaration.entity_class if declaration.component_class_name == component_name
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Resets the registry. Used between tests and by the Railtie's `to_prepare`.
|
|
118
|
+
def clear!
|
|
119
|
+
@declarations = {}
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# An opaque snapshot of the current declarations, for save/restore around a
|
|
124
|
+
# block that mutates the registry — chiefly tests that `clear!` the
|
|
125
|
+
# process-wide singleton and would otherwise wipe declarations that
|
|
126
|
+
# host/app classes made at load time. `Declaration` is frozen and holds only
|
|
127
|
+
# strings, so a shallow dup of the arrays is a safe, cheap copy.
|
|
128
|
+
def snapshot
|
|
129
|
+
@declarations.transform_values(&:dup)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Replaces the declarations with a previously taken #snapshot.
|
|
133
|
+
def restore(snapshot)
|
|
134
|
+
@declarations = snapshot.transform_values(&:dup)
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# The one place a Class is turned into a String — and the only thing the
|
|
141
|
+
# registry ever retains.
|
|
142
|
+
def name_for(klass)
|
|
143
|
+
raise ArgumentError, "expected a Class, got #{klass.inspect}" unless klass.is_a?(Module)
|
|
144
|
+
|
|
145
|
+
klass.name || raise(ArgumentError, <<~MESSAGE.squish)
|
|
146
|
+
cannot register an anonymous class: the registry keys entries by class
|
|
147
|
+
name so they survive Rails reloading. Assign the class to a constant
|
|
148
|
+
before declaring components on it.
|
|
149
|
+
MESSAGE
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|