smart_domain 0.1.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.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +25 -0
  4. data/LICENSE +21 -0
  5. data/README.md +1219 -0
  6. data/Rakefile +12 -0
  7. data/examples/blog_app/.dockerignore +51 -0
  8. data/examples/blog_app/.github/dependabot.yml +12 -0
  9. data/examples/blog_app/.github/workflows/ci.yml +67 -0
  10. data/examples/blog_app/.gitignore +30 -0
  11. data/examples/blog_app/.kamal/hooks/docker-setup.sample +3 -0
  12. data/examples/blog_app/.kamal/hooks/post-app-boot.sample +3 -0
  13. data/examples/blog_app/.kamal/hooks/post-deploy.sample +14 -0
  14. data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
  15. data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +3 -0
  16. data/examples/blog_app/.kamal/hooks/pre-build.sample +51 -0
  17. data/examples/blog_app/.kamal/hooks/pre-connect.sample +47 -0
  18. data/examples/blog_app/.kamal/hooks/pre-deploy.sample +122 -0
  19. data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  20. data/examples/blog_app/.kamal/secrets +20 -0
  21. data/examples/blog_app/.rubocop.yml +8 -0
  22. data/examples/blog_app/.ruby-version +1 -0
  23. data/examples/blog_app/Dockerfile +76 -0
  24. data/examples/blog_app/Gemfile +63 -0
  25. data/examples/blog_app/Gemfile.lock +408 -0
  26. data/examples/blog_app/README.md +24 -0
  27. data/examples/blog_app/README_EXAMPLE.md +328 -0
  28. data/examples/blog_app/Rakefile +6 -0
  29. data/examples/blog_app/app/assets/images/.keep +0 -0
  30. data/examples/blog_app/app/assets/stylesheets/application.css +10 -0
  31. data/examples/blog_app/app/controllers/api/base_controller.rb +61 -0
  32. data/examples/blog_app/app/controllers/api/v1/posts_controller.rb +158 -0
  33. data/examples/blog_app/app/controllers/api/v1/users_controller.rb +98 -0
  34. data/examples/blog_app/app/controllers/application_controller.rb +7 -0
  35. data/examples/blog_app/app/controllers/concerns/.keep +0 -0
  36. data/examples/blog_app/app/domains/.keep +0 -0
  37. data/examples/blog_app/app/domains/exceptions.rb +19 -0
  38. data/examples/blog_app/app/domains/post_management/events/post_created_event.rb +15 -0
  39. data/examples/blog_app/app/domains/post_management/events/post_deleted_event.rb +13 -0
  40. data/examples/blog_app/app/domains/post_management/events/post_updated_event.rb +13 -0
  41. data/examples/blog_app/app/domains/post_management/handlers/post_notification_handler.rb +33 -0
  42. data/examples/blog_app/app/domains/post_management/models/post.rb +21 -0
  43. data/examples/blog_app/app/domains/post_management/policies/post_policy.rb +49 -0
  44. data/examples/blog_app/app/domains/post_management/post_service.rb +93 -0
  45. data/examples/blog_app/app/domains/post_management/setup.rb +25 -0
  46. data/examples/blog_app/app/domains/user_management/events/user_created_event.rb +15 -0
  47. data/examples/blog_app/app/domains/user_management/events/user_deleted_event.rb +13 -0
  48. data/examples/blog_app/app/domains/user_management/events/user_updated_event.rb +13 -0
  49. data/examples/blog_app/app/domains/user_management/handlers/user_welcome_handler.rb +21 -0
  50. data/examples/blog_app/app/domains/user_management/models/user.rb +11 -0
  51. data/examples/blog_app/app/domains/user_management/policies/user_policy.rb +49 -0
  52. data/examples/blog_app/app/domains/user_management/setup.rb +25 -0
  53. data/examples/blog_app/app/domains/user_management/user_service.rb +93 -0
  54. data/examples/blog_app/app/events/.keep +0 -0
  55. data/examples/blog_app/app/events/application_event.rb +18 -0
  56. data/examples/blog_app/app/handlers/.keep +0 -0
  57. data/examples/blog_app/app/helpers/application_helper.rb +2 -0
  58. data/examples/blog_app/app/javascript/application.js +3 -0
  59. data/examples/blog_app/app/javascript/controllers/application.js +9 -0
  60. data/examples/blog_app/app/javascript/controllers/hello_controller.js +7 -0
  61. data/examples/blog_app/app/javascript/controllers/index.js +4 -0
  62. data/examples/blog_app/app/jobs/application_job.rb +7 -0
  63. data/examples/blog_app/app/mailers/application_mailer.rb +4 -0
  64. data/examples/blog_app/app/models/application_record.rb +3 -0
  65. data/examples/blog_app/app/models/concerns/.keep +0 -0
  66. data/examples/blog_app/app/models/organization.rb +6 -0
  67. data/examples/blog_app/app/policies/.keep +0 -0
  68. data/examples/blog_app/app/policies/application_policy.rb +23 -0
  69. data/examples/blog_app/app/services/.keep +0 -0
  70. data/examples/blog_app/app/services/application_service.rb +24 -0
  71. data/examples/blog_app/app/views/layouts/application.html.erb +29 -0
  72. data/examples/blog_app/app/views/layouts/mailer.html.erb +13 -0
  73. data/examples/blog_app/app/views/layouts/mailer.text.erb +1 -0
  74. data/examples/blog_app/app/views/pwa/manifest.json.erb +22 -0
  75. data/examples/blog_app/app/views/pwa/service-worker.js +26 -0
  76. data/examples/blog_app/bin/brakeman +7 -0
  77. data/examples/blog_app/bin/bundler-audit +6 -0
  78. data/examples/blog_app/bin/ci +6 -0
  79. data/examples/blog_app/bin/dev +2 -0
  80. data/examples/blog_app/bin/docker-entrypoint +8 -0
  81. data/examples/blog_app/bin/importmap +4 -0
  82. data/examples/blog_app/bin/jobs +6 -0
  83. data/examples/blog_app/bin/kamal +27 -0
  84. data/examples/blog_app/bin/rails +4 -0
  85. data/examples/blog_app/bin/rake +4 -0
  86. data/examples/blog_app/bin/rubocop +8 -0
  87. data/examples/blog_app/bin/setup +35 -0
  88. data/examples/blog_app/bin/thrust +5 -0
  89. data/examples/blog_app/config/application.rb +52 -0
  90. data/examples/blog_app/config/boot.rb +4 -0
  91. data/examples/blog_app/config/bundler-audit.yml +5 -0
  92. data/examples/blog_app/config/cable.yml +17 -0
  93. data/examples/blog_app/config/cache.yml +16 -0
  94. data/examples/blog_app/config/ci.rb +19 -0
  95. data/examples/blog_app/config/credentials.yml.enc +1 -0
  96. data/examples/blog_app/config/database.yml +41 -0
  97. data/examples/blog_app/config/deploy.yml +120 -0
  98. data/examples/blog_app/config/environment.rb +5 -0
  99. data/examples/blog_app/config/environments/development.rb +78 -0
  100. data/examples/blog_app/config/environments/production.rb +90 -0
  101. data/examples/blog_app/config/environments/test.rb +53 -0
  102. data/examples/blog_app/config/importmap.rb +7 -0
  103. data/examples/blog_app/config/initializers/active_domain.rb +37 -0
  104. data/examples/blog_app/config/initializers/assets.rb +7 -0
  105. data/examples/blog_app/config/initializers/content_security_policy.rb +29 -0
  106. data/examples/blog_app/config/initializers/filter_parameter_logging.rb +8 -0
  107. data/examples/blog_app/config/initializers/inflections.rb +16 -0
  108. data/examples/blog_app/config/locales/en.yml +31 -0
  109. data/examples/blog_app/config/master.key +1 -0
  110. data/examples/blog_app/config/puma.rb +42 -0
  111. data/examples/blog_app/config/queue.yml +18 -0
  112. data/examples/blog_app/config/recurring.yml +15 -0
  113. data/examples/blog_app/config/routes.rb +27 -0
  114. data/examples/blog_app/config/storage.yml +27 -0
  115. data/examples/blog_app/config.ru +6 -0
  116. data/examples/blog_app/db/cable_schema.rb +11 -0
  117. data/examples/blog_app/db/cache_schema.rb +12 -0
  118. data/examples/blog_app/db/migrate/20251230112502_create_organizations.rb +9 -0
  119. data/examples/blog_app/db/migrate/20251230112503_create_users.rb +12 -0
  120. data/examples/blog_app/db/migrate/20251230112504_create_posts.rb +14 -0
  121. data/examples/blog_app/db/queue_schema.rb +129 -0
  122. data/examples/blog_app/db/schema.rb +46 -0
  123. data/examples/blog_app/db/seeds.rb +9 -0
  124. data/examples/blog_app/lib/api_demo.rb +175 -0
  125. data/examples/blog_app/lib/demo.rb +150 -0
  126. data/examples/blog_app/lib/tasks/.keep +0 -0
  127. data/examples/blog_app/log/.keep +0 -0
  128. data/examples/blog_app/public/400.html +135 -0
  129. data/examples/blog_app/public/404.html +135 -0
  130. data/examples/blog_app/public/406-unsupported-browser.html +135 -0
  131. data/examples/blog_app/public/422.html +135 -0
  132. data/examples/blog_app/public/500.html +135 -0
  133. data/examples/blog_app/public/icon.png +0 -0
  134. data/examples/blog_app/public/icon.svg +3 -0
  135. data/examples/blog_app/public/robots.txt +1 -0
  136. data/examples/blog_app/script/.keep +0 -0
  137. data/examples/blog_app/storage/.keep +0 -0
  138. data/examples/blog_app/tmp/.keep +0 -0
  139. data/examples/blog_app/vendor/.keep +0 -0
  140. data/examples/blog_app/vendor/javascript/.keep +0 -0
  141. data/lib/generators/active_domain/domain/domain_generator.rb +116 -0
  142. data/lib/generators/active_domain/domain/templates/events/created_event.rb.tt +15 -0
  143. data/lib/generators/active_domain/domain/templates/events/deleted_event.rb.tt +13 -0
  144. data/lib/generators/active_domain/domain/templates/events/updated_event.rb.tt +13 -0
  145. data/lib/generators/active_domain/domain/templates/policy.rb.tt +49 -0
  146. data/lib/generators/active_domain/domain/templates/service.rb.tt +93 -0
  147. data/lib/generators/active_domain/domain/templates/setup.rb.tt +27 -0
  148. data/lib/generators/active_domain/install/install_generator.rb +58 -0
  149. data/lib/generators/active_domain/install/templates/README +28 -0
  150. data/lib/generators/active_domain/install/templates/application_event.rb +18 -0
  151. data/lib/generators/active_domain/install/templates/application_policy.rb +23 -0
  152. data/lib/generators/active_domain/install/templates/application_service.rb +24 -0
  153. data/lib/generators/active_domain/install/templates/initializer.rb +37 -0
  154. data/lib/smart_domain/configuration.rb +97 -0
  155. data/lib/smart_domain/domain/exceptions.rb +164 -0
  156. data/lib/smart_domain/domain/policy.rb +215 -0
  157. data/lib/smart_domain/domain/service.rb +230 -0
  158. data/lib/smart_domain/event/adapters/memory.rb +110 -0
  159. data/lib/smart_domain/event/base.rb +176 -0
  160. data/lib/smart_domain/event/handler.rb +98 -0
  161. data/lib/smart_domain/event/mixins.rb +156 -0
  162. data/lib/smart_domain/event/registration.rb +136 -0
  163. data/lib/smart_domain/generators/domain_generator.rb +4 -0
  164. data/lib/smart_domain/generators/install_generator.rb +4 -0
  165. data/lib/smart_domain/handlers/audit_handler.rb +216 -0
  166. data/lib/smart_domain/handlers/metrics_handler.rb +104 -0
  167. data/lib/smart_domain/integration/active_record.rb +169 -0
  168. data/lib/smart_domain/integration/multi_tenancy.rb +115 -0
  169. data/lib/smart_domain/railtie.rb +62 -0
  170. data/lib/smart_domain/tasks/domains.rake +43 -0
  171. data/lib/smart_domain/version.rb +5 -0
  172. data/lib/smart_domain.rb +77 -0
  173. data/smart_domain.gemspec +53 -0
  174. metadata +391 -0
