ecs_on_rails 0.2.2 → 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 +304 -0
- data/README.md +365 -50
- data/lib/ecs_rails/catalogue/address.rb +39 -0
- data/lib/ecs_rails/catalogue/calendar_date.rb +27 -0
- data/lib/ecs_rails/catalogue/counter.rb +46 -0
- data/lib/ecs_rails/catalogue/discard.rb +36 -0
- data/lib/ecs_rails/catalogue/email.rb +35 -0
- data/lib/ecs_rails/catalogue/geolocation.rb +53 -0
- data/lib/ecs_rails/catalogue/identifier.rb +23 -0
- data/lib/ecs_rails/catalogue/image.rb +28 -0
- data/lib/ecs_rails/catalogue/link.rb +27 -0
- data/lib/ecs_rails/catalogue/marker.rb +38 -0
- data/lib/ecs_rails/catalogue/money.rb +86 -0
- data/lib/ecs_rails/catalogue/name.rb +37 -0
- data/lib/ecs_rails/catalogue/password.rb +50 -0
- data/lib/ecs_rails/catalogue/period.rb +57 -0
- data/lib/ecs_rails/catalogue/phone.rb +30 -0
- data/lib/ecs_rails/catalogue/position.rb +18 -0
- data/lib/ecs_rails/catalogue/rating.rb +25 -0
- data/lib/ecs_rails/catalogue/relationship.rb +159 -0
- data/lib/ecs_rails/catalogue/role.rb +23 -0
- data/lib/ecs_rails/catalogue/schema_diff.rb +198 -0
- data/lib/ecs_rails/catalogue/search_vector.rb +38 -0
- data/lib/ecs_rails/catalogue/state.rb +71 -0
- data/lib/ecs_rails/catalogue/tags.rb +64 -0
- data/lib/ecs_rails/catalogue/text.rb +29 -0
- data/lib/ecs_rails/catalogue/timestamp.rb +34 -0
- data/lib/ecs_rails/catalogue/token.rb +77 -0
- data/lib/ecs_rails/catalogue.rb +405 -0
- data/lib/ecs_rails/component.rb +13 -7
- data/lib/ecs_rails/config.rb +33 -11
- data/lib/ecs_rails/dsl.rb +463 -97
- data/lib/ecs_rails/entity.rb +21 -5
- data/lib/ecs_rails/errors.rb +3 -0
- data/lib/ecs_rails/inverses.rb +398 -0
- data/lib/ecs_rails/lazy.rb +86 -23
- data/lib/ecs_rails/markers.rb +197 -0
- data/lib/ecs_rails/preloading.rb +19 -8
- data/lib/ecs_rails/presence.rb +83 -30
- data/lib/ecs_rails/querying.rb +158 -12
- data/lib/ecs_rails/railtie.rb +10 -0
- data/lib/ecs_rails/registry.rb +66 -21
- data/lib/ecs_rails/relationships.rb +168 -160
- data/lib/ecs_rails/slots.rb +183 -0
- data/lib/ecs_rails/validations.rb +3 -5
- data/lib/ecs_rails/version.rb +2 -2
- data/lib/ecs_rails.rb +7 -3
- data/lib/generators/ecs_rails/catalogue_options.rb +66 -0
- data/lib/generators/ecs_rails/component/templates/migration.rb.tt +9 -5
- data/lib/generators/ecs_rails/component/templates/model.rb.tt +2 -1
- data/lib/generators/ecs_rails/entity/entity_generator.rb +93 -0
- data/lib/generators/ecs_rails/entity/templates/entity.rb.tt +9 -0
- data/lib/generators/ecs_rails/install/install_generator.rb +29 -12
- data/lib/generators/ecs_rails/install/templates/application_component.rb.tt +3 -2
- data/lib/generators/ecs_rails/install/templates/application_entity.rb.tt +2 -2
- data/lib/generators/ecs_rails/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ecs_rails/install/templates/install_migration.rb.tt +38 -0
- data/lib/generators/ecs_rails/templates/catalogue_class.rb.tt +10 -0
- data/lib/generators/ecs_rails/upgrade/templates/catalogue_migration.rb.tt +18 -0
- data/lib/generators/ecs_rails/upgrade/templates/markers_migration.rb.tt +31 -0
- data/lib/generators/ecs_rails/upgrade/templates/relationships_migration.rb.tt +33 -0
- data/lib/generators/ecs_rails/upgrade/templates/slots_migration.rb.tt +23 -0
- data/lib/generators/ecs_rails/upgrade/upgrade_generator.rb +281 -0
- metadata +61 -4
- data/lib/generators/ecs_rails/install/templates/migration.rb.tt +0 -20
- data/lib/generators/ecs_rails/relationship/relationship_generator.rb +0 -99
- data/lib/generators/ecs_rails/relationship/templates/migration.rb.tt +0 -32
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
|
|
5
|
+
module EcsRails
|
|
6
|
+
# The catalogue: standard components that ship in the gem (ADR-0018).
|
|
7
|
+
#
|
|
8
|
+
# Each catalogue component is a module in this namespace — `Catalogue::Money`,
|
|
9
|
+
# `Catalogue::Address` — carrying its behaviour, its validations and a
|
|
10
|
+
# **schema declaration** the generators read. A host application owns the
|
|
11
|
+
# constant and includes the module into a one-line class:
|
|
12
|
+
#
|
|
13
|
+
# class Money < ApplicationComponent
|
|
14
|
+
# include EcsRails::Catalogue::Money
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# `rails g ecs_rails:install` writes those classes and ONE migration creating
|
|
18
|
+
# every catalogue table, so that after install and one `db:migrate` an
|
|
19
|
+
# application is built from the catalogue without another migration. The
|
|
20
|
+
# tables exist; a slot names the role (`component Text, prefix: :title`).
|
|
21
|
+
# `rails g ecs_rails:upgrade` diffs these declarations against the database
|
|
22
|
+
# when the gem is upgraded.
|
|
23
|
+
#
|
|
24
|
+
# Components are grouped into **sets** so a prototype need not carry every
|
|
25
|
+
# table: `core` (the default), `commerce`, and `social` / `saas` (named,
|
|
26
|
+
# empty in 0.3.0). See ADR-0018 §5 for the shelf.
|
|
27
|
+
#
|
|
28
|
+
# The one rule on names: never shadow a Ruby core constant (`Date`, `Time`,
|
|
29
|
+
# `Set`, `Range`) — hence `CalendarDate`. Where a name collides with a gem the
|
|
30
|
+
# application uses (`Money`), the one-line class is the remedy:
|
|
31
|
+
# `class Price < ApplicationComponent; include EcsRails::Catalogue::Money; end`.
|
|
32
|
+
module Catalogue
|
|
33
|
+
# A recorded schema declaration: the columns a catalogue table has beyond
|
|
34
|
+
# the ones every component table has (`id`, `entity_id`, `slot`, timestamps),
|
|
35
|
+
# plus any extra indexes and foreign keys.
|
|
36
|
+
#
|
|
37
|
+
# One declaration, two outputs. {#apply} creates the table on a live
|
|
38
|
+
# connection — the gem's own test schema is built this way — and {#to_ruby}
|
|
39
|
+
# renders the equivalent migration source for the install and upgrade
|
|
40
|
+
# generators. Because both read the same recording, the migration a user
|
|
41
|
+
# runs and the table the gem was tested against cannot drift (ADR-0018:
|
|
42
|
+
# "the schema declaration is the single source of truth for the table").
|
|
43
|
+
class Schema
|
|
44
|
+
# A declared column: `type` (Symbol, an ActiveRecord column type), `name`
|
|
45
|
+
# (Symbol) and `options` (Hash — `default:`, `null:`, `limit:`, `array:`, ...).
|
|
46
|
+
Column = Struct.new(:type, :name, :options)
|
|
47
|
+
|
|
48
|
+
# A declared index beyond the standard `(entity_id, slot)` one: `columns`
|
|
49
|
+
# (Array<Symbol>) and `options` (Hash — `unique:`, `using:`, `where:`, `name:`).
|
|
50
|
+
Index = Struct.new(:columns, :options)
|
|
51
|
+
|
|
52
|
+
# A declared foreign key to `entities` beyond the cascading `entity_id`
|
|
53
|
+
# one: `column` (Symbol) and `on_delete` (`:cascade` or `:nullify`).
|
|
54
|
+
ForeignKey = Struct.new(:column, :on_delete)
|
|
55
|
+
|
|
56
|
+
# The column types a declaration may use. Kept to what every supported
|
|
57
|
+
# PostgreSQL has and ActiveRecord maps natively, so a catalogue table
|
|
58
|
+
# never needs an extension the install migration did not enable.
|
|
59
|
+
COLUMN_TYPES = %i[
|
|
60
|
+
string text integer bigint float decimal boolean date datetime uuid jsonb tsvector
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
# The recorder the `schema` block yields as `t`. It answers the column
|
|
64
|
+
# types like a migration's table definition does, and records rather than
|
|
65
|
+
# executes.
|
|
66
|
+
class Recorder
|
|
67
|
+
def initialize(schema)
|
|
68
|
+
@schema = schema
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
COLUMN_TYPES.each do |type|
|
|
72
|
+
define_method(type) do |name, **options|
|
|
73
|
+
@schema.columns << Column.new(type, name.to_sym, options)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An index beyond the `(entity_id, slot)` one every table gets.
|
|
78
|
+
#
|
|
79
|
+
# @param columns [Array<Symbol>, Symbol]
|
|
80
|
+
# @param options [Hash] `unique:`, `using:`, `where:`, `name:`
|
|
81
|
+
# @return [void]
|
|
82
|
+
def index(columns, **options)
|
|
83
|
+
@schema.indexes << Index.new(Array(columns).map(&:to_sym), options)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A foreign key to `entities` beyond the cascading `entity_id` one.
|
|
87
|
+
#
|
|
88
|
+
# @param column [Symbol] the referencing column
|
|
89
|
+
# @param on_delete [Symbol] `:cascade` or `:nullify`
|
|
90
|
+
# @return [void]
|
|
91
|
+
def foreign_key(column, on_delete:)
|
|
92
|
+
@schema.foreign_keys << ForeignKey.new(column.to_sym, on_delete)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [Array<Column>]
|
|
97
|
+
attr_reader :columns
|
|
98
|
+
# @return [Array<Index>]
|
|
99
|
+
attr_reader :indexes
|
|
100
|
+
# @return [Array<ForeignKey>]
|
|
101
|
+
attr_reader :foreign_keys
|
|
102
|
+
|
|
103
|
+
# @yieldparam t [Recorder] the recorder to declare columns and indexes on
|
|
104
|
+
def initialize
|
|
105
|
+
@columns = []
|
|
106
|
+
@indexes = []
|
|
107
|
+
@foreign_keys = []
|
|
108
|
+
yield Recorder.new(self) if block_given?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Creates the table on `target` — anything answering `create_table`,
|
|
112
|
+
# `add_index` and `add_foreign_key`: an `ActiveRecord::Schema.define`
|
|
113
|
+
# block's receiver, a migration, or a connection.
|
|
114
|
+
#
|
|
115
|
+
# Every catalogue table gets the invariants of architecture.md §2 — UUID
|
|
116
|
+
# primary key, non-null `entity_id`, `slot` defaulting to `""`, a unique
|
|
117
|
+
# index on `(entity_id, slot)`, a cascading foreign key to `entities` —
|
|
118
|
+
# and then whatever the declaration added.
|
|
119
|
+
#
|
|
120
|
+
# @param target [#create_table] where to create it
|
|
121
|
+
# @param table_name [String, Symbol] the table to create
|
|
122
|
+
# @param force [Boolean, Symbol] `create_table`'s `force:` — `:cascade`
|
|
123
|
+
# drops an existing table first; a test schema wants that, a migration
|
|
124
|
+
# never does
|
|
125
|
+
# @return [void]
|
|
126
|
+
def apply(target, table_name:, force: false)
|
|
127
|
+
table_name = table_name.to_sym
|
|
128
|
+
target.create_table(table_name, id: :uuid, default: -> { "gen_random_uuid()" }, force: force) do |t|
|
|
129
|
+
t.uuid :entity_id, null: false
|
|
130
|
+
t.string :slot, null: false, default: ""
|
|
131
|
+
columns.each { |column| t.public_send(column.type, column.name, **column.options) }
|
|
132
|
+
t.timestamps
|
|
133
|
+
end
|
|
134
|
+
target.add_index(table_name, %i[entity_id slot], unique: true)
|
|
135
|
+
indexes.each { |index| target.add_index(table_name, index.columns, **index.options) }
|
|
136
|
+
target.add_foreign_key(table_name, :entities, column: :entity_id, on_delete: :cascade)
|
|
137
|
+
foreign_keys.each do |fk|
|
|
138
|
+
target.add_foreign_key(table_name, :entities, column: fk.column, on_delete: fk.on_delete)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The migration source that does what {#apply} does, for the generators'
|
|
143
|
+
# templates. Indented to sit inside a migration's `def change`.
|
|
144
|
+
#
|
|
145
|
+
# @param table_name [String, Symbol]
|
|
146
|
+
# @param indent [Integer] leading spaces
|
|
147
|
+
# @return [String]
|
|
148
|
+
def to_ruby(table_name:, indent: 4)
|
|
149
|
+
pad = " " * indent
|
|
150
|
+
lines = []
|
|
151
|
+
lines << "#{pad}create_table :#{table_name}, id: :uuid, default: -> { \"gen_random_uuid()\" } do |t|"
|
|
152
|
+
lines << "#{pad} t.uuid :entity_id, null: false"
|
|
153
|
+
lines << "#{pad} t.string :slot, null: false, default: \"\""
|
|
154
|
+
columns.each do |column|
|
|
155
|
+
lines << "#{pad} t.#{column.type} :#{column.name}#{render_options(column.options)}"
|
|
156
|
+
end
|
|
157
|
+
lines << "#{pad} t.timestamps"
|
|
158
|
+
lines << "#{pad}end"
|
|
159
|
+
lines << "#{pad}add_index :#{table_name}, [:entity_id, :slot], unique: true"
|
|
160
|
+
indexes.each do |index|
|
|
161
|
+
cols = index.columns.size == 1 ? ":#{index.columns.first}" : "[#{index.columns.map { |c| ":#{c}" }.join(', ')}]"
|
|
162
|
+
lines << "#{pad}add_index :#{table_name}, #{cols}#{render_options(index.options)}"
|
|
163
|
+
end
|
|
164
|
+
lines << "#{pad}add_foreign_key :#{table_name}, :entities, column: :entity_id, on_delete: :cascade"
|
|
165
|
+
foreign_keys.each do |fk|
|
|
166
|
+
lines << "#{pad}add_foreign_key :#{table_name}, :entities, column: :#{fk.column}, on_delete: :#{fk.on_delete}"
|
|
167
|
+
end
|
|
168
|
+
lines.join("\n")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Additive migration source after verifying the live catalogue structure.
|
|
172
|
+
# Existing incompatible definitions raise before any migration is written.
|
|
173
|
+
#
|
|
174
|
+
# @param table_name [String, Symbol]
|
|
175
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
176
|
+
# @param slot_upgrade [Boolean] an earlier migration will add the slot
|
|
177
|
+
# @param indent [Integer]
|
|
178
|
+
# @return [String] empty when every required structure is current
|
|
179
|
+
# @raise [SchemaMismatch] an explicit reviewed repair is required
|
|
180
|
+
def to_ruby_diff(table_name:, connection:, slot_upgrade: false, indent: 4)
|
|
181
|
+
SchemaDiff.new(self, table_name: table_name, connection: connection,
|
|
182
|
+
slot_upgrade: slot_upgrade).to_ruby(indent: indent)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# `, default: "", null: false` — options as they would be typed. Symbols,
|
|
188
|
+
# strings, numbers, booleans, nil and arrays render with `inspect`, which
|
|
189
|
+
# is exactly their Ruby literal.
|
|
190
|
+
def render_options(options)
|
|
191
|
+
return "" if options.empty?
|
|
192
|
+
|
|
193
|
+
", " + options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Extended into each catalogue component module. Gives the module the
|
|
198
|
+
# declaration DSL — `table`, `set`, `schema`, `included` — registers it in
|
|
199
|
+
# the catalogue, and makes `include EcsRails::Catalogue::Money` into a class
|
|
200
|
+
# set the table name and run the module's `included` block, the way an
|
|
201
|
+
# ActiveSupport::Concern would.
|
|
202
|
+
#
|
|
203
|
+
# module EcsRails::Catalogue::Money
|
|
204
|
+
# extend EcsRails::Catalogue::Definition
|
|
205
|
+
#
|
|
206
|
+
# table "monies"
|
|
207
|
+
# set :commerce
|
|
208
|
+
# schema do |t|
|
|
209
|
+
# t.integer :amount_cents, null: false, default: 0
|
|
210
|
+
# t.string :currency, null: false, default: "USD", limit: 3
|
|
211
|
+
# end
|
|
212
|
+
#
|
|
213
|
+
# included do
|
|
214
|
+
# validates :currency, format: /\A[A-Z]{3}\z/
|
|
215
|
+
# end
|
|
216
|
+
#
|
|
217
|
+
# def +(other) = ...
|
|
218
|
+
# end
|
|
219
|
+
#
|
|
220
|
+
# Not an ActiveSupport::Concern, deliberately: a concern's `included` block
|
|
221
|
+
# has no handle on *which* concern is being included, and the table name is
|
|
222
|
+
# per catalogue component. `Module#included(base)` does.
|
|
223
|
+
module Definition
|
|
224
|
+
# Registers the module with the catalogue as it is defined.
|
|
225
|
+
#
|
|
226
|
+
# @api private
|
|
227
|
+
def self.extended(mod)
|
|
228
|
+
Catalogue.register(mod)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The module's short name, as a Symbol: `:money`, `:calendar_date`. Also
|
|
232
|
+
# the default class name's underscore form and the file name install
|
|
233
|
+
# writes.
|
|
234
|
+
#
|
|
235
|
+
# @return [Symbol]
|
|
236
|
+
def catalogue_name
|
|
237
|
+
name.demodulize.underscore.to_sym
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The default app class name: `"Money"`, `"CalendarDate"`.
|
|
241
|
+
#
|
|
242
|
+
# @return [String]
|
|
243
|
+
def class_name
|
|
244
|
+
name.demodulize
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Declares or reads the table name. Set once in the module body; the
|
|
248
|
+
# including class may still override it (`self.table_name = ...`) after
|
|
249
|
+
# the include, which is how an app whose `emails` table is already taken
|
|
250
|
+
# keeps the catalogue component — the app owns the table name too.
|
|
251
|
+
#
|
|
252
|
+
# @param name [String, nil]
|
|
253
|
+
# @return [String]
|
|
254
|
+
def table(name = nil)
|
|
255
|
+
@table = name.to_s if name
|
|
256
|
+
@table || class_name.underscore.pluralize
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Declares or reads the set. `:core` when not declared.
|
|
260
|
+
#
|
|
261
|
+
# @param name [Symbol, nil]
|
|
262
|
+
# @return [Symbol]
|
|
263
|
+
def set(name = nil)
|
|
264
|
+
@set = name.to_sym if name
|
|
265
|
+
@set || :core
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Declares (or reads) the component's primary attribute — the one a
|
|
269
|
+
# labelled slot stands for, so `component Text, prefix: :title` also
|
|
270
|
+
# delegates `post.title` (see EcsRails::Slots::Component::ClassMethods#primary).
|
|
271
|
+
# Applied to the including class at include time.
|
|
272
|
+
#
|
|
273
|
+
# @param name [Symbol, nil]
|
|
274
|
+
# @return [Symbol, nil]
|
|
275
|
+
def primary_attribute(name = nil)
|
|
276
|
+
@primary_attribute = name.to_sym if name
|
|
277
|
+
@primary_attribute
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Declares (with a block) or reads the schema.
|
|
281
|
+
#
|
|
282
|
+
# @yieldparam t [Schema::Recorder]
|
|
283
|
+
# @return [Schema]
|
|
284
|
+
def schema(&block)
|
|
285
|
+
@schema = Schema.new(&block) if block
|
|
286
|
+
@schema ||= Schema.new
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# With a block: records what to run in the including class — validations,
|
|
290
|
+
# `slot_option`s, associations — exactly as a concern's `included` does.
|
|
291
|
+
# Without one, it is Ruby's own include hook: sets the table name and runs
|
|
292
|
+
# that block.
|
|
293
|
+
#
|
|
294
|
+
# @param base [Class, nil]
|
|
295
|
+
# @return [void]
|
|
296
|
+
def included(base = nil, &block)
|
|
297
|
+
if block
|
|
298
|
+
@included_block = block
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
base.table_name = table
|
|
303
|
+
base.primary(primary_attribute) if primary_attribute
|
|
304
|
+
base.class_eval(&@included_block) if @included_block
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
class << self
|
|
309
|
+
# Every catalogue component, in definition (file) order.
|
|
310
|
+
#
|
|
311
|
+
# @return [Array<Module>]
|
|
312
|
+
def components
|
|
313
|
+
@components ||= []
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Records a catalogue module as it is defined ({Definition.extended}).
|
|
317
|
+
#
|
|
318
|
+
# @param mod [Module]
|
|
319
|
+
# @return [void]
|
|
320
|
+
# @api private
|
|
321
|
+
def register(mod)
|
|
322
|
+
components << mod unless components.include?(mod)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# The catalogue component named `name`, by catalogue name, class name or
|
|
326
|
+
# table.
|
|
327
|
+
#
|
|
328
|
+
# @param name [Symbol, String]
|
|
329
|
+
# @return [Module, nil]
|
|
330
|
+
def [](name)
|
|
331
|
+
key = name.to_s
|
|
332
|
+
components.find do |mod|
|
|
333
|
+
mod.catalogue_name.to_s == key || mod.class_name == key || mod.table == key
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# The set names, in the order they first appear.
|
|
338
|
+
#
|
|
339
|
+
# @return [Array<Symbol>]
|
|
340
|
+
def sets
|
|
341
|
+
(components.map(&:set) + %i[core commerce social saas]).uniq
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The components in the given sets, in catalogue order.
|
|
345
|
+
#
|
|
346
|
+
# @param names [Array<Symbol, String>] set names; `:all` for everything
|
|
347
|
+
# @return [Array<Module>]
|
|
348
|
+
# @raise [ArgumentError] for an unknown set
|
|
349
|
+
def in_sets(*names)
|
|
350
|
+
names = names.flatten.map(&:to_sym)
|
|
351
|
+
return components.dup if names.include?(:all)
|
|
352
|
+
|
|
353
|
+
unknown = names - sets
|
|
354
|
+
raise ArgumentError, "unknown catalogue set#{'s' if unknown.size > 1}: #{unknown.join(', ')}; " \
|
|
355
|
+
"known: #{sets.join(', ')}" unless unknown.empty?
|
|
356
|
+
|
|
357
|
+
components.select { |mod| names.include?(mod.set) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Creates every catalogue table (or those of the given sets) on `target`.
|
|
361
|
+
# The gem's test schema is built this way.
|
|
362
|
+
#
|
|
363
|
+
# @param target [#create_table]
|
|
364
|
+
# @param sets [Array<Symbol>, Symbol] default every set
|
|
365
|
+
# @param except [Array<Module, Symbol>] components to skip
|
|
366
|
+
# @param force [Boolean, Symbol] passed to {Schema#apply}
|
|
367
|
+
# @return [void]
|
|
368
|
+
def create_tables(target, sets: :all, except: [], force: false)
|
|
369
|
+
skipped = except.map { |c| c.is_a?(Module) ? c : self[c] }
|
|
370
|
+
in_sets(sets).each do |mod|
|
|
371
|
+
next if skipped.include?(mod)
|
|
372
|
+
|
|
373
|
+
mod.schema.apply(target, table_name: mod.table, force: force)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
require "ecs_rails/catalogue/schema_diff"
|
|
381
|
+
require "ecs_rails/catalogue/relationship"
|
|
382
|
+
require "ecs_rails/catalogue/marker"
|
|
383
|
+
require "ecs_rails/catalogue/name"
|
|
384
|
+
require "ecs_rails/catalogue/email"
|
|
385
|
+
require "ecs_rails/catalogue/password"
|
|
386
|
+
require "ecs_rails/catalogue/phone"
|
|
387
|
+
require "ecs_rails/catalogue/address"
|
|
388
|
+
require "ecs_rails/catalogue/geolocation"
|
|
389
|
+
require "ecs_rails/catalogue/link"
|
|
390
|
+
require "ecs_rails/catalogue/text"
|
|
391
|
+
require "ecs_rails/catalogue/identifier"
|
|
392
|
+
require "ecs_rails/catalogue/counter"
|
|
393
|
+
require "ecs_rails/catalogue/rating"
|
|
394
|
+
require "ecs_rails/catalogue/timestamp"
|
|
395
|
+
require "ecs_rails/catalogue/calendar_date"
|
|
396
|
+
require "ecs_rails/catalogue/period"
|
|
397
|
+
require "ecs_rails/catalogue/position"
|
|
398
|
+
require "ecs_rails/catalogue/state"
|
|
399
|
+
require "ecs_rails/catalogue/tags"
|
|
400
|
+
require "ecs_rails/catalogue/search_vector"
|
|
401
|
+
require "ecs_rails/catalogue/discard"
|
|
402
|
+
require "ecs_rails/catalogue/image"
|
|
403
|
+
require "ecs_rails/catalogue/role"
|
|
404
|
+
require "ecs_rails/catalogue/token"
|
|
405
|
+
require "ecs_rails/catalogue/money"
|
data/lib/ecs_rails/component.rb
CHANGED
|
@@ -23,8 +23,9 @@ module EcsRails
|
|
|
23
23
|
# Scopes, validations, callbacks and associations all work as normal: this is
|
|
24
24
|
# a plain AR model that happens to hang off an entity.
|
|
25
25
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# Relationship target types are supplied by the owner's declaration as slot
|
|
27
|
+
# options; the shared Relationship component does not name domain classes
|
|
28
|
+
# (ADR-0017).
|
|
28
29
|
class Component < ActiveRecord::Base
|
|
29
30
|
self.abstract_class = true
|
|
30
31
|
|
|
@@ -34,13 +35,18 @@ module EcsRails
|
|
|
34
35
|
# virtual instance (architecture.md §3).
|
|
35
36
|
include Lazy::Component
|
|
36
37
|
|
|
38
|
+
# Labelled components (RFC-0014 / ADR-0015), from the component's side: the
|
|
39
|
+
# slot-aware reader name, and the `slot_option` DSL through which a
|
|
40
|
+
# declaration site configures this component per slot.
|
|
41
|
+
include Slots::Component
|
|
42
|
+
|
|
37
43
|
# Every component belongs to exactly one entity (architecture.md §1).
|
|
38
44
|
#
|
|
39
45
|
# The association targets the abstract ApplicationEntity, which has no table
|
|
40
46
|
# of its own — but EcsRails::Entity sets table_name to "entities", so the
|
|
41
47
|
# generated query is an ordinary select against that table. The loaded row's
|
|
42
48
|
# `model` column then decides which subclass to instantiate, via
|
|
43
|
-
# Entity.
|
|
49
|
+
# Entity.instantiate_instance_of (ADR-0008's amendment). This makes `email.entity`
|
|
44
50
|
# return a User rather than an ApplicationEntity (RFC-0003, ADR-0008).
|
|
45
51
|
#
|
|
46
52
|
# `optional: false` is explicit rather than inherited from
|
|
@@ -60,9 +66,9 @@ module EcsRails
|
|
|
60
66
|
# destructive; the DB layer already has the right one.
|
|
61
67
|
belongs_to :entity, class_name: "ApplicationEntity", optional: false
|
|
62
68
|
|
|
63
|
-
# The unique index on entity_id (ADR-0005
|
|
64
|
-
#
|
|
65
|
-
# racing. RFC-0003 asserts RecordNotUnique
|
|
66
|
-
# is left where it is enforced.
|
|
69
|
+
# The unique index on (entity_id, slot) (ADR-0005, generalised by ADR-0015)
|
|
70
|
+
# is the real enforcement, and a uniqueness validation here would cost a
|
|
71
|
+
# SELECT on every save while still racing. RFC-0003 asserts RecordNotUnique
|
|
72
|
+
# from the database, so the guard is left where it is enforced.
|
|
67
73
|
end
|
|
68
74
|
end
|
data/lib/ecs_rails/config.rb
CHANGED
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module EcsRails
|
|
4
|
-
#
|
|
4
|
+
# Configuration: the generator directory layout (ADR-0010) and the name of
|
|
5
|
+
# the host app's `Relationship` component (ADR-0017).
|
|
5
6
|
#
|
|
6
|
-
# The
|
|
7
|
-
# name (RFC-0002) and by the `model` discriminator (ADR-0002), so the
|
|
8
|
-
# does not care where a class file lives
|
|
9
|
-
#
|
|
10
|
-
# can echo the chosen path.
|
|
7
|
+
# The layout half is generator-only: entities and components are keyed by
|
|
8
|
+
# class name (RFC-0002) and by the `model` discriminator (ADR-0002), so the
|
|
9
|
+
# registry does not care where a class file lives; the generators (RFC-0008)
|
|
10
|
+
# read it to know where to write, and the initializer they emit echoes it.
|
|
11
11
|
#
|
|
12
|
-
# `entities_path` is the single knob. Components always live in a
|
|
13
|
-
# subdirectory of it, which is why `components_path` is derived
|
|
14
|
-
# separately settable — ADR-0010 deliberately exposes one path,
|
|
15
|
-
#
|
|
12
|
+
# `entities_path` is the single layout knob. Components always live in a
|
|
13
|
+
# `components` subdirectory of it, which is why `components_path` is derived
|
|
14
|
+
# rather than separately settable — ADR-0010 deliberately exposes one path,
|
|
15
|
+
# not two.
|
|
16
|
+
#
|
|
17
|
+
# `relationship_class_name` and `marker_class_name` ARE read at runtime — by
|
|
18
|
+
# `relates_to` and `marker`, on every call, so a reloaded constant is picked
|
|
19
|
+
# up. They exist for the one app whose domain already has a `Relationship` or
|
|
20
|
+
# a `Marker`; everyone else leaves the defaults.
|
|
21
|
+
# @example Placing entities under app/models (components remain in a subdirectory)
|
|
16
22
|
# # config/initializers/ecs_rails.rb
|
|
17
23
|
# EcsRails.configure { |config| config.entities_path = "app/models" }
|
|
18
24
|
#
|
|
19
25
|
# @see EcsRails.configure
|
|
20
26
|
class Config
|
|
21
27
|
# Entities land here; the default is the ADR-0010 layout. Set it to
|
|
22
|
-
# `"app/models"` to
|
|
28
|
+
# `"app/models"` to place entities there and components in its subdirectory.
|
|
23
29
|
#
|
|
24
30
|
# @return [String] the directory entities are generated into
|
|
25
31
|
attr_accessor :entities_path
|
|
26
32
|
|
|
33
|
+
# The host app's catalogue component that backs every `relates_to`
|
|
34
|
+
# (EcsRails::Catalogue::Relationship, ADR-0017). `rails g ecs_rails:install`
|
|
35
|
+
# writes it as `Relationship`; rename here only if that constant is taken.
|
|
36
|
+
#
|
|
37
|
+
# @return [String] the class name, resolved by `constantize` at each use
|
|
38
|
+
attr_accessor :relationship_class_name
|
|
39
|
+
|
|
40
|
+
# The host app's catalogue component that backs every `marker`
|
|
41
|
+
# (EcsRails::Catalogue::Marker, ADR-0018 §4). `rails g ecs_rails:install`
|
|
42
|
+
# writes it as `Marker`; rename here only if that constant is taken.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] the class name, resolved by `constantize` at each use
|
|
45
|
+
attr_accessor :marker_class_name
|
|
46
|
+
|
|
27
47
|
def initialize
|
|
28
48
|
@entities_path = "app/entities"
|
|
49
|
+
@relationship_class_name = "Relationship"
|
|
50
|
+
@marker_class_name = "Marker"
|
|
29
51
|
end
|
|
30
52
|
|
|
31
53
|
# Components live in a `components` subdirectory of the entities path. The
|