Class: Seams::Generators::AccountsGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
EjectAware, HostInjector
Defined in:
lib/generators/seams/accounts/accounts_generator.rb

Overview

Generates a canonical Accounts engine on top of the generic engine scaffold. Adds:

- Accounts::Account model. Tenant boundary. UUID PK.
- Accounts::Membership model. Joins Auth::Identity to Account
  with a role enum (owner/admin/member/system); identity_id is
  nullable for system actors used by audit-log writes.
- Accounts::Current per-request namespace.
- Accounts::AccountScoped model concern (default_scope to
  Current.account, opt-out via .unscoped).
- Accounts::Authorization controller concern (default-on
  ensure_account_access; opt out via disallow_account_scope or
  require_access_without_membership; helpers ensure_admin /
  ensure_staff).
- Migrations for accounts + accounts_memberships (pgcrypto).
- lib/accounts/engine.rb registers the canonical events:
  account.created.accounts, account.cancelled.accounts,
  membership.created.accounts, membership.role_changed.accounts,
  membership.removed.accounts.

The engine ships NO controllers in Wave 9 — hosts drive their own account-creation flows; this engine is the model + concern layer.

Run with: bin/rails generate seams:accounts

Constant Summary collapse

ENGINE_NAME =
"accounts"

Constants included from EjectAware

EjectAware::EJECT_HEADER_PREFIX

Instance Method Summary collapse

Methods included from EjectAware

#ejected?, #template_unless_ejected

Methods included from HostInjector

#host_inject_gem, #host_inject_include_in_application_controller, #host_inject_include_in_user, #host_inject_mount, #host_uninject_gem, #host_uninject_include, #host_uninject_mount, #routes_draw_anchor

Instance Method Details

#create_concernsObject



73
74
75
76
77
78
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 73

def create_concerns
  template_unless_ejected "lib/concerns/account_scoped.rb.tt",
                          engine_path("lib/accounts/concerns/account_scoped.rb")
  template_unless_ejected "lib/concerns/authorization.rb.tt",
                          engine_path("lib/accounts/concerns/authorization.rb")
end

#create_dummy_appObject



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 113

def create_dummy_app
  # Post Wave 9: the dummy app does NOT ship a host User model.
  # Auth::Identity is the canonical human; accounts membership
  # is the per-tenant role row. The accounts engine specs DO
  # exercise Auth::Identity directly (a Membership without an
  # Identity to point at is meaningless), so we ship a slim
  # stub at app/models/auth/identity.rb the same way the
  # notifications engine does.
  Seams::Generators::DummyAppWriter.write!(
    engine_path: File.join(destination_root, "engines", ENGINE_NAME),
    engine_module: "Accounts",
    mount_at: "/accounts",
    schema: dummy_schema,
    host_user: dummy_host_identity,
    host_user_path: "app/models/auth/identity.rb"
  )
  write_auth_current_stub
  template "spec/runtime/accounts_boot_spec.rb.tt",
           engine_path("spec/runtime/accounts_boot_spec.rb")
end

#create_engine_skeletonObject



46
47
48
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 46

def create_engine_skeleton
  EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
end

#create_factoriesObject



87
88
89
90
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 87

def create_factories
  template_unless_ejected "spec/factories/accounts.rb.tt",
                          engine_path("spec/factories/accounts.rb")
end

#create_migrationsObject



80
81
82
83
84
85
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 80

def create_migrations
  template "db/migrate/create_accounts.rb.tt",
           engine_path("db/migrate/#{timestamp(0)}_create_accounts.rb")
  template "db/migrate/create_accounts_memberships.rb.tt",
           engine_path("db/migrate/#{timestamp(1)}_create_accounts_memberships.rb")
end

#create_modelsObject



62
63
64
65
66
67
68
69
70
71
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 62

def create_models
  template_unless_ejected "app/models/application_record.rb.tt",
                          engine_path("app/models/accounts/application_record.rb")
  template_unless_ejected "app/models/account.rb.tt",
                          engine_path("app/models/accounts/account.rb")
  template_unless_ejected "app/models/membership.rb.tt",
                          engine_path("app/models/accounts/membership.rb")
  template_unless_ejected "app/models/current.rb.tt",
                          engine_path("app/models/accounts/current.rb")