@@ -0,0 +1,328 @@
1
+ # SmartDomain Example: Blog Application
2
+
3
+ This is a complete example Rails application demonstrating all the features of the **SmartDomain** gem.
4
+
5
+ ## What This Example Demonstrates
6
+
7
+ This blog application showcases how SmartDomain brings Domain-Driven Design (DDD) and Event-Driven Architecture (EDA) patterns to Rails:
8
+
9
+ 1. **Domain Services** - Business logic encapsulated in service objects
10
+ 2. **Domain Events** - Immutable events with type safety and validation
11
+ 3. **Event Bus** - Publish-subscribe pattern for event distribution
12
+ 4. **Generic Handlers** - 70% boilerplate reduction with `register_standard_handlers`
13
+ 5. **Custom Event Handlers** - Domain-specific handlers for side effects
14
+ 6. **Event Mixins** - Reusable event attributes (Actor, ChangeTracking, etc.)
15
+ 7. **Domain Policies** - Pundit-style authorization
16
+ 8. **Multi-tenancy** - Organization-based data isolation
17
+ 9. **ActiveRecord Integration** - Transaction-safe event publishing
18
+
19
+ ## Application Structure
20
+
21
+ This example uses a **domain-centric structure** where all domain-specific files (models, events, handlers, policies) are grouped within each domain directory. This follows Domain-Driven Design (DDD) best practices and the Aeyes architecture pattern.
22
+
23
+ ```
24
+ app/
25
+ ├── domains/
26
+ │ ├── user_management/ # User domain (bounded context)
27
+ │ │ ├── models/
28
+ │ │ │ └── user.rb # User model with SmartDomain integration
29
+ │ │ ├── events/
30
+ │ │ │ ├── user_created_event.rb # User created event
31
+ │ │ │ ├── user_updated_event.rb # User updated event
32
+ │ │ │ └── user_deleted_event.rb # User deleted event
33
+ │ │ ├── handlers/
34
+ │ │ │ └── user_welcome_handler.rb # Custom: Send welcome emails
35
+ │ │ ├── policies/
36
+ │ │ │ └── user_policy.rb # User authorization
37
+ │ │ ├── user_service.rb # Business logic for users
38
+ │ │ └── setup.rb # Event handler registration
39
+ │ │
40
+ │ └── post_management/ # Post domain (bounded context)
41
+ │ ├── models/
42
+ │ │ └── post.rb # Post model with SmartDomain integration
43
+ │ ├── events/
44
+ │ │ ├── post_created_event.rb # Post created event
45
+ │ │ ├── post_updated_event.rb # Post updated event
46
+ │ │ └── post_deleted_event.rb # Post deleted event
47
+ │ ├── handlers/
48
+ │ │ └── post_notification_handler.rb # Custom: Notify on publish
49
+ │ ├── policies/
50
+ │ │ └── post_policy.rb # Post authorization
51
+ │ ├── post_service.rb # Business logic for posts
52
+ │ └── setup.rb # Event handler registration
53
+
54
+ ├── events/
55
+ │ └── application_event.rb # Base event class (shared)
56
+ ├── handlers/
57
+ │ └── .keep
58
+ ├── policies/
59
+ │ └── application_policy.rb # Base policy class (shared)
60
+ └── models/
61
+ ├── organization.rb # Multi-tenant organization (shared)
62
+ └── application_record.rb # Base model class (shared)
63
+ ```
64
+
65
+ **Key Design Decisions:**
66
+
67
+ 1. **Domain-Centric Organization**: All domain-specific code lives within `app/domains/{domain}/` subdirectories
68
+ 2. **Shared Base Classes**: Base classes like `ApplicationEvent` and `ApplicationPolicy` remain at the top level since they're shared across domains
69
+ 3. **Bounded Contexts**: Each domain is a self-contained bounded context with its own models, events, handlers, and policies
70
+ 4. **Autoloading Configuration**: Rails is configured to autoload from domain subdirectories (see `config/application.rb`)
71
+
72
+ ## Setup Instructions
73
+
74
+ ### 1. Install Dependencies
75
+
76
+ ```bash
77
+ cd examples/blog_app
78
+ bundle install
79
+ ```
80
+
81
+ ### 2. Setup Database
82
+
83
+ ```bash
84
+ rails db:create db:migrate
85
+ ```
86
+
87
+ ### 3. Run the Demo Script
88
+
89
+ ```bash
90
+ rails runner lib/demo.rb
91
+ ```
92
+
93
+ This will run a comprehensive demonstration showing:
94
+ - Creating an organization (multi-tenancy)
95
+ - Creating users via domain services
96
+ - Publishing domain events
97
+ - Generic and custom event handlers
98
+ - Creating and publishing blog posts
99
+ - Authorization with policies
100
+ - Multi-tenant data isolation
101
+
102
+ ## Key Features Demonstrated
103
+
104
+ ### 1. Domain Services
105
+
106
+ Services encapsulate business logic and publish domain events:
107
+
108
+ ```ruby
109
+ service = UserManagement::UserService.new(
110
+ current_user: admin,
111
+ organization_id: org.id
112
+ )
113
+
114
+ user = service.create(User, {
115
+ email: "john@example.com",
116
+ name: "John Doe",
117
+ role: "admin"
118
+ })
119
+ # Automatically publishes UserCreatedEvent
120
+ ```
121
+
122
+ ### 2. Event Publishing with Mixins
123
+
124
+ Events use mixins for common attributes:
125
+
126
+ ```ruby
127
+ event = UserCreatedEvent.new(
128
+ event_type: "user.created",
129
+ aggregate_id: user.id,
130
+ aggregate_type: "User",
131
+ organization_id: org.id,
132
+ actor_id: current_user.id, # From ActorMixin
133
+ actor_email: current_user.email, # From ActorMixin
134
+ user_id: user.id,
135
+ email: user.email
136
+ )
137
+ ```
138
+
139
+ ### 3. Generic Handlers (70% Boilerplate Reduction)
140
+
141
+ One line registers audit and metrics handlers for all events:
142
+
143
+ ```ruby
144
+ # In app/domains/user_management/setup.rb
145
+ SmartDomain::Event::Registration.register_standard_handlers(
146
+ domain: 'user',
147
+ events: %w[created updated deleted],
148
+ include_audit: true,
149
+ include_metrics: true
150
+ )
151
+ # Replaces ~50 lines of manual subscription code!
152
+ ```
153
+
154
+ ### 4. Custom Event Handlers
155
+
156
+ Add domain-specific side effects:
157
+
158
+ ```ruby
159
+ class UserWelcomeHandler < SmartDomain::Event::Handler
160
+ def can_handle?(event_type)
161
+ event_type == "user.created"
162
+ end
163
+
164
+ def handle(event)
165
+ UserMailer.welcome_email(event.user_id).deliver_later
166
+ end
167
+ end
168
+
169
+ # Register in setup.rb
170
+ welcome_handler = UserWelcomeHandler.new
171
+ SmartDomain::Event.bus.subscribe('user.created', welcome_handler)
172
+ ```
173
+
174
+ ### 5. Change Tracking
175
+
176
+ Track what changed in update events:
177
+
178
+ ```ruby
179
+ event = PostUpdatedEvent.new(
180
+ event_type: "post.updated",
181
+ aggregate_id: post.id,
182
+ aggregate_type: "Post",
183
+ organization_id: org.id,
184
+ changed_fields: ["published", "published_at"],
185
+ old_values: { "published" => false, "published_at" => nil },
186
+ new_values: { "published" => true, "published_at" => Time.current }
187
+ )
188
+ ```
189
+
190
+ ### 6. Authorization with Policies
191
+
192
+ Pundit-style policies for domain authorization:
193
+
194
+ ```ruby
195
+ class PostPolicy < ApplicationPolicy
196
+ def update?
197
+ user_present? && (
198
+ user.role == "admin" ||
199
+ (user.role == "editor" && owner?)
200
+ )
201
+ end
202
+
203
+ def destroy?
204
+ user_present? && user.role == "admin"
205
+ end
206
+
207
+ private
208
+
209
+ def owner?
210
+ record.user_id == user.id
211
+ end
212
+ end
213
+ ```
214
+
215
+ ### 7. Multi-tenancy
216
+
217
+ Organization-based data isolation:
218
+
219
+ ```ruby
220
+ # Set tenant context
221
+ SmartDomain::Integration::TenantContext.with_tenant(org.id) do
222
+ # All queries are automatically scoped to this organization
223
+ users = User.all # Only returns users from org
224
+ posts = Post.all # Only returns posts from org
225
+ end
226
+
227
+ # Models use TenantScoped mixin
228
+ class User < ApplicationRecord
229
+ include SmartDomain::Integration::TenantScoped
230
+ tenant_key :organization_id
231
+ end
232
+ ```
233
+
234
+ ### 8. ActiveRecord Integration
235
+
236
+ Transaction-safe event publishing:
237
+
238
+ ```ruby
239
+ class Post < ApplicationRecord
240
+ include SmartDomain::Integration::ActiveRecord
241
+
242
+ after_commit :publish_created_event, on: :create
243
+
244
+ def publish_created_event
245
+ event = PostCreatedEvent.new(...)
246
+ add_domain_event(event)
247
+ end
248
+ end
249
+ # Events only publish after transaction commits!
250
+ ```
251
+
252
+ ## Event Flow Example
253
+
254
+ When a user is created through the service:
255
+
256
+ ```
257
+ User.create! → UserService.create
258
+
259
+ UserCreatedEvent published
260
+
261
+ ├─→ AuditHandler (logs to Rails.logger + audit table)
262
+ ├─→ MetricsHandler (increments metrics)
263
+ └─→ UserWelcomeHandler (sends welcome email)
264
+ ```
265
+
266
+ All handlers run synchronously in the current thread with error isolation.
267
+
268
+ ## Running Tests
269
+
270
+ The example app doesn't include tests, but demonstrates patterns you can test:
271
+
272
+ ```ruby
273
+ # Test event publishing
274
+ expect {
275
+ service.create(User, attributes)
276
+ }.to publish_event(UserCreatedEvent)
277
+
278
+ # Test handlers
279
+ handler = UserWelcomeHandler.new
280
+ event = UserCreatedEvent.new(...)
281
+ expect { handler.handle(event) }.to send_email
282
+
283
+ # Test policies
284
+ policy = PostPolicy.new(user, post)
285
+ expect(policy.update?).to be true
286
+ ```
287
+
288
+ ## Viewing Logs
289
+
290
+ All events and handler executions are logged. Run the demo and watch the logs:
291
+
292
+ ```bash
293
+ rails runner lib/demo.rb | grep -E '\[SmartDomain|User|Post'
294
+ ```
295
+
296
+ You'll see:
297
+ - Event publications
298
+ - Handler subscriptions
299
+ - Audit logs
300
+ - Metrics increments
301
+ - Custom handler executions
302
+
303
+ ## Next Steps
304
+
305
+ 1. **Explore the generated files**: Look at the domain services, events, and policies
306
+ 2. **Customize the business logic**: Add validation, workflows, or complex rules
307
+ 3. **Add more handlers**: Create handlers for emails, notifications, webhooks
308
+ 4. **Try multi-tenancy**: Create multiple organizations and see data isolation
309
+ 5. **Add authorization**: Use policies in controllers to enforce permissions
310
+ 6. **Extend events**: Add more mixins like SecurityContextMixin or ReasonMixin
311
+
312
+ ## Learn More
313
+
314
+ - **Gem Documentation**: ../../README.md
315
+ - **SmartDomain GitHub**: https://github.com/rachid/smart_domain
316
+ - **Configuration**: config/initializers/smart_domain.rb
317
+
318
+ ## Key Takeaways
319
+
320
+ 1. **Services own business logic**, controllers delegate
321
+ 2. **Events are immutable** and published after transaction commits
322
+ 3. **Generic handlers** eliminate 70% of boilerplate code
323
+ 4. **Custom handlers** enable domain-specific side effects
324
+ 5. **Policies centralize** authorization logic
325
+ 6. **Multi-tenancy** is automatic with TenantContext
326
+ 7. **Everything is type-safe** and validated
327
+
328
+ This architecture scales from small apps to large platforms while maintaining clean, maintainable code.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
File without changes
@@ -0,0 +1,10 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css.
3
+ *
4
+ * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
5
+ * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
6
+ * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
7
+ * depending on specificity.
8
+ *
9
+ * Consider organizing styles into separate files for maintainability.
10
+ */
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ # Base API controller with common functionality
5
+ class BaseController < ApplicationController
6
+ # Skip CSRF for API requests
7
+ skip_before_action :verify_authenticity_token
8
+
9
+ # Simple authentication simulation
10
+ # In a real app, this would use JWT, sessions, etc.
11
+ before_action :set_current_organization
12
+ before_action :set_current_user
13
+
14
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
15
+ rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
16
+ rescue_from SmartDomain::Domain::Exceptions::UnauthorizedError, with: :forbidden
17
+
18
+ private
19
+
20
+ def set_current_organization
21
+ # In real app: decode from JWT, session, subdomain, etc.
22
+ # For demo: use header or first organization
23
+ org_id = request.headers['X-Organization-Id']
24
+ @current_organization = if org_id
25
+ Organization.find(org_id)
26
+ else
27
+ Organization.first || Organization.create!(name: "Demo Organization")
28
+ end
29
+ end
30
+
31
+ def set_current_user
32
+ # In real app: decode from JWT token
33
+ # For demo: use header or create demo user
34
+ user_id = request.headers['X-User-Id']
35
+ @current_user = if user_id
36
+ User.find(user_id)
37
+ else
38
+ # Create a demo admin user if none exists
39
+ @current_organization.users.first || @current_organization.users.create!(
40
+ email: "admin@example.com",
41
+ name: "Demo Admin",
42
+ role: "admin"
43
+ )
44
+ end
45
+ end
46
+
47
+ attr_reader :current_user, :current_organization
48
+
49
+ def not_found(exception)
50
+ render json: { error: exception.message }, status: :not_found
51
+ end
52
+
53
+ def unprocessable_entity(exception)
54
+ render json: { error: exception.message, details: exception.record.errors }, status: :unprocessable_entity
55
+ end
56
+
57
+ def forbidden(exception)
58
+ render json: { error: exception.message }, status: :forbidden
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V1
5
+ # Posts API Controller
6
+ #
7
+ # Demonstrates:
8
+ # - Thin controller delegating to PostService (domain layer)
9
+ # - Policy authorization using PostPolicy
10
+ # - Custom actions (publish/unpublish) triggering events
11
+ # - Change tracking in update events
12
+ class PostsController < Api::BaseController
13
+ before_action :set_post, only: [:show, :update, :destroy, :publish, :unpublish]
14
+
15
+ # GET /api/v1/posts
16
+ def index
17
+ @posts = SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
18
+ # Optional filter by published status
19
+ scope = Post.includes(:user)
20
+ scope = params[:published] == 'true' ? scope.published : scope if params[:published].present?
21
+ scope.all
22
+ end
23
+
24
+ render json: @posts, include: :user
25
+ end
26
+
27
+ # GET /api/v1/posts/:id
28
+ def show
29
+ authorize @post, :show?
30
+ render json: @post, include: :user
31
+ end
32
+
33
+ # POST /api/v1/posts
34
+ def create
35
+ service = PostManagement::PostService.new(
36
+ current_user: current_user,
37
+ organization_id: current_organization.id
38
+ )
39
+
40
+ # Service handles:
41
+ # 1. Validation
42
+ # 2. Creating the post
43
+ # 3. Publishing PostCreatedEvent
44
+ # 4. Triggering audit and metrics handlers
45
+ @post = service.create_post(post_params.merge(user_id: current_user.id))
46
+
47
+ render json: @post, status: :created
48
+ end
49
+
50
+ # PATCH/PUT /api/v1/posts/:id
51
+ def update
52
+ authorize @post, :update?
53
+
54
+ service = PostManagement::PostService.new(
55
+ current_user: current_user,
56
+ organization_id: current_organization.id
57
+ )
58
+
59
+ # Service publishes PostUpdatedEvent with change tracking
60
+ @post = service.update_post(@post, post_params)
61
+
62
+ render json: @post
63
+ end
64
+
65
+ # DELETE /api/v1/posts/:id
66
+ def destroy
67
+ authorize @post, :destroy?
68
+
69
+ service = PostManagement::PostService.new(
70
+ current_user: current_user,
71
+ organization_id: current_organization.id
72
+ )
73
+
74
+ # Service publishes PostDeletedEvent
75
+ service.delete_post(@post)
76
+
77
+ head :no_content
78
+ end
79
+
80
+ # POST /api/v1/posts/:id/publish
81
+ # Custom action demonstrating change tracking events
82
+ def publish
83
+ authorize @post, :update?
84
+
85
+ # Track old state for change tracking
86
+ old_published = @post.published
87
+ old_published_at = @post.published_at
88
+
89
+ @post.publish!
90
+
91
+ # Publish event with change tracking
92
+ event = PostUpdatedEvent.new(
93
+ event_type: "post.updated",
94
+ aggregate_id: @post.id.to_s,
95
+ aggregate_type: "Post",
96
+ organization_id: current_organization.id.to_s,
97
+ actor_id: current_user.id.to_s,
98
+ actor_email: current_user.email,
99
+ post_id: @post.id.to_s,
100
+ changed_fields: ["published", "published_at"],
101
+ old_values: { "published" => old_published, "published_at" => old_published_at },
102
+ new_values: { "published" => true, "published_at" => @post.published_at.iso8601 }
103
+ )
104
+
105
+ SmartDomain::Event.bus.publish(event)
106
+
107
+ render json: @post
108
+ end
109
+
110
+ # POST /api/v1/posts/:id/unpublish
111
+ def unpublish
112
+ authorize @post, :update?
113
+
114
+ old_published = @post.published
115
+ old_published_at = @post.published_at
116
+
117
+ @post.unpublish!
118
+
119
+ # Publish event with change tracking
120
+ event = PostUpdatedEvent.new(
121
+ event_type: "post.updated",
122
+ aggregate_id: @post.id.to_s,
123
+ aggregate_type: "Post",
124
+ organization_id: current_organization.id.to_s,
125
+ actor_id: current_user.id.to_s,
126
+ actor_email: current_user.email,
127
+ post_id: @post.id.to_s,
128
+ changed_fields: ["published", "published_at"],
129
+ old_values: { "published" => old_published, "published_at" => old_published_at&.iso8601 },
130
+ new_values: { "published" => false, "published_at" => nil }
131
+ )
132
+
133
+ SmartDomain::Event.bus.publish(event)
134
+
135
+ render json: @post
136
+ end
137
+
138
+ private
139
+
140
+ def set_post
141
+ @post = SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
142
+ Post.find(params[:id])
143
+ end
144
+ end
145
+
146
+ def post_params
147
+ params.require(:post).permit(:title, :body, :published)
148
+ end
149
+
150
+ def authorize(post, action)
151
+ policy = PostPolicy.new(current_user, post)
152
+ unless policy.public_send(action)
153
+ raise SmartDomain::Domain::Exceptions::UnauthorizedError, "Not authorized to #{action} this post"
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V1
5
+ # Users API Controller
6
+ #
7
+ # Demonstrates:
8
+ # - Thin controller delegating to UserService (domain layer)
9
+ # - Policy authorization using UserPolicy
10
+ # - Multi-tenancy with current_organization
11
+ # - Events automatically published via service layer
12
+ class UsersController < Api::BaseController
13
+ before_action :set_user, only: [:show, :update, :destroy]
14
+
15
+ # GET /api/v1/users
16
+ def index
17
+ # Automatically scoped to current_organization via TenantScoped
18
+ @users = SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
19
+ User.all
20
+ end
21
+
22
+ render json: @users
23
+ end
24
+
25
+ # GET /api/v1/users/:id
26
+ def show
27
+ authorize @user, :show?
28
+ render json: @user
29
+ end
30
+
31
+ # POST /api/v1/users
32
+ def create
33
+ service = UserManagement::UserService.new(
34
+ current_user: current_user,
35
+ organization_id: current_organization.id
36
+ )
37
+
38
+ # Service handles:
39
+ # 1. Business logic validation
40
+ # 2. Creating the user
41
+ # 3. Publishing UserCreatedEvent
42
+ # 4. Triggering handlers (audit, metrics, welcome email)
43
+ @user = service.create_user(user_params)
44
+
45
+ render json: @user, status: :created
46
+ end
47
+
48
+ # PATCH/PUT /api/v1/users/:id
49
+ def update
50
+ authorize @user, :update?
51
+
52
+ service = UserManagement::UserService.new(
53
+ current_user: current_user,
54
+ organization_id: current_organization.id
55
+ )
56
+
57
+ # Service publishes UserUpdatedEvent with change tracking
58
+ @user = service.update_user(@user, user_params)
59
+
60
+ render json: @user
61
+ end
62
+
63
+ # DELETE /api/v1/users/:id
64
+ def destroy
65
+ authorize @user, :destroy?
66
+
67
+ service = UserManagement::UserService.new(
68
+ current_user: current_user,
69
+ organization_id: current_organization.id
70
+ )
71
+
72
+ # Service publishes UserDeletedEvent
73
+ service.delete_user(@user)
74
+
75
+ head :no_content
76
+ end
77
+
78
+ private
79
+
80
+ def set_user
81
+ @user = SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
82
+ User.find(params[:id])
83
+ end
84
+ end
85
+
86
+ def user_params
87
+ params.require(:user).permit(:email, :name, :role)
88
+ end
89
+
90
+ def authorize(user, action)
91
+ policy = UserPolicy.new(current_user, user)
92
+ unless policy.public_send(action)
93
+ raise SmartDomain::Domain::Exceptions::UnauthorizedError, "Not authorized to #{action} this user"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
3
+ allow_browser versions: :modern
4
+
5
+ # Changes to the importmap will invalidate the etag for HTML responses
6
+ stale_when_importmap_changes
7
+ end
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartDomain
4
+ module Domain
5
+ module Exceptions
6
+ # Base domain exception
7
+ class DomainError < StandardError; end
8
+
9
+ # Authorization error
10
+ class UnauthorizedError < DomainError; end
11
+
12
+ # Validation error
13
+ class ValidationError < DomainError; end
14
+
15
+ # Not found error
16
+ class NotFoundError < DomainError; end
17
+ end
18
+ end
19
+ end