end

#create_runtime_specsObject



156
157
158
159
160
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 156

def create_runtime_specs
  # Currently a single runtime boot spec covers events, schema,
  # create_with_owner, and Accounts::Current. Split into multiple
  # files in a later phase if the file grows past ~120 lines.
end

#create_unit_specsObject



92
93
94
95
96
97
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 92

def create_unit_specs
  template_unless_ejected "spec/models/accounts/account_spec.rb.tt",
                          engine_path("spec/models/accounts/account_spec.rb")
  template_unless_ejected "spec/models/accounts/membership_spec.rb.tt",
                          engine_path("spec/models/accounts/membership_spec.rb")
end

#overwrite_engine_entry_pointObject



50
51
52
53
54
55
56
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 50

def overwrite_engine_entry_point
  # engine.rb / lib/accounts.rb stay framework-managed.
  template "lib/engine.rb.tt",                 engine_path("lib/accounts/engine.rb"),        force: true
  template "lib/accounts.rb.tt",               engine_path("lib/accounts.rb"),               force: true
  template_unless_ejected "lib/configuration.rb.tt",
                          engine_path("lib/accounts/configuration.rb")
end

#overwrite_readmeObject



99
100
101
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 99

def overwrite_readme
  template "README.md.tt", engine_path("README.md"), force: true
end

#overwrite_routesObject



58
59
60
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 58

def overwrite_routes
  template_unless_ejected "config/routes.rb.tt", engine_path("config/routes.rb"), force: true
end

#report_summaryObject



172
173
174
175
176
177
178
179
180
181
182
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 172

def report_summary
  say ""
  say "  Accounts engine generated at engines/accounts/", :green
  say ""
  say "  Next steps:", :yellow
  say "    1. bin/rails db:migrate"
  say "    2. Include Accounts::Authorization in your ApplicationController"
  say "    3. Wire Accounts::Current.account in a before_action"
  say "    4. Run the engine specs: bin/rails seams:test[accounts]"
  say ""
end

#update_exposed_concernsObject



103
104
105
106
107
108
109
110
111
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 103

def update_exposed_concerns
  rubocop_path = engine_path(".rubocop.yml")
  return unless File.exist?(rubocop_path)

  contents     = File.read(rubocop_path)
  replacement  = "  ExposedConcerns:\n    - Accounts::AccountScoped\n    - Accounts::Authorization"
  contents.sub!(/^  ExposedConcerns: \[\]$/, replacement)
  File.write(rubocop_path, contents)
end

#wire_into_hostObject



162
163
164
165
166
167
168
169
170
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 162

def wire_into_host
  # factory_bot_rails powers spec/factories/accounts.rb. Lives
  # in the host's test group only.
  host_inject_gem("factory_bot_rails", "~> 6.4", group: :test)
  host_inject_mount(engine_class: "Accounts::Engine", at: "/accounts")
  # NB: no host_inject_include_in_user — the host User is going
  # away in Wave 9. Hosts that DO keep a User model wire it up
  # themselves.
end

#write_auth_current_stubObject

Write a tiny ‘Auth::Current` stub so the accounts engine specs (which read Current.identity) can run without pulling in the full auth engine.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/generators/seams/accounts/accounts_generator.rb', line 137

def write_auth_current_stub
  path = File.join(destination_root, "engines", ENGINE_NAME,
                   "spec/dummy/app/models/auth/current.rb")
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, <<~RB)
    # frozen_string_literal: true
    # Slim Auth::Current stub for the accounts dummy app. Stands in
    # for the real Auth::Current (which lives in the auth engine,
    # not loaded by the dummy) so accounts specs can wire
    # `Current.identity = identity` against the same surface area
    # the canonical seams host uses.
    module Auth
      class Current < ActiveSupport::CurrentAttributes
        attribute :identity
      end
    end
  RB
end